Tìm hiểu về Frozen collections trong .Net 8

4 min read

Frozen collections

Hãy thảo luận trước về ý nghĩa của việc một list được đóng băng (frozen collections). Và chúng ta sẽ làm điều này với một số code, hoạt động trên phiên bản .NET 8:

List<int> normalList = new List<int> { 1, 2, 3 };
ReadOnlyCollection<int> readonlyList = normalList.AsReadOnly();
FrozenSet<int> frozenSet = normalList.ToFrozenSet();
ImmutableList<int> immutableList = normalList.ToImmutableList();

normalList.Add(4);

Console.WriteLine($"List count: {normalList.Count}");
Console.WriteLine($"ReadOnlyList count: {readonlyList.Count}");
Console.WriteLine($"FrozenSet count: {frozenSet.Count}");
Console.WriteLine($"ImmutableList count: {immutableList.Count}");

Kết quả:

List count: 4
ReadOnlyList count: 4
FrozenSet count: 3
ImmutableList count: 3

Hãy xem xét từng điểm một:

  • Một ReadOnlyList chỉ là một “view” của đối tượng mà nó được tạo ra từ đó. Vì vậy nếu bạn cập nhật list ban đầu mà nó được tạo ra, ReadOnlyList cũng sẽ phản ánh thay đổi đó. Người dùng của ReadOnlyList không có cách nào để thay đổi trạng thái bên trong của chính list đó.
  • Một collection có thể đóng băng là một collection có thể được thay đổi cho đến khi nó bị đóng băng. Một khi đã bị đóng băng, nó sẽ không phản ánh các thay đổi nữa. Đó là lý do tại sao chúng ta thấy số lượng phần tử là 3 thay vì 4 (như trong ReadOnlyList).
  • List immutable cũng chỉ chứa 3 phần tử, vậy list immutable khác với đối tượng frozen của chúng ta như thế nào?

Trong ví dụ này có 2 sự khác biệt chính. Thứ nhất, đối tượng frozen là một Set chứ không phải một list (lý do chính: hiện tại không có FrozenList hoặc FrozenCollection). Một set là không có thứ tự và không thể chứa các phần tử trùng lặp. Điều này có thể thay đổi trong tương lai, hãy xem phần tuyên bố ở đầu. Thứ hai và là yếu tố chính: Các collection immutable có các cách hiệu quả để thay đổi list bằng cách tạo một list mới:CopyR

var immutableList = normalList.ToImmutableList();
// This will create a new immutable List
var newList = immutableList.Add(2);

Chúng ta không thể tìm thấy behaviour này với các collection đã đóng băng (frozen collections). Hiện tại .NET 8 về cơ bản có hai loại collection đóng băng:

  • FrozenSet
  • FrozenDictionary

Thế thì tại sao?

Câu hỏi đặt ra là: Tại sao chúng ta phải giới thiệu khái niệm này?. Và đó là một câu hỏi hay. Liệu chúng ta không thể giải quyết vấn đề này với cách tiếp cận hiện tại của các đối tượng read-only hoặc immutable hay sao? Câu trả lời là có và không. Các đối tượng frozen, do có những hạn chế như vậy, có thể mang lại một số lợi ích về hiệu năng. Framework ngày càng được tối ưu hóa với mỗi bản cập nhật và Microsoft tiếp tục hành trình đó. Thường thì các collection được khởi tạo tại một thời điểm nào đó nhưng sẽ không bao giờ thay đổi trạng thái của chúng.

Benchmark

Hãy xem xét các con số và xem nơi mà frozen set, ví dụ như, có hiệu năng vượt trội hơn list (cổ điển) của chúng ta:

public class LookupBenchmark
{
    private const int Iterations = 1000;
    private readonly List<int> list = Enumerable.Range(0, Iterations).ToList();
    private readonly FrozenSet<int> frozenSet = Enumerable.Range(0, Iterations).ToFrozenSet();
    private readonly HashSet<int> hashSet = Enumerable.Range(0, Iterations).ToHashSet();
    private readonly ImmutableHashSet<int> immutableHashSet= Enumerable.Range(0, Iterations).ToImmutableHashSet();

    [Benchmark(Baseline = true)]
    public void LookupList()
    {
        for (var i = 0; i < Iterations; i++)
            _ = list.Contains(i);
    }

