3

五个 .NET 性能小贴士

 3 years ago
source link: https://www.cnblogs.com/willick/p/15068802.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

原文:bit.ly/3wSpO4o
作者:Nikita Starichenko
翻译:精致码农

大家好!今天我想和大家分享几个 .NET 的性能小贴士与基准测试。

我的系统环境:

  • BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19042.985
  • Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
  • .NET SDK=5.0.104

我将以百分比的形式提供基准测试结果,其中 100% 是最快的结果。

用 StringBuilder 拼接字符串

我们知道,字符串 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)

DictionaryHashSet 的测试结果是:

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)

ArrayPool 用于短时大数组

数组的分配和回收的开销可能是相当昂贵的,高频地执行这些分配会增加 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 有如下几个好处:

  • 当结构类型不是类的一部分时,它们被分配在堆栈中,根本不需要垃圾回收。
  • 当结构是类(或任何引用类型)的一部分时,它们被存储在堆中。在这种情况下,它们是内联存储的,并且会随包含类型回收而回收。内联意味着该结构的数据是按原样存储的,这与引用类型相反,在引用类型中,指针被存储到堆上另一个位置。所以回收的成本要低很多。
  • 结构比引用类型占用的内存更少,因为它们没有 ObjectHeaderMethodTable

基准测试例子:

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)

ConcurrentQueue<T> 代替 ConcurrentBag<T>

在没有基准测试的情况下,不要使用 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)

感谢大家阅读!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK