DotNETWeekly-io / DotNetWeekly

DotNet weekly newsletter
MIT License
198 stars 3 forks source link

【文章推荐】不可变字典比较 #666

Closed gaufung closed 2 weeks ago

gaufung commented 1 month ago

https://goatreview.com/choosing-best-immutable-dictionary-csharp-projects/

gaufung commented 4 weeks ago

C# 中有 ReadOnlyDictionary, ImmutableDictionaryForzenDictionary 三种类型,那么它们在功能和性能上有什么区别呢?

[Benchmark]
public void Create_ReadOnlyDictionary() {
  var dictionnary = new Dictionary<string, Goat>();
  for (int i = 0; i < 1000000; i++) {
    dictionnary.Add(i.ToString(), new Goat{Name = i.ToString()});
  }
  var result = new ReadOnlyDictionary<string, Goat>(dictionnary);
}

[Benchmark]
public void Create_ImmutableDictionary() {
  var dictionnary = new Dictionary<string, Goat>();
  for (int i = 0; i < 1000000; i++) {
    dictionnary.Add(i.ToString(), new Goat{Name = i.ToString()});
  }
  var result = dictionnary.ToImmutableDictionary();
}

[Benchmark]
public void Create_FrozenDictionary() {
  var dictionnary = new Dictionary<string, Goat>();
  for (int i = 0; i < 1000000; i++) {
    dictionnary.Add(i.ToString(), new Goat{Name = i.ToString()});
  }
  var result = dictionnary.ToFrozenDictionary();
}

Benchmark 的结果如下

Method Mean Error StdDev Gen0 Gen1 Gen2 Allocated
Create_ReadOnlyDictionary 967.4 us 742.6 us 40.70 us 220.7031 220.7031 220.7031 1.72 MB
Create_ImmutableDictionary 6,556.5 us 27,352.5 us 1,499.28 us 218.7500 218.7500 218.7500 2.33 MB
Create_FrozenDictionary 2,985.9 us 7,518.3 us 412.10 us 359.3750 359.3750 359.3750 2.6 MB

可以看出 ReadOnlyDictionary 创建最快,FrozenDictionary 次之,最后是 ImmutableDictionary 最慢。 原因是什么呢? ReadOnlyDictionary 是复用了底层 Dictionary 结构,只不过丢弃了修改的操作,比如 Add 等,所以如果还能访问底层的 Dictionary, 也是有机会去修改 ReadOnlyDictionary; ImmutableDictionaryImmutable 的实现,当你在修改这个字典的时候,它会创建一个新的字典,而保持原有的不变;

[Benchmark]
public void TryGetValue_ReadOnlyDictionary() {
  for (int i = 0; i < 1000000; i++) {
    var index = Random.Shared.Next(0, N);
    _readOnlyDictionary.TryGetValue(index.ToString(), out var value);
  }
}

[Benchmark]
public void TryGetValue_ImmutableDictionary() {
  for (int i = 0; i < 1000000; i++) {
    var index = Random.Shared.Next(0, N);
    _immutableDictionary.TryGetValue(index.ToString(), out var value);
  }
}

[Benchmark]
public void TryGetValue_FrozenDictionary() {
  for (int i = 0; i < 1000000; i++) {
    var index = Random.Shared.Next(0, N);
    _frozenDictionary.TryGetValue(index.ToString(), out var value);
  }
}

Benchmark 结果如下

Method Mean Error StdDev Gen0 Allocated
TryGetValue_ReadOnlyDictionary 676.7 us 502.6 us 27.55 us 31.2500 303.14 KB
TryGetValue_ImmutableDictionary 1,514.5 us 845.4 us 46.34 us 31.2500 303.12 KB
TryGetValue_FrozenDictionary 391.3 us 1,828.4 us 100.22 us 32.7148 303.13 KB

可以看出 ReadOnlyDictionary 创建最快,FrozenDictionary 次之,最后是 ImmutableDictionary 最慢。之前我们讨论 ImmutableDictionary 是不可变的,可以在多线程中使用。但是 FrozenDictionary 也是不可变的字段,但是它在读取的数据的性能上比 ImmutableDicionary 好很多。

所以,我们可以得到结论,在多线程环境中, ReadOnlyDictionary 并不是好的类型,ImmutableDictionary 可以在多线程写操作中发挥作用,FrozenDictionary 在多线程读操作中发挥作用。