Search…

Tối Ưu Xử Lý Chuỗi với StringBuilder - Phần 1

16/08/20218 min read
Bài viết giới thiệu và phân tích hiệu năng khi xử lí chuỗi với đối tượng String và StringBuilder trong C#.

Sự khác nhau giữa việc xử lí chuỗi với StringStringBuilder, phân tích ưu nhược điểm của cả hai nhằm giúp độc giả có cái nhìn tổng quan về hai đối tượng này. Bài viết sử dụng ngôn ngữ lập trình C# (nền tảng .NET 4.0 trở lên) để minh họa cho các ví dụ.

* Ngoài ra bạn có thể xem như tương tự cho sự khác biệt giữa String và StringBuilder trong Java.

Nhu cầu xử lí chuỗi

Trong lập trình nói chung, nhu cầu xử lí thông tin là vô cùng cần thiết. Những thông tin này, còn gọi là Input, đều được người dùng đưa vào dưới dạng các chuỗi kí tự được chúng ta định sẵn. Do đó, việc xử lí chuỗi kí tự trong lập trình đã trở thành bước quan trọng nhưng cơ bản nhất để ứng dụng có thể "hiểu" những yêu cầu từ người dùng.

Trong các ngôn ngữ phát triển đi trước như C, việc xử lí chuỗi được các lập trình viên xử lí thông qua việc xử lí trên mảng kí tự char[] đi kèm với các phương thức xử lí cần thiết. Tuy nhiên đến thế hệ các ngôn ngữ lập trình hướng đối tượng, chúng ta phát sinh ra nhu cầu, tại sao không hiện thực một đối tượng sao cho bản thân đối tượng ấy có khả năng nhận được dữ liệu dạng chuỗi kí tự và đi kèm theo là các phương thức xử lí mạnh mẽ được tích hợp đồng bộ đi kèm. Như thế, một đối tượng với tên gọi String được ra đời.

String và StringBuilder

String

String là một đối tượng trong C# (tương tự như một số ngôn ngữ lập trình hướng đối tượng khác) dùng để hiện thực một chuỗi kí tự Unicode (rỗng hoặc khác rỗng).

StringBuilder

String không tốt cho các trường hợp đồi hỏi hiệu năng, vì vậy StringBuilder ra đời, là một đối tượng chuyên xử lí chuỗi với hiệu năng cao hơn.

Vấn đề xử lí chuỗi với String và StringBuilder

Cấu trúc tổ chức String và StringBuilder

String

Là một đối tượng kiểu immutable (bất biến) và sealed (bị niêm phong, không có khả năng tạo ra kế thừa), nghĩa là khi bạn tạo mới một đối tượng, các giá trị của đối tượng này hoàn toàn không thể thay đổi.

Sự thay đổi giá trị của String nếu diễn ra sẽ diễn ra trên một đối tượng mới.

StringBuilder

public sealed class StringBuilder : ISerializable
{
    // Fields
    private const string CapacityField = "Capacity";
    internal const int DefaultCapacity = 0x10;
    internal char[] m_ChunkChars;
    internal int m_ChunkLength;
    internal int m_ChunkOffset;
    internal StringBuilder m_ChunkPrevious;
    internal int m_MaxCapacity;
    private const string MaxCapacityField = "m_MaxCapacity";
    internal const int MaxChunkSize = 0x1f40;
    private const string StringValueField = "m_StringValue";
    private const string ThreadIDField = "m_currentThread";
}

Nhìn vào cấu trúc trên ta có vài nhận xét như sau:

  • internal char[] m_ChunkChars: StringBuilder dùng một mảng char để lưu trữ các dữ liệu.
  • internal StringBuilder m_ChunkPrevious: StringBuilder được tổ chức như một danh sách liên kết, do đó việc thao tác chuỗi cơ bản như chèn, xóa, cắt, tìm kiếm chuỗi… sẽ là những thao tác trên danh sách liên kết.
  • internal int m_MaxCapacity: StringBuilder cũng có giới hạn sức chứa dữ liệu.

