zoukankan      html  css  js  c++  java
  • 五个 .NET 性能小贴士

    原文: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)
    

    感谢大家阅读!

    作者:精致码农-王亮

    出处:http://cnblogs.com/willick

    联系:liam.wang@live.com

    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如有问题或建议,请多多赐教,非常感谢。
  • 相关阅读:
    条形码校验码生成
    js 模仿块级作用域(私有作用域)、私有变量
    js 闭包
    js 继承
    javascript 创建对象
    jQuery.noConflict() 函数
    C#对话框-打开和保存对话框(转)
    String.format()的用法
    转:WPF中ListBox的创建和多种绑定用法
    在wpf或winform关闭子窗口或对子窗口进行某个操作后刷新父窗口
  • 原文地址:https://www.cnblogs.com/willick/p/15068802.html
Copyright © 2011-2022 走看看