原文:bit.ly/3wSpO4o
作者:Nikita Starichenko
翻译:精致码农
大家好!今天我想和大家分享几个 .NET 的性能小贴士与基准测试。
我的系统环境:
我将以百分比的形式提供基准测试结果,其中 100% 是最快的结果。
我们知道,字符串 string
是不可变的。因此,每当你拼接字符串时,就会分配一个新的字符串对象,并填充内容,最终被回收。所有这些都有昂贵开销,这就是为什么 StringBuilder
在字符串拼接时总有更好的性能。
基准测试例子:
private static StringBuilder sb = new();[Benchmark]public void Concat3() => ExecuteConcat(3);[Benchmark]public void Concat5() => ExecuteConcat(5);[Benchmark]public void Concat10() => ExecuteConcat(10);[Benchmark]public void Concat100() => ExecuteConcat(100);[Benchmark]public void Concat1000() => ExecuteConcat(1000);[Benchmark]public void Builder3() => ExecuteBuilder(3);[Benchmark]public void Builder5() => ExecuteBuilder(5);[Benchmark]public void Builder10() => ExecuteBuilder(10);[Benchmark]public void Builder100() => ExecuteBuilder(100);[Benchmark]public void Builder1000() => ExecuteBuilder(1000);public void ExecuteConcat(int size){ string s = ""; for (int i = 0; i < size; i++) { s += "a"; }}public void ExecuteBuilder(int size){ sb.Clear(); for (int i = 0; i < size; i++) { sb.Append("a"); }}
结果:
1. 3 string concatenations - 218% (35.21 ns)2. 3 StringBuilder concatenations - 100% (16.09 ns)1. 5 string concatenations - 277% (66.99 ns)2. 5 StringBuilder concatenations - 100% (24.16 ns)1. 10 string concatenations - 379% (160.69 ns)2. 10 StringBuilder concatenations - 100% (42.37 ns)1. 100 string concatenations - 711% (2,796.63 ns)2. 100 StringBuilder concatenations - 100% (393.12 ns)1. 1000 string concatenations - 3800% (144,100.46 ns)2. 1000 StringBuilder concatenations - 100% (3,812.22 ns)
.NET 提供了很多集合类型,比如 List<T>
, Dictionary<T>
, 和 HashSet<T>
。所有这些集合都有动态的容量,当你添加更多的项目时,它们的大小会自动扩大。
当集合达到其大小限制时,它将分配一个新的更大的内存缓冲区,这意味着要进行额外的开销去分配容量。
基准测试例子:
[Benchmark]public void ListDynamicCapacity(){ List<int> list = new List<int>(); for (int i = 0; i < Size; i++) { list.Add(i); }}[Benchmark]public void ListPlannedCapacity(){ List<int> list = new List<int>(Size); for (int i = 0; i < Size; i++) { list.Add(i); }}
在第一个方法中,List
集合使用默认容量初始化,并动态扩大。在第二个方法中,初始容量被设置为它所需要的固定大小。
对于 1000 个项目,其结果是:
1. List Dynamic Capacity - 140% (2.490 us)2. List Planned Capacity - 100% (1.774 us)
Dictionary
和 HashSet
的测试结果是:
1. Dictionary Dynamic Capacity - 233% (20.314 us)2. Dictionary Planned Capacity - 100% (8.702 us)1. HashSet Dynamic Capacity - 223% (17.004 us)2. HashSet Planned Capacity - 100% (7.624 us)
数组的分配和回收的开销可能是相当昂贵的,高频地执行这些分配会增加 GC 的压力并损害性能。一个优雅的解决方案使用是 System.Buffers.ArrayPool
类,它可以在 NuGet 的 Systems.Buffers
中找到。
这个思想和 ThreadPool
很相似。为数组分配一个共享缓冲区,你可以重复使用,而不需要实际分配和回收它们占用的内存。基本用法是调用 ArrayPool<T>.Shared.Rent(size)
,这将返回一个常规数组,你可以以任何方式使用它。完成后,调用 ArrayPool<int>.Shared.Return(array)
将缓冲区返回到共享池中。
基准测试例子:
[Benchmark]public void RegularArray(){ int[] array = new int[ArraySize];}[Benchmark]public void SharedArrayPool(){ var pool = ArrayPool<int>.Shared; int[] array = pool.Rent(ArraySize); pool.Return(array);}
ArraySize = 1000
的结果:
1. Regular Array - 2270% (440.41 ns)2. Shared ArrayPool - 100% (19.40 ns)
当涉及到对象回收时,Struct 有如下几个好处:
ObjectHeader
和 MethodTable
。基准测试例子:
class VectorClass{ public int X { get; set; } public int Y { get; set; }}struct VectorStruct{ public int X { get; set; } public int Y { get; set; }}private const int ITEMS = 10000;[Benchmark]public void WithClass(){ VectorClass[] vectors = new VectorClass[ITEMS]; for (int i = 0; i < ITEMS; i++) { vectors[i] = new VectorClass(); vectors[i].X = 5; vectors[i].Y = 10; }}[Benchmark]public void WithStruct(){ VectorStruct[] vectors = new VectorStruct[ITEMS]; // At this point all the vectors instances are already allocated with default values for (int i = 0; i < ITEMS; i++) { vectors[i].X = 5; vectors[i].Y = 10; }}
结果:
1. With Class - 742% (88.83 us)2. With Struct - 100% (11.97 us)
在没有基准测试的情况下,不要使用 ConcurrentBag<T>
。这个集合是为非常特殊的使用场景而设计的(当经常有项目被排队的线程删除时)。如果需要一个并发的集合队列,请选择 ConcurrentQueue<T>
。
基准测试例子:
private static int Size = 1000;[Benchmark]public void Bag(){ ConcurrentBag<int> bag = new(); for (int i = 0; i < Size; i++) { bag.Add(i); }}[Benchmark]public void Queue(){ ConcurrentQueue<int> bag = new(); for (int i = 0; i < Size; i++) { bag.Enqueue(i); }}
结果:
1. ConcurrentBag - 165% (24.21 us)2. ConcurrentQueue - 100% (14.64 us)
感谢大家阅读!