Hiệu năng của String và StringBuilder

Hiệu năng của String

Trước khi vào hiệu năng của StringBuilder, hãy xem xét một chút vấn đề hiệu năng của String để hiểu rõ hơn sự khác nhau biệt với StringBuilder.

Tạo một đối tượng String và gán cho nó một giá trị là Hello.

String MyExampleString = new String();
MyExampleString = "Hello";
01
Bộ nhớ lưu trữ "Hello"

MyExampleString được khởi tạo và cấp phát một vùng nhớ tại 0x90000.

Nối thêm chuỗi choMyExampleString:

MyExampleString = MyExampleString + ", is it me you're looking for";
02

MyExampleString tham chiếu đến một đối tượng khác (tạm gọi là UndentifyString) tại một vùng nhớ mới 0x83320 và gán giá trị mới vào.

Sau khi quá trình gán giá trị hoàn tất tại UndentifyString, MyExampleString sẽ tham chiếu đến UndentifyString. Vùng nhớ cũ 0x90000 sẽ được Garbage Collector dọn dẹp.

Hiệu năng của StringBuilder

Minh họa 1

Tạo một đối tượng StringBuilder MyExampleStringBuilder và gán giá trị tương tự như ví dụ trên.

StringBuilder MyExampleStringBuilder = new StringBuilder();
MyExampleStringBuilder.Append(“Hello”);
01
Bộ nhớ lưu trữ "Hello"

MyExampleStringBuilder được khởi tạo và cấp phát một vùng nhớ là 0x90000 (giả sử vùng nhớ 0x90000 ở ví dụ trên đã được thu hồi).

Thêm dữ liệu vào để MyExampleStringBuilder thành Hello, is it me you're looking for

MyExampleStringBuilder.Append(", is it me you're looking for");
03

StringBuilder lại có cơ chế hoàn toàn khác. Thay vì tạo mới vùng nhớ như String, StringBuilder có khả năng thao tác trên chính vùng nhớ được cấp phát.

Minh họa 2

Trong minh họa này, tôi sẽ tính toán thời gian thực thi của StringStringBuilder với cùng một yêu cầu.

Cụ thể tôi sẽ tạo 2 đối tượng StringStringBuilder chứa kí tự rỗng. Lần lượt cộng giá trị “X” vào giá trị của 2 đối tượng trên với số lần cộng đều là 100 ngàn lần. Hãy xem thời gian thực thi phương thức trên của 2 đối tượng StringStringBuilder.

Khởi tạo các biến cần thiết.

const int Length = 1;
const int Loops = 100000;
DateTime StartTime;
DateTime EndTime;
int i;
string TextSource = new String('X', Lenght);
string TextDest = "";
Thực thi cộng chuỗi cho TestDest bằng toán tử + của String với số lần là Loops và tính toán thời gian thực thi với StartTimeEndTime.
StartTime = DateTime.Now;
for (i = 0; i < Loops; i++)
    TextDest += TextSource;
EndTime = DateTime.Now;
Console.WriteLine("String took " + (EndTime - StartTime).TotalSeconds + " seconds.");

Kết quả nhận được: (EndTime - StartTime).TotalSeconds  =~ 2.5 giây.

Thực thi cộng chuỗi cho TestDest bằng phương thức Append() của StringBuilder với số lần là Loops và tính toán thời gian thực thi với StartTimeEndTime.

StartTime = DateTime.Now;
StringBuilder sb = new StringBuilder((int)(Lenght * Loops * 1.1));
for (i = 0; i < Loops; i++) 
    sb.Append(TextSource);
EndTime = DateTime.Now;
Console.WriteLine("String Builder took " + (EndTime - StartTime).TotalSeconds + " seconds.");

