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

照片来自 CHUTTERSNAPUnsplash

欢迎阅读.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的性能改进的更多相关文章

  1. .NET性能系列文章二:Newtonsoft.Json vs. System.Text.Json

    微软终于追上了? 图片来自 Glenn Carstens-Peters Unsplash 欢迎来到.NET性能系列的另一章.这个系列的特点是对.NET世界中许多不同的主题进行研究.基准和比较.正如标题 ...

  2. Redis变慢?深入浅出Redis性能诊断系列文章(一)

    (本文首发于"数据库架构师"公号,订阅"数据库架构师"公号,一起学习数据库技术)   Redis 作为一款业内使用率最高的内存数据库,其拥有非常高的性能,单节点 ...

  3. Java GC 专家系列5:Java应用性能优化的原则

    本文是GC专家系列中的第五篇.在第一篇理解Java垃圾回收中我们学习了几种不同的GC算法的处理过程,GC的工作方式,新生代与老年代的区别.所以,你应该已经了解了JDK 7中的5种GC类型,以及每种GC ...

  4. 一篇关于PHP性能的文章

    一篇关于PHP性能的文章 昨晚清理浏览器收藏夹网址时,发现了http://www.phpbench.com/,想起来应该是2015年发现的一个比较性能的文章,我就点进去看了看,发现还是全英文耶,刚好最 ...

  5. 【微信小程序开发•系列文章六】生命周期和路由

    这篇文章理论的知识比较多一些,都是个人观点,描述有失妥当的地方希望读者指出. [微信小程序开发•系列文章一]入门 [微信小程序开发•系列文章二]视图层 [微信小程序开发•系列文章三]数据层 [微信小程 ...

  6. IT软件人员的技术学习内容(写给技术迷茫中的你) - 项目管理系列文章

    前面笔者曾经写过一篇关于IT从业者的职业道路文章(见笔者文:IT从业者的职业道路(从程序员到部门经理) - 项目管理系列文章).然后有读者提建议说写写技术方面的路线,所以就有了本文.本文从初学者到思想 ...

  7. .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引

    系列文章索引: .NET面试题解析(01)-值类型与引用类型 .NET面试题解析(02)-拆箱与装箱 .NET面试题解析(03)-string与字符操作 .NET面试题解析(04)-类型.方法与继承 ...

  8. [转]领域驱动设计系列文章(2)——浅析VO、DTO、DO、PO的概念、区别和用处

    原文地址:http://www.blogjava.net/johnnylzb/archive/2010/05/27/321968.html 上一篇文章作为一个引子,说明了领域驱动设计的优势,从本篇文章 ...

  9. 领域驱动设计系列文章——浅析VO、DTO、DO、PO的概念、区别和用处

    本篇文章主要讨论一下我们经常会用到的一些对象:VO.DTO.DO和PO. 由于不同的项目和开发人员有不同的命名习惯,这里我首先对上述的概念进行一个简单描述,名字只是个标识,我们重点关注其概念: 概念: ...

随机推荐

  1. BZOJ4569 [Scoi2016]萌萌哒(并查集,倍增)

    类似\(ST表\)的思想,倍增\(log(n)\)地合并 你是我家的吗?不是就来呀啦啦啦.还有要来的吗?没了!那有多少个家就映射多少答案呀 倍增原来这么好玩 #include <iostream ...

  2. linux centos7开启防火墙端口

    firewall-cmd --zone=public --add-port=3306/tcp --permanent firewall-cmd --reload

  3. 【Java】学习路径50-线程死锁问题

    生活化的例子:比如说有两个人,一把刀和一把叉子: 第一个人先需要一把刀,然后还需要一把叉子: 第二个人先需要一把叉子,然后还需要一把刀. 我们理想的情况是:一个人拿着刀,然后再拿到叉子,把事情做完,然 ...

  4. Spring MVC组件之HandlerAdapter

    Spring MVC组件之HandlerAdapter HandlerAdapter概述 HandlerAdapter组件是一个处理器Handler的适配器.HandlerAdapter组件的主要作用 ...

  5. Linux安装Jenkins及配置svn使用

    目录 1. 下载 2. 创建文件夹 3. 安装 4. 修改端口,不用这步 5. 安装插件提速 6. 启动 7. 页面访问 8. 新建用户 9. 安装Subversion插件 10. 安装maven插件 ...

  6. AtCoder Beginner Contest 255(E-F)

    Aising Programming Contest 2022(AtCoder Beginner Contest 255) - AtCoder E - Lucky Numbers 题意: 给两个数组a ...

  7. 第七十六篇:ref引用(在vue中引用Dom的方法)

    好家伙, 引子: jQuery简化了程序员操作DOM的过程 vue 优势:MVVM 在vue中,程序员不需要操作DOM.程序员只需要把数据维护好即可!(数据驱动视图) 那么若要在vue中操作dom,这 ...

  8. 手写tomcat——编写一个echo http服务器

    核心代码如下: public class DiyTomcat1 { public void run() throws IOException { ServerSocket serverSocket = ...

  9. FileInputStream字节输入流

    FileInputStream字节输入流 编码思想:首相顶一个FileInputStream字节输入流对象,fis设置为nul,在try/catch里面放入FileInputStream字节输入流对象 ...

  10. http服务(postman调用方法及反参)

    #region 监听url #region 监听url路径请求 static HttpListener httpobj; private void listeningUrl() { //提供一个简单的 ...