• .NET性能系列文章一:.NET7的性能改进


    这些方法在.NET7中变得更快#

    照片来自 CHUTTERSNAPUnsplash

    欢迎阅读.NET性能系列的第一章。这一系列的特点是对.NET世界中许多不同的主题进行研究、比较性能。正如标题所说的那样,本章节在于.NET7中的性能改进。你将看到哪种方法是实现特定功能最快的方法,以及大量的技巧和敲门,如何付出较小的代价就能最大化你代码性能。如果你对这些主题感兴趣,那请您继续关注。

    .NET 7目前(17.10.2022)处于预览阶段,将于2022年11月发布。通过这个新版本,微软提供了一些大的性能改进。这篇 .NET性能系列的第一篇文章,是关于从.NET6到.NET7最值得注意的性能改进。

    LINQ

    最相关的改进肯定是在LINQ中,在.NET 7中dotnet社区利用LINQ中对数字数组的处理来使用Vector(SIMD)。这大大改善了一些LINQ方法性能,你可以在Listint[]以及其他数字集合上调用。现在LINQ方法也能直接访问底层数组,而不是使用枚举器访问。让我们来看看这些方法相对于.NET 6是如何表现的。

    我使用BenchmarkDotNet来比较.NET6和.NET7相同代码的性能。

    1. Min 和 Max 方法#

    首先是LINQ方法Min()Max()。它们被用来识别数字枚举中的最低值或最高值。新的实现特别要求有一个先前枚举的集合作为源,因此我们必须在这个基准测试中创建一个数组。

    [Params(1000)]
    public int Length { get; set; }
    
    private int[] arr;
    
    [GlobalSetup]
    public void GlobalSetup() => arr = Enumerable.Range(0, Length).ToArray();
    
    [Benchmark]
    public int Min() => arr.Min();
    
    [Benchmark]
    public int Max() => arr.Max();
    

    在.NET 6和.NET 7上执行这些基准,在我的机器上会得出以下结果。

    方法 运行时 数组长度 平均值 比率 分配
    Min 1000 3,494.08 ns 53.24 32 B
    Min 1000 65.64 ns 1.00 -
    Max 1000 3,025.41 ns 45.92 32 B
    Max 1000 65.93 ns 1.00 -

    这里非常突出的是新的.NET7所展示的性能改进有多大。我们可以看到与.NET 6相比,改进幅度超过4500%。这不仅是因为在内部实现中使用了另一种类型,而且还因为不再发生额外的堆内存分配。

    2. Average 和 Sum#

    另一个很大的改进是Average()Sum()方法。当处理大的double集合时,这些性能优化能展现出更好的结果,这就是为什么我们要用一个double[]来测试它们。

    [Params(1000)]
    public int Length { get; set; }
    
    private double[] arr;
    
    [GlobalSetup]
    public void GlobalSetup()
    {
        var random = new Random();
        arr = Enumerable
            .Range(0, Length)
            .Select(_ => random.NextDouble())
            .ToArray();
    }
    
    [Benchmark]
    public double Average() => arr.Average();
    
    [Benchmark]
    public double Sum() => arr.Sum();
    

    结果显示,性能显著提高了500%以上,而且同样没有了内存分配!

    方法 运行时 数组长度 平均值 比率 分配
    Average 1000 3,438.0 ns 5.50 32 B
    Average 1000 630.3 ns 1.00 -
    Sum 1000 3,303.8 ns 5.25 32 B
    Sum 1000 629.3 ns 1.00 -

    这里的性能提升并不像前面的例子那么突出,但还是非常高的!

    3. Order#

    接下来是这是新增了两个排序方法Order()OrderDescending()。当你不想映射到IComparable 类型时,应该使用新的方法取代.NET7中旧的OrderBy()OrderByDescending()方法。

    [Params(1000)]
    public int Length { get; set; }
    
    private double[] arr;
    
    [GlobalSetup]
    public void GlobalSetup()
    {
        var random = new Random();
        arr = Enumerable
            .Range(0, Length)
            .Select(_ => random.NextDouble())
            .ToArray();
    }
    
    [Benchmark]
    public double[] OrderBy() => arr.OrderBy(d => d).ToArray();
    
    #if NET7_0
    [Benchmark]
    public double[] Order() => arr.Order().ToArray();
    #endif
    
    方法 数组长度 平均值 分配
    OrderBy 1000 51.13 μs 27.61 KB
    Order 1000 50.82 μs 19.77 KB

    在这个基准中,只使用了.NET 7,因为Order()方法在旧的运行时中不可用。

    我们无法看到这两种方法之间的性能影响。然而,我们可以看到的是在堆内存分配方面有很大的改进,这将显著减少垃圾收集,从而节省一些GC时间。

    System.IO

    在.NET 7中,Windows下的IO性能有了些许改善。WriteAllText()方法不再使用那么多分配的内存,ReadAllText()方法与.NET 6相比也快了一些。

    [Benchmark]
    public void WriteAllText() => File.WriteAllText(path1, content);
    
    [Benchmark]
    public string ReadAllText() => File.ReadAllText(path2);
    
    方法 运行时 平均值 比率 分配
    WriteAllText 193.50 μs 1.03 10016 B
    WriteAllText 187.32 μs 1.00 464 B
    ReadAllText 23.29 μs 1.08 24248 B
    ReadAllText 21.53 μs 1.00 24248 B

    序列化 (System.Text.Json)

    来自System.Text.Json命名空间的JsonSerializer得到了一个小小的升级,一些使用了反射的自定义处理程序会在幕后为你缓存,即使你初始化一个JsonSerialzierOptions的新实例。

    private JsonSerializerOptions options = new JsonSerializerOptions();
    private TestClass instance = new TestClass("Test");
    
    [Benchmark(Baseline = true)]
    public string Default() => JsonSerializer.Serialize(instance);
    
    [Benchmark]
    public string CachedOptions() => JsonSerializer.Serialize(instance, options);
    
    [Benchmark]
    public string NoCachedOptions() => JsonSerializer.Serialize(instance, new JsonSerializerOptions());
    
    public record TestClass(string Test);
    

    在上面代码中,对NoCachedOptions()的调用通常会导致JsonSerialzierOptions的额外实例化和一些自动生成的处理程序。在.NET 7中这些实例是被缓存的,当你在代码中使用这种方法时,你的性能会好一些。否则,无论如何都要缓存你的JsonSerialzierOptions,就像在CachedOptions例子中,你不会看到很大的提升。

    方法 运行时 平均值 比率 分配 分配比率
    Default 135.4 ns 1.04 208 B 3.71
    CachedOptions 145.9 ns 1.12 208 B 3.71
    NoCachedOptions 90,069.7 ns 691.89 7718 B 137.82
    Default 130.2 ns 1.00 56 B 1.00
    CachedOptions 129.8 ns 0.99 56 B 1.00
    NoCachedOptions 533.8 ns 4.10 345 B 6.16

    基本类型

    1. Guid 相等比较#

    有一项改进,肯定会导致现代应用程序的性能大增,那就是对Guid相等比较的新实现。

    private Guid guid0 = Guid.Parse("18a2c952-2920-4750-844b-2007cb6fd42d");
    private Guid guid1 = Guid.Parse("18a2c952-2920-4750-844b-2007cb6fd42d");
    
    [Benchmark]
    public bool GuidEquals() => guid0 == guid1;
    
    方法 运行时 平均值 比率
    GuidEquals 1.808 ns 1.49
    GuidEquals 1.213 ns 1.00

    可以感觉到,新的实现也使用了SIMD,比旧的实现快30%左右。

    由于有大量的API使用Guid作为实体的标识符,这肯定会积极的产生影响。

    2. BigInt 解析#

    一个很大的改进发生在将巨大的数字从字符串解析为BigInteger类型。就我个人而言,在一些区块链项目中,我曾使用过BigInteger类型,在那里有必要使用这种类型来表示ETH代币的精度。所以在性能方面,这对我来说会很方便。

    private string bigIntString = string.Concat(Enumerable.Repeat("123456789", 100000));
    
    [Benchmark]
    public BigInteger ParseBigInt() => BigInteger.Parse(bigIntString);
    
    方法 运行时 平均值 比率 分配
    ParseBigInt 2.058 s 1.62 2.09 MB
    ParseBigInt 1.268 s 1.00 2.47 MB

    我们可以看到性能有了明显的提高,不过我们也看到它比.NET6上多分配一些内存。

    3. Boolean 解析#

    对于解析boolean类型,我们也有显著的性能改进:

    [Benchmark]
    public bool ParseBool() => bool.TryParse("True", out _);
    
    方法 运行时 平均值 比率
    ParseBool 8.164 ns 5.21
    ParseBool 1.590 ns 1.00

    诊断

    System.Diagnostics命名空间也进行了升级。进程处理有两个重大改进,Stopwatch有一个新功能。

    1. GetProcessByName#

    [Benchmark]
    public Process[] GetProcessByName() 
          => Process.GetProcessesByName("dotnet.exe");
    
    方法 运行时 平均值 比率 分配 分配比率
    GetProcessByName 2.065 ms 1.04 529.89 KB 247.31
    GetProcessByName 1.989 ms 1.00 2.14 KB 1.00

    新的GetProcessByName()的速度并不明显,但使用的分配内存比前者少得多。

    2. GetCurrentProcessName#

    [Benchmark]
    public string GetCurrentProcessName() 
          => Process.GetCurrentProcess().ProcessName;
    
    方法 运行时 平均值 比率 分配 分配比率
    GetCurrentProcessName 1,955.67 μs 103.02 3185 B 6.98
    GetCurrentProcessName 18.98 μs 1.00 456 B 1.00

    在这里,我们可以看到一个更有效的内存方法,对.NET 7的实现有极高的性能提升。

    3. Stopwatch#

    Stopwatch被广泛用于测量运行时的性能。到目前为止,存在的问题是,使用Stopwatch需要分配堆内存。为了解决这个问题,dotnet社区实现了一个静态函数GetTimestamp(),它仍然需要一个复杂的逻辑来有效地获得时间差。现在又实现了另一个静态方法,名为GetElapsedTime(),在这里你可以传递之前的时间戳,并在不分配堆内存的情况下获得经过的时间。

    [Benchmark(Baseline = true)]
    public TimeSpan OldStopwatch()
    {
        Stopwatch sw = Stopwatch.StartNew();
        return sw.Elapsed;
    }
    
    [Benchmark]
    public TimeSpan NewStopwatch()
    {
        long timestamp = Stopwatch.GetTimestamp();
        return Stopwatch.GetElapsedTime(timestamp);
    }
    
    Method Mean Ratio Allocated Alloc Ratio
    OldStopwatch 39.44 ns 1.00 40 B 1.00
    NewStopwatch 37.13 ns 0.94 - 0.00

    这种方法的速度优化并不明显,然而节省堆内存分配可以说是值得的。

    结尾#

    我希望,我可以在性能和基准测试的世界里给你一个有趣的切入点。如果你关于特定性能主题想法,请在评论中告诉我。

    如果你喜欢这个系列的文章,请务必关注我,因为还有很多有趣的话题等着你。

    谢谢你的阅读!

    版权#

    原文版权:Tobias Streng
    翻译版权:InCerry
    原文链接:
    https://medium.com/@tobias.streng/net-performance-series-1-performance-improvements-in-net-7-fb793f8f5f71

  • 相关阅读:
    整合生成型AI战略:从宏观思维到小步实践
    一篇文章搞定什么是nodeJs它和NPM关系与应用
    visual studio设置主题和背景颜色
    苹果开发初学者指南:Xcode 如何为运行的 App 添加环境变量(Environmental Variable)
    Tomcat运行流程、Servlet运行原理以及常用API
    flutterdart chacha20加密
    LeetCode LCP 57. 打地鼠 -- 动态规划+二分查询过滤无效数据
    Spark大数据分析与实战笔记(第三章 Spark RDD 弹性分布式数据集-05)
    【c++】运算符重载实例
    黑猫带你学UFS协议第2篇:UFS相关名词释义
  • 原文地址:https://www.cnblogs.com/InCerry/p/net-performance-series-1-performance-improvements-in-net-7-fb793f8f5f71.html