Note: see the link below for the English version of this article.
https://duongnt.com/string-interning
Như ta đã biết, string
trong C# là immutable reference type. Vì thế khi ta tạo nhiều string với cùng nội dung, tất cả các string đó sẽ được cấp phát bộ nhớ một cách độc lập cho dù giá trị của chúng là như nhau. Điều này sẽ dẫn đến lãng phí bộ nhớ. Để cải thiện tình hình, CLR sử dụng một tính năng gọi là string interning. Với tính năng này, nhiều string với cùng nội dung có thể tham chiếu tới cùng một vùng trong bộ nhớ.
Điểm cần lưu ý khi so sánh string
Khi sử dụng string
, nhiều khi ta quên mất rằng string
thực ra là reference type. Đó là vì trong .NET, kiểu string
được thiết kế để kết quả khi so sánh chúng là đúng như cách suy nghĩ thông thường. Ta xem thử đoạn code dưới đây.
var s1 = "An ";
var s2 = "example";
var s3 = "An example";
var s4 = s1 + s2;
Console.WriteLine(s3 == s4); // True
Console.WriteLine(s3.Equals(s4)); // True
Nhưng thực ra s3
và s4
là 2 object hoàn toàn khác biệt.
Console.WriteLine(ReferenceEquals(s3, s4)); // False
Lý do lệnh s3.Equals(s4)
trả về True
trong đoạn code trước đó là vì hàm Equals
trong class string
đã được override như ta thấy tại đây. Nó gọi hàm EqualsHelper
để so sánh từng ký tự trong 2 string và trả về true
nếu nội dung của chúng là trùng nhau.
if (this == null) //this is necessary to guard against reverse-pinvokes and
throw new NullReferenceException(); //other callers who do not use the callvirt instruction
if (value == null)
return false;
if (Object.ReferenceEquals(this, value))
return true;
if (this.Length != value.Length)
return false;
return EqualsHelper(this, value);
String interning cho literals
Khi ta khai báo một string literal, nó sẽ được intern. Có nghĩa là khi tạo thêm các string literal khác với cùng nội dung, chúng vẫn sẽ tham chiếu tới cùng một vùng trong bộ nhớ.
var s1 = "An example";
var s2 = "An example";
Console.WriteLine(ReferenceEquals(s1, s2)); // True
Ta cũng có thể định nghĩa string literal như dưới đây.
var s1 = "An example";
var s2 = "An " + "example";
Console.WriteLine(ReferenceEquals(s1, s2)); // True
String literal có những lợi thế như sau.
- Vì tất cả các string với cùng nội dung đều tham chiếu tới cùng một vùng trong bộ nhớ nên ta tiết kiệm được bộ nhớ. Vì thế ta cũng giúp giảm tải cho Garbage Collector.
- Việc so sánh 2 string đã được intern cũng nhanh hơn vì ta chỉ cần so sánh reference của chúng thay vì phải so sánh nội dung.
Vậy tại sao ta lại chỉ intern cho string literal mà không intern cả các string được tạo bằng code? Lý do của điều đó được giải thích trong bài blog này của Eric Lippert. Nói ngắn gọn thì việc intern một cách tùy tiện sẽ có những tác hại sau.
- Khi có quá nhiều string được intern, việc tạo một string mới sẽ tốn nhiều tài nguyên hơn. Đó là vì ta phải kiểm tra xem string mới đã được intern sẵn rồi hay chưa. Ta có thể dùng bảng hash để giúp việc tìm kiếm này nhanh hơn. Nhưng việc duy trì một bảng hash lớn cũng là tốn kém, và việc tính hash tất cả các string cũng không phải là miễn phí.
- Khi một string đã được intern, string đó sẽ tồn tại trong bộ nhớ cho đến khi ứng dụng được đóng lại. Garbage Collector không thể giải phỏng phần bộ nhớ mà string đó sử dụng. Điều này có thể dẫn đến rò rỉ bộ nhớ.
Intern string một cách thủ công
Mặc dù vậy, ta vẫn có thể tự mình quản lý việc intern các string bằng 2 hàm string.Intern
và string.IsInterned
. Khi ta gọi hàm string.Intern
và truyền vào một string, hàm này sẽ trả về reference tới string đó nếu string đã được intern. Còn nếu string chưa được intern thì string.Intern
sẽ intern string và trả về reference.
var s1 = "An ";
var s2 = "example";
var s3 = "An example";
var s4 = string.Intern(s1 + s2);
Console.WriteLine(ReferenceEquals(s3, s4)); // True
Ta có thể dùng hàm string.IsInterned
để kiểm tra xem một string đã được intern hay chưa. Nếu string đã được intern, hàm này sẽ trả về reference tới string đó. Còn nếu string chưa được intern, string.IsInterned
sẽ trả về null.
var s1 = "An ";
var s2 = "example";
var s3 = string.Intern(s1 + s2);
Console.WriteLine(string.IsInterned(s3)); // "An example"
Console.WriteLine(string.IsInterned(s3 + "1")); // "null"
Cần nhớ rằng dù ta chỉ dùng string literal làm biến tạm thì string literal đó sẽ vẫn được intern. Đoạn code dưới đây có thể làm ta bất ngờ.
var s1 = string.IsInterned("Một string không được dùng ở bất kỳ đâu khác.");
Console.WriteLine(s1); // "Một string không được dùng ở bất kỳ đâu khác."
Sau khi intern thì các string được lưu ở đâu?
Trong trường hợp ta tự intern một string sau khi khởi tạo nó bằng code, ta có thể tham khảo sơ đồ sau. Sơ đồ này được lấy từ cuốn Pro .NET Memory Management của tác giả Konrad Kokosa.
Giá trị hash của từng string sau khi intern được lưu trong StringLiteralMap ở tại Private Heap, đây là phần bộ nhớ không chịu sự quản lý của runtime. StringLiteralMap lưu hash này cùng với địa chỉ tương ứng của string trỏ tới LargeHeapHandleTable. LargeHeapHandleTable nằm trong Large Object Heap và chịu sự quản lý của runtime. Trong LargeHeapHandleTable có reference tới phần bộ nhớ lưu giá trị của string ở trong Managed Heap. Tùy vào kích cỡ của string mà string đó sẽ nằm trong Small Object Heap hoặc Large Object Heap (đối với string lớn hơn 85.000 byte).
Đối với string literal, khi mã nguồn được compile, chúng sẽ được lưu vào trong metadata. Khi ta có nhiều string literal với cùng nội dung, compiler cũng chỉ ghi nội dung đó vào metadata một lần duy nhất. Và khi đoạn code đó được sử dụng, giá trị hash của từng string literal trong metadata sẽ được so sánh với các giá trị trong StringLiteralMap. Chỉ những string nào chưa có sẵn hash trong StringLiteralMap thì mới được khởi tạo và cấp phát bộ nhớ.
Kết thúc
String interning là một tính năng tương đối thú vị, nhưng ta lại không mấy khi cần đến nó trong thực tế. Và nếu ta lạm dụng tính năng này thì hiệu năng của ứng dụng của ta sẽ bị ảnh hưởng xấu. Ta chỉ nên cân nhắc sử dụng string interning nếu ta cần tạo rất nhiều string mà nội dung của chúng lại hay trùng nhau.
One Thought on “Khái quát về string interning trong C#”