    [Benchmark]
    public void LookupFrozen()
    {
        for (var i = 0; i < Iterations; i++)
            _ = frozenSet.Contains(i);
    }

    [Benchmark]
    public void LookupHashSet()
    {
        for (var i = 0; i < Iterations; i++)
            _ = hashSet.Contains(i);
    }

    [Benchmark]
    public void LookupImmutableHashSet()
    {
        for (var i = 0; i < Iterations; i++)
            _ = immutableHashSet.Contains(i);
    }
}

Kết quả:

BenchmarkDotNet=v0.13.2, OS=macOS Monterey 12.6.1 (21G217) [Darwin 21.6.0]
Apple M1 Pro, 1 CPU, 10 logical and 10 physical cores
.NET SDK=8.0.100-alpha.1.22570.9
  [Host]     : .NET 8.0.0 (8.0.22.55902), Arm64 RyuJIT AdvSIMD
  DefaultJob : .NET 8.0.0 (8.0.22.55902), Arm64 RyuJIT AdvSIMD


|                 Method |      Mean |     Error |    StdDev | Ratio |
|----------------------- |----------:|----------:|----------:|------:|
|             LookupList | 57.561 us | 0.1346 us | 0.1124 us |  1.00 |
|           LookupFrozen |  1.963 us | 0.0119 us | 0.0093 us |  0.03 |
|          LookupHashSet |  2.997 us | 0.0314 us | 0.0294 us |  0.05 |
| LookupImmutableHashSet | 15.422 us | 0.3034 us | 0.6333 us |  0.26 |

thứ này nhanh hơn nhiều so với List thông thường! Và đây là ví dụ điển hình về việc chúng ta sẽ sử dụng FrozenSet hoặc các set nói chung. Điều tương tự cũng áp dụng cho FrozenDictionary. Để công bằng, chúng ta phải so sánh với HashSet thông thường và ImmutableHashSet. Và ở đây chúng ta cũng thấy rằng FrozenSet có hiệu năng vượt trội hơn.

Thông tin thêm: ImmutableHashSet đã có một số cải tiến về hiệu năng với .NET 8. Nếu bạn thực hiện cùng một benchmark với .NET 7, ImmutableHashSet sẽ chậm hơn!

Tạo FrozenSet

Tất nhiên, việc thể hiện các thao tác tìm kiếm và những thứ tương tự thì rất tốt, nhưng có thể được xem là mới một nửa câu chuyện. Việc tạo các set tốn kém hơn so với việc tạo một List<int> đơn giản. Điều này cũng phản ánh trường hợp sử dụng điển hình của FrozenSet. Bạn tạo chúng một lần ở đầu và sau đó thường xuyên truy cập chúng (ví dụ thông qua tìm kiếm). Đây là một số con số. Chúng ta sẽ tạo các đối tượng đó dựa trên một mảng đã có sẵn:

private readonly int[] from = Enumerable.Range(0, 1000).ToArray();

[Benchmark(Baseline = true)]
public List<int> CreateList() => from.ToList();

[Benchmark]
public FrozenSet<int> CreateFrozenList() => from.ToFrozenSet();

[Benchmark]
public HashSet<int> CreateHashSet() => from.ToHashSet();

[Benchmark]
public ImmutableHashSet<int> CreateImmutableHashSet() => from.ToImmutableHashSet();

Kết quả:

|                 Method |         Mean |     Error |    StdDev |  Ratio | RatioSD |
|----------------------- |-------------:|----------:|----------:|-------:|--------:|
|             CreateList |     251.9 ns |   2.41 ns |   2.14 ns |   1.00 |    0.00 |
|       CreateFrozenList |  57,626.2 ns | 973.67 ns | 910.77 ns | 228.94 |    4.17 |
|          CreateHashSet |   6,694.7 ns | 132.08 ns | 171.74 ns |  26.72 |    0.74 |
| CreateImmutableHashSet | 200,541.6 ns | 972.71 ns | 812.25 ns | 796.58 |    8.33 |

Kết luận

.NET Framework sẽ có ngày càng nhiều kiểu dữ liệu chuyên biệt hơn, có thể giúp chúng ta thể hiện ý định tốt hơn và cũng giúp chúng ta viết code có hiệu năng cao. Một bổ sung đáng chào đón cho điều này là các frozen collections!

Microsoft Document: link

Avatar photo

Leave a Reply

Your email address will not be published. Required fields are marked *