Kết quả nhận được: (EndTime - StartTime).TotalSeconds  =~ 0.001 giây.

Kết quả thời gian thực thi lệnh với cùng một điều kiện của StringStringBuilder rất chênh lệch.

Với các thao tác chuỗi mà có sự thay đổi giá trị với String, cứ mỗi lệnh thao tác chuỗi được thực thi, String sẽ tạo hẳn một đối tượng mới đi kèm là vùng nhớ mới đồng thời sao chép giá trị của đối tượng cũ ở vùng nhớ cũ và kết quả mà lệnh thao tác chuỗi thực thi vào đối tượng mới và vùng nhớ mới. Ví dụ như ở minh họa trên:

  • Khi i = 0, TextDest = "", giả sử vùng nhớ TestDestA
  • Gọi lệnh TextDest += TextSource
  • TextDest tạo ra một đối tượng String khác (tạm gọi là UndentifyString) được cấp vùng nhớ mới là B và giá trị vùng nhớ này là kết quả của việc sao chép giá trị hiện tại của TextDest (vùng nhớ A) là giá trị rỗng "" cộng với giá trị mới được đưa vào của TextSource"X". Sau khi quá trình sao chép hoàn tất, TestDest tham chiếu tới vùng nhớ của UndentifyString.

Vậy nên giá trị mới của TextDest tại vùng nhớ B“X”. Vùng nhớ cũ TestDest sẽ được Garbage Collector dọn dẹp.

Việc này được lặp lại cho đến khi vòng lặp kết thúc.

Một số lượng rất lớn đối tượng phụ đi kèm là những vùng nhớ mới được tạo ra để phục vụ thao tác cộng chuỗi đơn giản. Sau đó là quá trình sao chép dữ liệu sang đối tượng mới, làm tốn tài nguyên máy tính lẫn mang nhiều nguy cơ tiềm ẩn trong quá trình thực thi chương trình, cũng như thời gian thực thi lệnh sẽ rất dài.

Tuy nhiên, các thao chuỗi của StringBuilder lại chỉ diễn ra trên vùng nhớ chúng được cấp phát do chúng dùng một mảng dữ liệu để lưu trữ. Điều này nghĩa là sự thay đổi về giá trị của StringBuilder hoàn toàn được xử lí “nội bộ”. Do vậy tài nguyên cũng như thời gian thực thi của StringBuilder là tốt hơn String.

Kết luận

  • Việc thao tác với chuỗi (gán, cắt, thêm…) với StringBuilder diễn ra ngay trên đối tượng được cấp phát.
  • Ngược lại, String sẽ tạo ra đối tượng mới.
  • Về hiệu năng, StringBuilder tỏ ra vượt trội hơn so với String khi nghiệp vụ bạn đang xử lí đòi hỏi việc thao tác chuỗi mà giá trị chuỗi biến động với số lượng lớn.
  • Tuy nhiên StringBuilder không hỗ trợ vấn đề thao tác chuỗi mạnh mẽ như String, như nhiều toán tử thao tác như +, +=, =, … cũng như các phương thức như Contains, ToList, ToArray, ...
  • Nếu nghiệp vụ bạn đang xử lí yêu cầu việc thao tác chuỗi mà giá trị của chuỗi biến động ít, String hoàn toàn là sự lựa chọn hợp lí bởi sự hỗ trợ đa dạng và đầy đủ các toán tử thao tác chuỗi và phương thức tích hợp đi kèm và cả tốc độ lập trình.
IO Stream

IO Stream Co., Ltd

30 Trinh Dinh Thao, Hoa Thanh ward, Tan Phu district, Ho Chi Minh city, Vietnam
+84 28 22 00 11 12
developer@iostream.co

383/1 Quang Trung, ward 10, Go Vap district, Ho Chi Minh city
Business license number: 0311563559 issued by the Department of Planning and Investment of Ho Chi Minh City on February 23, 2012

©IO Stream, 2013 - 2024