.NET 7的性能改进有哪些具体细节?

摘要:原文 | Stephen Toub 翻译 | 郑子铭 同样,为了不做不必要的工作,有一个相当常见的模式出现在string.Substring和span.Slice等方法中。 span = span.Slice(offset, str.Len
原文 | Stephen Toub 翻译 | 郑子铭 同样,为了不做不必要的工作,有一个相当常见的模式出现在string.Substring和span.Slice等方法中。 span = span.Slice(offset, str.Length - offset); 这里需要注意的是,这些方法都有重载,只取起始偏移量。由于指定的长度是指定偏移量后的剩余部分,所以调用可以简化为。 span = span.Slice(offset); 这不仅可读性和可维护性更强,而且还有一些小的效率优势,例如,在64位上,Slice(int, int)构造函数比Slice(int)有一个额外的加法,而对于32位,Slice(int, int)构造函数会产生一个额外的比较和分支。因此,简化这些调用对代码维护和性能都是有益的,dotnet/runtime#68937对所有发现的该模式的出现都进行了简化。dotnet/runtime#73882使其更具影响力,它简化了string.Substring,以消除不必要的开销,例如,它将四个参数验证检查浓缩为一个快速路径比较(在64位进程中)。 好了,关于弦的问题就到此为止。那跨度呢?C# 11中最酷的功能之一是对Ref字段的新支持。什么是引用字段?你对C#中的引用很熟悉,我们已经讨论过它们本质上是可管理的指针,也就是说,由于它引用的对象在堆上被移动,运行时可以随时更新的指针。这些引用可以指向对象的开头,也可以指向对象内部的某个地方,在这种情况下,它们被称为 "内部指针"。Ref从1.0开始就存在于C#中,但那时它主要是通过引用传递给方法调用,例如 class Data { public int Value; } ... void Add(ref int i) { i++; } ... var d = new Data { Value = 42 }; Add(ref d.Value); Debug.Assert(d.Value == 43); 后来的C#版本增加了拥有本地参考文献的能力,例如。 void Add(ref int i) { ref j = ref i; j++; } 甚至是要有 ref 的返回,例如 ref int Add(ref int i) { ref j = ref i; j++; return ref j; } 这些设施更为先进,但它们在整个更高性能的代码库中被广泛使用,近年来.NET中的许多优化在很大程度上是由于这些与ref相关的能力而实现的。 Span和ReadOnlySpan本身在很大程度上是基于引用的。例如,许多旧的集合类型上的索引器被实现为一个get/set属性,例如。 private T[] _items; ... public T this[int i] { get => _items[i]; set => _items[i] = value; } 但不是 span。Span的索引器看起来更像这样。 public ref T this[int index] { get { if ((uint)index >= (uint)_length) ThrowHelper.ThrowIndexOutOfRangeException(); return ref Unsafe.Add(ref _reference, index); } } 注意这里只有一个getter,没有setter;这是因为它返回一个指向实际存储位置的ref T。它是一个可写的引用,所以你可以对它进行赋值,例如,你可以写。 span[i] = value; 但这并不等同于调用一些设置器。 span.set_Item(i, value); 它实际上等同于使用getter来检索引用,然后通过该引用写入一个值,比如说 ref T item = ref span.get_Item(i); item = value; 这一切都很好,但是getter定义中的_reference是什么?好吧,Span实际上只是一个由两个字段组成的元组:一个引用(被引用的内存的开始)和一个长度(该引用中的多少个元素被包含在span中)。在过去,运行时必须用一个内部类型(ByReference)来解决这个问题,该类型被运行时特别认可为一个引用。但是从C# 11和.NET 7开始,Ref结构现在可以包含Ref字段,这意味着Span现在可以被定义为如下内容。
阅读全文