.NET性能系列文章一:.NET7的性能改进
这些方法在.NET7中变得更快

照片来自 CHUTTERSNAP 的 Unsplash
欢迎阅读.NET性能系列的第一章。这一系列的特点是对.NET世界中许多不同的主题进行研究、比较性能。正如标题所说的那样,本章节在于.NET7中的性能改进。你将看到哪种方法是实现特定功能最快的方法,以及大量的技巧和敲门,如何付出较小的代价就能最大化你代码性能。如果你对这些主题感兴趣,那请您继续关注。
.NET 7目前(17.10.2022)处于预览阶段,将于2022年11月发布。通过这个新版本,微软提供了一些大的性能改进。这篇 .NET性能系列的第一篇文章,是关于从.NET6到.NET7最值得注意的性能改进。
LINQ
最相关的改进肯定是在LINQ中,在.NET 7中dotnet社区利用LINQ中对数字数组的处理来使用Vector<T>(SIMD)。这大大改善了一些LINQ方法性能,你可以在List<int>或int[]以及其他数字集合上调用。现在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
.NET性能系列文章一:.NET7的性能改进的更多相关文章
- .NET性能系列文章二:Newtonsoft.Json vs. System.Text.Json
微软终于追上了? 图片来自 Glenn Carstens-Peters Unsplash 欢迎来到.NET性能系列的另一章.这个系列的特点是对.NET世界中许多不同的主题进行研究.基准和比较.正如标题 ...
- Redis变慢?深入浅出Redis性能诊断系列文章(一)
(本文首发于"数据库架构师"公号,订阅"数据库架构师"公号,一起学习数据库技术) Redis 作为一款业内使用率最高的内存数据库,其拥有非常高的性能,单节点 ...
- Java GC 专家系列5:Java应用性能优化的原则
本文是GC专家系列中的第五篇.在第一篇理解Java垃圾回收中我们学习了几种不同的GC算法的处理过程,GC的工作方式,新生代与老年代的区别.所以,你应该已经了解了JDK 7中的5种GC类型,以及每种GC ...
- 一篇关于PHP性能的文章
一篇关于PHP性能的文章 昨晚清理浏览器收藏夹网址时,发现了http://www.phpbench.com/,想起来应该是2015年发现的一个比较性能的文章,我就点进去看了看,发现还是全英文耶,刚好最 ...
- 【微信小程序开发•系列文章六】生命周期和路由
这篇文章理论的知识比较多一些,都是个人观点,描述有失妥当的地方希望读者指出. [微信小程序开发•系列文章一]入门 [微信小程序开发•系列文章二]视图层 [微信小程序开发•系列文章三]数据层 [微信小程 ...
- IT软件人员的技术学习内容(写给技术迷茫中的你) - 项目管理系列文章
前面笔者曾经写过一篇关于IT从业者的职业道路文章(见笔者文:IT从业者的职业道路(从程序员到部门经理) - 项目管理系列文章).然后有读者提建议说写写技术方面的路线,所以就有了本文.本文从初学者到思想 ...
- .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引
系列文章索引: .NET面试题解析(01)-值类型与引用类型 .NET面试题解析(02)-拆箱与装箱 .NET面试题解析(03)-string与字符操作 .NET面试题解析(04)-类型.方法与继承 ...
- [转]领域驱动设计系列文章(2)——浅析VO、DTO、DO、PO的概念、区别和用处
原文地址:http://www.blogjava.net/johnnylzb/archive/2010/05/27/321968.html 上一篇文章作为一个引子,说明了领域驱动设计的优势,从本篇文章 ...
- 领域驱动设计系列文章——浅析VO、DTO、DO、PO的概念、区别和用处
本篇文章主要讨论一下我们经常会用到的一些对象:VO.DTO.DO和PO. 由于不同的项目和开发人员有不同的命名习惯,这里我首先对上述的概念进行一个简单描述,名字只是个标识,我们重点关注其概念: 概念: ...
随机推荐
- 你有对象类,我有结构体,Go lang1.18入门精炼教程,由白丁入鸿儒,go lang结构体(struct)的使用EP06
再续前文,在面向对象层面,Python做到了超神:万物皆为对象,而Ruby,则干脆就是神:飞花摘叶皆可对象.二者都提供对象类操作以及继承的方式为面向对象张目,但Go lang显然有一些特立独行,因为它 ...
- 巨细靡遗流程控制,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang流程结构详解EP09
流程结构就是指程序逻辑到底怎么执行,进而言之,程序执行逻辑的顺序.众所周知,程序整体都是自上由下执行的,但有的时候,又不仅仅是从上往下执行那么简单,大体上,Go lang程序的流程控制结构一共有三种: ...
- 刷题记录:Codeforces Round #731 (Div. 3)
Codeforces Round #731 (Div. 3) 20210803.网址:https://codeforces.com/contest/1547. 感觉这次犯的低级错误有亿点多-- A 一 ...
- HMS Core Discovery第17期回顾|音随我动,秒变音色造型师
HMS Core Discovery第17期直播<音随我动,秒变音色造型师>,已于8月25日圆满结束,本期直播我们邀请了HMS Core音频编辑服务的产品经理.技术专家以及创新娱乐类应用& ...
- Android 自动取色并设置沉浸式状态栏
Android 自动取色并设置沉浸式状态栏 - Stars-One的杂货小窝 最近在进行产品的优化,也是研究了下沉浸式状态栏的实现方法及自动取色,记录一下笔记 设置沉浸式状态栏 1.添加依赖 这里,是 ...
- 【c语言学习】1 基础环境安装调试
1-1下载 vs2019 vs2019下载链接https://visualstudio.microsoft.com/zh-hans/vs/community/ 1-2安装配置环境 记得勾选上c++开发 ...
- [双重 for 循环]打印一个倒三角形
[双重 for 循环]打印一个倒三角形 核心算法 里层循环:j = i; j <= 10; j++ 当i=1时,j=1 , j<=10,j++,打印10个星星 当i=2时,j=2 , j& ...
- CDH6.2.0 搭建大数据集群
1. 资料准备 现在官网https://www.cloudera.com 需要注册账号,未来可能会收费等问题,十分麻烦,这里有一份我自己百度云的备份 链接: https://pan.baidu.com ...
- 使用Kali的wifite和aircrack-ng联合破解wifi密码
准备材料 有kali的虚拟机,这里推荐VM 一个超级便宜的USB无线网卡,很便宜三十几块钱 一个靠谱的WPA密码字典(关于字典文件,我这里整理了好多,可联系我.QQ:1213456261) 1.运行k ...
- Containerd和Docker的关系
联系 容器运行时(Container Runtime)是Kubernetes(k8s)最重要的组件之一,负责管理镜像和容器的生命周期.Kubelet通过Container Runtime Interf ...