- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
原文 | Stephen Toub 。
翻译 | 郑子铭 。
我们已经看过了代码生成和GC,线程和矢量化,互操作......让我们把注意力转向系统中的一些基本类型。像int、bool和double这样的基本类型,像Guid和DateTime这样的核心类型,它们构成了构建一切的支柱,每一个版本都能看到这些类型的改进,这让人兴奋.
来自 @CarlVerret 的 dotnet/runtime#62301 极大地提高了double.Parse和float.Parse将UTF16文本解析为浮点值的能力。这一点特别好,因为它是基于 @lemire 和 @CarlVerret 最近的一些研究 ,他们用C#和.NET 5实现了一个非常快速的浮点数解析实现,而这个实现现在已经进入了.NET 7.
private string[] _valuesToParse;
[GlobalSetup]
public void Setup()
{
using HttpClient hc = new HttpClient();
string text = hc.GetStringAsync("https://raw.githubusercontent.com/CarlVerret/csFastFloat/1d800237275f759b743b86fcce6680d072c1e834/Benchmark/data/canada.txt").Result;
var lines = new List<string>();
foreach (ReadOnlySpan<char> line in text.AsSpan().EnumerateLines())
{
ReadOnlySpan<char> trimmed = line.Trim();
if (!trimmed.IsEmpty)
{
lines.Add(trimmed.ToString());
}
}
_valuesToParse = lines.ToArray();
}
[Benchmark]
public double ParseAll()
{
double total = 0;
foreach (string s in _valuesToParse)
{
total += double.Parse(s);
}
return total;
}
方法 | 运行时 | 平均值 | 比率 |
---|---|---|---|
ParseAll | .NET 6.0 | 26.84 ms | 1.00 |
ParseAll | .NET 7.0 | 12.63 ms | 0.47 |
bool.TryParse和bool.TryFormat也得到了改进。 dotnet/runtime#64782 通过使用BinaryPrimitives执行更少的写和读,简化了这些实现。例如,TryFormat通过执行以下操作而不是写出 "True".
destination[0] = 'T';
destination[1] = 'r';
destination[2] = 'u';
destination[3] = 'e';
这需要四次写操作,相反,它可以通过一次写来实现相同的操作.
BinaryPrimitives.WriteUInt64LittleEndian(MemoryMarshal.AsBytes(destination), 0x65007500720054); // "True"
那0x65007500720054是内存中四个字符的数值,是一个单一的ulong。你可以通过一个微观的基准测试看到这些变化的影响.
private bool _value = true;
private char[] _chars = new char[] { 'T', 'r', 'u', 'e' };
[Benchmark] public bool ParseTrue() => bool.TryParse(_chars, out _);
[Benchmark] public bool FormatTrue() => _value.TryFormat(_chars, out _);
方法 | 运行时 | 平均值 | 比率 |
---|---|---|---|
ParseTrue | .NET 6.0 | 7.347 ns | 1.00 |
ParseTrue | .NET 7.0 | 2.327 ns | 0.32 |
FormatTrue | .NET 6.0 | 3.030 ns | 1.00 |
FormatTrue | .NET 7.0 | 1.997 ns | 0.66 |
Enum也得到了一些性能上的提升。例如,当执行像Enum.IsDefined、Enum.GetName或Enum.ToString这样的操作时,该实现会查询所有定义在枚举上的值的缓存。这个缓存包括Enum中每个定义的枚举的字符串名称和值。它也是按数组中的值排序的,所以当这些操作之一被执行时,代码使用Array.BinarySearch来找到相关条目的索引。这方面的问题是开销的问题。当涉及到算法复杂性时,二进制搜索比线性搜索更快;毕竟,二进制搜索是O(log N),而线性搜索是O(N)。然而,在线性搜索中,每一步算法的开销也较少,因此对于较小的N值,简单地做简单的事情会快很多。这就是 dotnet/runtime#57973 对枚举的作用。对于小于或等于32个定义值的枚举,现在的实现只是通过内部的SpanHelpers.IndexOf(在跨度、字符串和数组上的IndexOf背后的工作程序)进行线性搜索,而对于超过这个值的枚举,它进行SpanHelpers.BinarySearch(这是对Array.BinarySearch的实现).
private DayOfWeek[] _days = Enum.GetValues<DayOfWeek>();
[Benchmark]
public bool AllDefined()
{
foreach (DayOfWeek day in _days)
{
if (!Enum.IsDefined(day))
{
return false;
}
}
return true;
}
方法 | 运行时 | 平均值 | 比率 |
---|---|---|---|
AllDefined | .NET 6.0 | 159.28 ns | 1.00 |
AllDefined | .NET 7.0 | 94.86 ns | 0.60 |
Enums在与Nullable 和EqualityComparer .Default的配合下也得到了提升。EqualityComparer .Default缓存了一个从所有对Default的访问中返回的EqualityComparer 实例的单子实例。该单例根据相关的T进行初始化,实现者可以从众多不同的内部实现中进行选择,例如专门用于字节的ByteArrayComparer,用于实现IComparable 的T的GenericEqualityComparer ,等等。对于任意类型来说,万能的是一个ObjectEqualityComparer 。 dotnet/runtime#68077 修复了这一问题,它确保了nullable enums被映射到(现有的)Nullable 的专门比较器上,并简单地调整了其定义以确保它能与enums很好地配合。结果表明,以前有多少不必要的开销.
private DayOfWeek?[] _enums = Enum.GetValues<DayOfWeek>().Select(e => (DayOfWeek?)e).ToArray();
[Benchmark]
[Arguments(DayOfWeek.Saturday)]
public int FindEnum(DayOfWeek value) => IndexOf(_enums, value);
private static int IndexOf<T>(T[] values, T value)
{
for (int i = 0; i < values.Length; i++)
{
if (EqualityComparer<T>.Default.Equals(values[i], value))
{
return i;
}
}
return -1;
}
方法 | 运行时 | 平均值 | 比率 |
---|---|---|---|
FindEnum | .NET 6.0 | 421.608 ns | 1.00 |
FindEnum | .NET 7.0 | 5.466 ns | 0.01 |
不容忽视的是,Guid的平等操作也变快了,这要感谢 @madelson 的 dotnet/runtime#66889 。以前的Guid实现将数据分成4个32位的值,并进行4个int的比较。有了这个改变,如果当前的硬件支持128位SIMD,实现就会将两个Guid的数据加载为两个向量,并简单地进行一次比较.
private Guid _guid1 = Guid.Parse("0aa2511d-251a-4764-b374-4b5e259b6d9a");
private Guid _guid2 = Guid.Parse("0aa2511d-251a-4764-b374-4b5e259b6d9a");
[Benchmark]
public bool GuidEquals() => _guid1 == _guid2;
方法 | 运行时 | 平均值 | 比率 | 代码大小 |
---|---|---|---|---|
GuidEquals | .NET 6.0 | 2.119 ns | 1.00 | 90 B |
GuidEquals | .NET 7.0 | 1.354 ns | 0.64 | 78 B |
dotnet/runtime#59857 还改进了DateTime.Equals的一些开销。DateTime是用一个单一的ulong _dateData字段实现的,其中大部分位存储了从1/1/0001 12:00am开始的ticks偏移量,每个tick是100纳秒,并且前两个位描述了DateTimeKind。因此,公共的Ticks属性返回_dateData的值,但前两位被屏蔽掉了,例如:_dateData & 0x3FFFFFFFFFFFFFFF。然后,平等运算符只是将一个DateTime的Ticks与其他DateTime的Ticks进行比较,这样我们就可以有效地得到(dt1._dateData & 0x3FFFFFFFFFFF)==(dt2._dateData & 0x3FFFFFFFFFFF)。然而,作为一个微观的优化,可以更有效地表达为((dt1._dateData ^ dt2._dateData) << 2) == 0。在这种微小的操作中很难衡量差异,但你可以简单地从所涉及的指令数量中看出,在.NET 6上,这产生了.
; Program.DateTimeEquals()
mov rax,[rcx+8]
mov rdx,[rcx+10]
mov rcx,0FFFFFFFFFFFF
and rax,rcx
and rdx,rcx
cmp rax,rdx
sete al
movzx eax,al
ret
; Total bytes of code 34
而在.NET 7上则产生.
; Program.DateTimeEquals()
mov rax,[rcx+8]
mov rdx,[rcx+10]
xor rax,rdx
shl rax,2
sete al
movzx eax,al
ret
; Total bytes of code 22
所以我们得到的不是mov、and、and、cmp,而是xor和shl.
由于 @SergeiPavlov 的 dotnet/runtime#72712 和 @SergeiPavlov 的 dotnet/runtime#73277 ,对DateTime的其他操作也变得更有效率。在另一个.NET受益于最新研究进展的案例中,这些PR实现了Neri和Schneider的 "Euclidean Affine Functions and Applications to Calendar Algorithms " 中的算法,以改进DateTime.Day、DateTime.DayOfYear、DateTime.DayOfYear的算法。 DateTime.DayOfYear、DateTime.Month和DateTime.Year,以及DateTime.GetDate()的内部助手,该助手被DateTime.AddMonths、Utf8Formatter.TryFormat(DateTime, ...)、DateTime.TryFormat和DateTime.ToString等一堆其他方法使用.
private DateTime _dt = DateTime.UtcNow;
private char[] _dest = new char[100];
[Benchmark] public int Day() => _dt.Day;
[Benchmark] public int Month() => _dt.Month;
[Benchmark] public int Year() => _dt.Year;
[Benchmark] public bool TryFormat() => _dt.TryFormat(_dest, out _, "r");
方法 | 运行时 | 平均值 | 比率 |
---|---|---|---|
Day | .NET 6.0 | 5.2080 ns | 1.00 |
Day | .NET 7.0 | 2.0549 ns | 0.39 |
Month | .NET 6.0 | 4.1186 ns | 1.00 |
Month | .NET 7.0 | 2.0945 ns | 0.51 |
Year | .NET 6.0 | 3.1422 ns | 1.00 |
Year | .NET 7.0 | 0.8200 ns | 0.26 |
TryFormat | .NET 6.0 | 27.6259 ns | 1.00 |
TryFormat | .NET 7.0 | 25.9848 ns | 0.94 |
所以,我们已经谈到了对一些类型的改进,但在这个版本中,围绕原始类型的最重要的是 "通用数学",它几乎影响了.NET中的每一个原始类型。这里有一些重要的改进,有些改进已经酝酿了十几年了.
6月份有一篇关于 通用数学 的优秀博文,所以我在这里就不多说了。然而,在高层次上,现在有超过30个新的接口,利用新的C# 11静态抽象接口方法功能,暴露了从指数函数到三角函数到标准数字运算符的广泛操作,所有这些都可以通过泛型来实现,因此你可以编写一个实现,对这些接口进行泛型操作,并将你的代码应用于实现接口的任何类型....NET 7中所有的数字类型都是如此(不仅包括基数,还包括例如BigInteger和Complex)。这个功能的预览版,包括必要的运行时支持、语言语法、C#编译器支持、通用接口和接口实现,都在.NET 6和C# 10中提供,但它不支持生产使用,你必须下载一个实验性参考程序集才能获得。在 dotnet/runtime#65731 中,所有这些支持都作为支持的功能进入了.NET 7。 dotnet/runtime#66748 、 dotnet/runtime#67453 、 dotnet/runtime#69391 、 dotnet/runtime#69582 、 dotnet/runtime#69756 、 dotnet/runtime#71800 都根据.NET 6和.NET 7预览中的使用反馈以及我们API审查小组的适当API审查(.NET中每个新的API都要经过这一过程)更新设计和实施。 dotnet/runtime#67714 添加了对用户定义的检查运算符的支持,这是C# 11的一个新特性,它使运算符的非检查和检查变化都能被暴露出来,编译器会根据检查的上下文选择正确的运算符。 dotnet/runtime#68096 还添加了对C# 11新的无符号右移运算符(>>)的支持。) dotnet/runtime#69651 , dotnet/runtime#67939 , dotnet/runtime#73274 , dotnet/runtime#71033 , dotnet/runtime#71010 , dotnet/runtime#68251 , dotnet/runtime#68217 , 以及 dotnet/runtime#68094 都为各种操作增加了大量新的公共面积,所有这些都有高效的管理实现,在许多情况下都是基于开源的 AMD数学库 .
虽然这些支持都是主要针对外部消费者的,但核心库确实在内部消耗了一些。你可以在 dotnet/runtime#68226 和 dotnet/runtime#68183 这样的PR中看到这些API是如何清理消耗代码的,甚至在保持性能的同时,使用接口来重复Enumerable.Sum/Average/Min/Max中大量的LINQ代码。这些方法对int、long、float、double和decimal有多个重载。GitHub上的差异总结讲述了能够删除多少代码的故事.
另一个简单的例子来自.NET 7中新的System.Formats.Tar库,顾名思义,它用于读写多种 tar文件格式 中的任何一种档案。tar文件格式包括八进制的整数值,所以TarReader类需要解析八进制值。这些值中有些是32位整数,有些是64位整数。与其有两个独立的ParseOctalAsUInt32和ParseOctalAsUInt64方法, dotnet/runtime#74281 ]将这些方法合并成一个ParseOctal ,其中T : struct, INumber 的约束。然后,该实现完全以T为单位,并可用于这些类型中的任何一种(加上任何其他符合约束条件的类型,如果有必要的话)。这个例子特别有趣的是ParseOctal 方法包括使用checked,例如value = checked((value * octalFactor) + T.CreateTruncating(digit)); 。这只是因为C# 11包括上述对 用户定义的检查运算符 的支持,使通用数学接口能够同时支持正常和检查品种,例如IMultiplyOperators<,,>接口包含这些方法.
static abstract TResult operator *(TSelf left, TOther right);
static virtual TResult operator checked *(TSelf left, TOther right) => left * right;
而编译器会根据上下文选择合适的一个.
除了所有获得这些接口的现有类型外,还有一些新的类型。 dotnet/runtime#69204 增加了新的Int128和UInt128类型。由于这些类型实现了所有相关的通用数学接口,它们带有大量的方法,每个都超过100个,所有这些都在托管代码中有效实现。在未来,我们的目标是通过JIT进一步优化其中的一些集合,并利用硬件加速的优势.
来自 @am11 的 dotnet/runtime#63881 对Math.Abs和Math.AbsF(绝对值)进行了迁移,来自 @alexcovington 的 dotnet/runtime#56236 对Math.ILogB和MathF.ILogB(base 2整数对数)进行了迁移。后者的实现是基于相同算法的MUSL libc实现,除了提高性能(部分是通过避免管理代码和本地代码之间的转换,部分是通过实际采用的算法),它还可以从本地代码中删除两个不同的实现,一个来自coreclr端,一个来自mono端,从可维护性的角度来看,这总是一个不错的胜利.
[Benchmark]
[Arguments(12345.6789)]
public int ILogB(double arg) => Math.ILogB(arg);
方法 | 运行时 | 参数 | 平均值 | 比率 |
---|---|---|---|---|
ILogB | .NET 6.0 | 12345.6789 | 4.056 ns | 1.00 |
ILogB | .NET 7.0 | 12345.6789 | 1.059 ns | 0.26 |
其他数学运算也得到了不同程度的改进。Math{F}.Truncate在 dotnet/runtime#65014 中被 @MichalPetryka 改进,使其成为JIT的内在因素,这样在Arm64上,JIT可以直接发出frintz指令。 dotnet/runtime#65584 对Max和Min做了同样的改进,这样可以使用Arm特有的fmax和fmin指令。在 dotnet/runtime#71567 中,几个BitConverter APIs也被变成了本征,以便在一些通用数学场景中能够更好地生成代码.
dotnet/runtime#55121 来自 @key-moon ,它也改进了解析,不过是针对BigInteger,更确切地说,是针对非常非常大的BigIntegers。之前采用的将字符串解析为BigInteger的算法是O(N^2),其中N是数字的数量,虽然算法复杂度比我们通常希望的要高,但它的常数开销很低,所以对于合理大小的数值来说还是合理的。相比之下,有一种替代算法可以在O(N * (log N)^2)时间内运行,但涉及的常数因素要高得多。这使得它只值得为真正的大数字而转换。这就是这个PR所做的。它实现了替代算法,并在输入至少为20000位时切换到它(所以,是的,很大)。但是对于这么大的数字,它有很大的区别.
private string _input = string.Concat(Enumerable.Repeat("1234567890", 100_000)); // "One miiilliiiion digits"
[Benchmark]
public BigInteger Parse() => BigInteger.Parse(_input);
方法 | 运行时 | 平均值 | 比率 |
---|---|---|---|
Parse | .NET 6.0 | 3.474 s | 1.00 |
Parse | .NET 7.0 | 1.672 s | 0.48 |
同样与BigInteger有关(而且不仅仅是针对真正的大数据),来自 @sakno 的 dotnet/runtime#35565 将BigInteger的大部分内部结构修改为基于跨度而非数组。这反过来又使得大量使用堆栈分配和分片来避免分配开销,同时还通过将一些代码从不安全的指针转移到安全的跨度来提高可靠性和安全性。主要的性能影响在分配数量上是可见的,特别是与除法有关的操作.
private BigInteger _bi1 = BigInteger.Parse(string.Concat(Enumerable.Repeat("9876543210", 100)));
private BigInteger _bi2 = BigInteger.Parse(string.Concat(Enumerable.Repeat("1234567890", 100)));
private BigInteger _bi3 = BigInteger.Parse(string.Concat(Enumerable.Repeat("12345", 10)));
[Benchmark]
public BigInteger ModPow() => BigInteger.ModPow(_bi1, _bi2, _bi3);
方法 | 运行时 | 平均值 | 比率 | 已分配 | 分配比率 |
---|---|---|---|---|---|
ModPow | .NET 6.0 | 1.527 ms | 1.00 | 706 B | 1.00 |
ModPow | .NET 7.0 | 1.589 ms | 1.04 | 50 B | 0.07 |
虽然有许多形式的计算会消耗应用程序中的资源,但一些最常见的计算包括处理存储在数组、字符串和跨度中的数据。因此,在每一个.NET版本中,你都会看到一个焦点,那就是尽可能多地从这种情况下消除开销,同时也找到方法来进一步优化开发人员通常执行的具体操作.
让我们从一些新的API开始,这些API可以帮助编写更有效的代码。在检查字符串解析/处理代码时,很常见的是检查字符是否包含在各种集合中。例如,你可能会看到一个寻找ASCII数字的字符的循环.
while (i < str.Length)
{
if (str[i] >= '0' && str[i] <= '9')
{
break;
}
i++;
}
或为ASCII字母 。
while (i < str.Length)
{
if ((str[i] >= 'a' && str[i] <= 'z') || (str[i] >= 'A' && str[i] <= 'Z'))
{
break;
}
i++;
}
或其他此类团体。有趣的是,这类检查的编码方式存在广泛的差异,往往取决于开发者在优化它们方面付出了多少努力,或者在某些情况下甚至可能没有意识到一些性能被留在了桌面上。例如,同样的ASCII字母检查可以被写成.
while (i < str.Length)
{
if ((uint)((c | 0x20) - 'a') <= 'z' - 'a')
{
break;
}
i++;
}
这虽然更 "紧张",但也更简明、更有效。它利用了一些技巧。首先,它不是通过两次比较来确定该字符是否大于或等于下限和小于或等于上限,而是根据该字符和下限之间的距离进行一次比较((uint)(c - 'a'))。如果'c'超出'z',那么'c'-'a'将大于25,比较将失败。如果'c'早于'a',那么'c'-'a'将是负数,然后将其转换为uint,将导致它环绕到一个巨大的数字,也大于25,再次导致比较失败。因此,我们能够支付一个额外的减法来避免整个额外的比较和分支,这几乎总是一个好的交易。第二个技巧是,|0x20。ASCII表有一些深思熟虑的关系,包括大写的'A'和小写的'a'只差一个位('A'是0b1000001,'a'是0b1100001)。因此,从任何小写ASCII字母到大写ASCII字母,我们只需要& ~0x20(关闭该位),而从任何大写ASCII字母到小写ASCII字母的相反方向,我们只需要| 0x20(打开该位)。我们可以在我们的范围检查中利用这一点,将我们的char c规范化为小写字母,这样我们就可以用一个位的低成本来实现小写和大写的范围检查。当然,这些技巧并不是我们希望每个开发者都必须知道并在每次使用时都要写的。取而代之的是,.NET 7在System.Char上公开了一堆新的助手来封装这些常见的检查,并以一种有效的方式完成。Char已经有了IsDigit和IsLetter这样的方法,它们提供了这些名称的更全面的Unicode含义(例如,有~320个Unicode字符被归为 "数字")。现在在.NET 7中,也有了这些帮助工具.
这些方法是由 dotnet/runtime#69318 添加的,它还在 dotnet/runtime 中执行此类检查的几十个地方采用了这些方法(其中许多采用了效率较低的方法).
另一个专注于封装通用模式的新API是新的MemoryExtensions.CommonPrefixLength方法,由 dotnet/runtime#67929 引入。该方法接受两个ReadOnlySpan 实例或一个Span 和一个ReadOnlySpan ,以及一个可选的IEqualityComparer ,并返回每个输入跨度开始时相同元素的数量。当你想知道两个输入的第一处不同时,这很有用。来自 @gfoidl 的 dotnet/runtime#68210 然后利用新的Vector128功能,提供了一个基本的矢量化实现。因为它要比较两个序列并寻找它们之间的第一个不同点,这个实现使用了一个巧妙的技巧,那就是用一个单一的方法来实现序列与字节的比较。如果被比较的T是bitwise-equatable,并且没有提供自定义的平等比较器,那么它就把跨度中的引用重新解释为字节引用,并使用单一的共享实现.
另一组新的API是IndexOfAnyExcept和LastIndexOfAnyExcept方法,由 dotnet/runtime#67941 引入,并由 dotnet/runtime#71146 和 dotnet/runtime#71278 用于各种附加调用站点。虽然有些拗口,但这些方法还是很方便的。它们的作用就像它们的名字一样:IndexOf(T value)搜索输入中第一个出现的值,而IndexOfAny(T value0, T value1, ...)搜索输入中第一个出现的value0, value1等的任何一个。而 IndexOfAnyExcept(T value) 则是搜索不等于 value 的东西的第一次出现,同样 IndexOfAnyExcept(T value0, T value1, ...) 也是搜索不等于 value0, value1 等东西的第一次出现。例如,假设你想知道一个整数数组是否完全是0,你现在可以写成.
bool allZero = array.AsSpan().IndexOfAnyExcept(0) < 0;
dotnet/runtime#73488 也将这一重载矢量化.
private byte[] _zeros = new byte[1024];
[Benchmark(Baseline = true)]
public bool OpenCoded()
{
foreach (byte b in _zeros)
{
if (b != 0)
{
return false;
}
}
return true;
}
[Benchmark]
public bool IndexOfAnyExcept() => _zeros.AsSpan().IndexOfAnyExcept((byte)0) < 0;
方法 | 平均值 | 比率 |
---|---|---|
OpenCoded | 370.47 ns | 1.00 |
IndexOfAnyExcept | 23.84 ns | 0.06 |
当然,虽然新的 "索引的 "变化是有帮助的,但我们已经有一堆这样的方法了,而且重要的是它们要尽可能的高效。这些核心的IndexOf{Any}方法被用于大量的地方,其中很多是对性能敏感的,所以每一个版本都会得到额外的温柔呵护。虽然像 dotnet/runtime#67811 这样的PR通过密切关注正在生成的汇编代码获得了收益(在这种情况下,调整了IndexOf和IndexOfAny中用于Arm64的一些检查以获得更好的利用率),但这里最大的改进是在一些地方添加了矢量化而以前没有使用过,或者矢量化方案被彻底修改以获得显著收益。让我们从 dotnet/runtime#63285 开始,它为许多使用IndexOf和LastIndexOf的字节和字符的 "子串 "带来了巨大的改进。以前,对于像str.IndexOf("hello")这样的调用,其实现基本上是重复搜索 "h",当找到 "h "时,再执行SequenceEqual来匹配剩余部分。然而,正如你所想象的那样,很容易遇到这样的情况:被搜索的第一个字符非常常见,以至于你不得不经常跳出矢量循环,以便进行完整的字符串比较。相反,PR实现了一种 基于SIMD友好算法的子串搜索算法 。它不是只搜索第一个字符,而是对第一个和最后一个字符在适当的距离内进行矢量化搜索。在我们的 "hello "例子中,在任何给定的输入中,找到一个 "h "的可能性要比找到一个 "h "后面跟着一个 "o "的可能性大得多,因此这个实现能够在矢量循环中停留更长的时间,获得更少的误报,迫使它走SequenceEqual路线。该实现还可以处理所选的两个字符相等的情况,在这种情况下,它会迅速寻找另一个不相等的字符,以使搜索效率最大化。我们可以通过几个例子看到这一切的影响.
private static readonly string s_haystack = new HttpClient().GetStringAsync("https://www.gutenberg.org/files/1661/1661-0.txt").Result;
[Benchmark]
[Arguments("Sherlock")]
[Arguments("elementary")]
public int Count(string needle)
{
ReadOnlySpan<char> haystack = s_haystack;
int count = 0, pos;
while ((pos = haystack.IndexOf(needle)) >= 0)
{
haystack = haystack.Slice(pos + needle.Length);
count++;
}
return count;
}
这是从古腾堡计划中拉下《夏洛克-福尔摩斯历险记》的文本,然后用IndexOf来计算文本中出现的 "夏洛克 "和 "初级 "的基准。在我的机器上,我得到这样的结果.
方法 | 运行时 | 基准 | 平均值 | 比率 |
---|---|---|---|---|
Count | .NET 6.0 | Sherlock | 43.68 us | 1.00 |
Count | .NET 7.0 | Sherlock | 48.33 us | 1.11 |
Count | .NET 6.0 | elementary | 1,063.67 us | 1.00 |
Count | .NET 7.0 | elementary | 56.04 us | 0.05 |
对于 "Sherlock "来说,.NET 7的性能实际上比.NET 6要差一些;不多,但也有10%。这是因为在源文本中只有很少的大写字母 "S",确切地说,在文档的593,836个字符中只有841个。在起始字符的密度只有0.1%的情况下,新的算法并没有带来多少好处,因为现有的算法只搜索了第一个字符,几乎抓住了所有可能的矢量化收益,而且我们在搜索'S'和'k'时确实付出了一些开销,而之前我们只搜索了'S'。相比之下,文件中有54,614个'e'字符,几乎占到源文件的10%。在这种情况下,.NET 7比.NET 6快了20倍,在.NET 7上计算所有的'e'需要53us,而在.NET 6上则需要1084us。在这种情况下,新方案产生了巨大的收益,通过对'e'和特定距离的'y'进行矢量搜索,这种组合的频率低得多。这是其中一种情况,尽管我们可以看到一些特定的输入有小的退步,但总体上还是有巨大的观察收益.
另一个显著改变所采用的算法的例子是 dotnet/runtime#67758 ,它使某种程度的矢量化被应用到IndexOf("...", StringComparison.OrdinalIgnoreCase)。以前,这个操作是通过一个相当典型的子串搜索来实现的,在输入字符串的每个位置做一个内循环来比较目标字符串,除了对每个字符执行ToUpper,以便以不区分大小写的方式进行。现在有了这个基于Regex以前使用的方法的PR,如果目标字符串以ASCII字符开始,实现可以使用IndexOf(如果该字符不是ASCII字母)或IndexOfAny(如果该字符是ASCII字母)来快速跳到第一个可能的匹配位置。让我们来看看与我们刚才看的完全相同的基准,但调整为使用OrdinalIgnoreCase.
private static readonly string s_haystack = new HttpClient().GetStringAsync("https://www.gutenberg.org/files/1661/1661-0.txt").Result;
[Benchmark]
[Arguments("Sherlock")]
[Arguments("elementary")]
public int Count(string needle)
{
ReadOnlySpan<char> haystack = s_haystack;
int count = 0, pos;
while ((pos = haystack.IndexOf(needle, StringComparison.OrdinalIgnoreCase)) >= 0)
{
haystack = haystack.Slice(pos + needle.Length);
count++;
}
return count;
}
在这里,这两个词在.NET 7上比在.NET 6上快了约4倍.
方法 | 运行时 | 基准 | 平均值 | 比率 |
---|---|---|---|---|
Count | .NET 6.0 | Sherlock | 2,113.1 us | 1.00 |
Count | .NET 7.0 | Sherlock | 467.3 us | 0.22 |
Count | .NET 6.0 | elementary | 2,325.6 us | 1.00 |
Count | .NET 7.0 | elementary | 638.8 us | 0.27 |
因为我们现在做的是一个矢量的IndexOfAny('S', 's')或IndexOfAny('E', 'e'),而不是手动行走每个字符并进行比较。( dotnet/runtime#73533 现在使用同样的方法来处理IndexOf(char, StringComparison.OrdinalIgnoreCase)。) 。
另一个例子来自 dotnet/runtime#67492 ,来自 @gfoidl 。它更新了MemoryExtensions.Contains,采用了我们之前讨论的在矢量操作结束时处理剩余元素的方法:处理最后一个矢量的数据,即使这意味着重复一些已经完成的工作。这对较小的输入特别有帮助,否则处理时间可能会被这些遗留物的串行处理所支配.
private byte[] _data = new byte[95];
[Benchmark]
public bool Contains() => _data.AsSpan().Contains((byte)1);
方法 | 运行时 | 平均值 | 比率 |
---|---|---|---|
Contains | .NET 6.0 | 15.115 ns | 1.00 |
Contains | .NET 7.0 | 2.557 ns | 0.17 |
dotnet/runtime#60974 来自 @alexcovington ,扩大了 IndexOf 的影响。在此PR之前,IndexOf是针对一个和两个字节大小的原始类型的矢量化,但此PR也将其扩展到四个和八个字节大小的原始类型。与其他大多数矢量实现一样,它检查T是否是位数相等的,这对矢量化很重要,因为它只看内存中的位数,而不注意可能被定义在该类型上的任何等价实现。在今天的实践中,这意味着这只限于运行时对其有深入了解的少数类型(Boolean, Byte, SByte, UInt16, Int16, Char, UInt32, Int32, UInt64, Int64, UIntPtr, IntPtr, Rune, 和枚举),但在理论上它可以在未来被扩展.
private int[] _data = new int[1000];
[Benchmark]
public int IndexOf() => _data.AsSpan().IndexOf(42);
方法 | 运行时 | 平均值 | 比率 |
---|---|---|---|
IndexOf | .NET 6.0 | 252.17 ns | 1.00 |
IndexOf | .NET 7.0 | 78.82 ns | 0.31 |
Performance Improvements in .NET 7 。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可.
欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布.
如有任何疑问,请与我联系 ( MingsonZheng@outlook.com ) 。
最后此篇关于【译】.NET7中的性能改进(九)的文章就讲到这里了,如果你想了解更多关于【译】.NET7中的性能改进(九)的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
任何 Web、桌面或移动开发人员都经常使用图像。你可以从 C#、HTML、XAML、CSS、C++、VB、TypeScript 甚至代码注释中引用它们。有些图像是本地的,有些存在于线上或网络共享中
您是否曾经难以理解一个提交在做什么或者为什么要做?在审查或协作代码更改时,您是否希望有更多的清晰度和上下文?如果您的回答是肯定的,那么您会喜欢 GitHub Copilot 为您所做的——生成提交
Visual Studio 订阅(无论是专业版还是企业版)提供的不仅仅是软件使用权;这是一个全面的工具包,旨在显著提高您的开发能力和职业发展。这些订阅每年可以为您节省数千美元,提供各种服务,从每月
随着最近 Visual Studio 的资源管理器的改进,开发人员将得到一种全新的享受!我们非常激动地宣布重新设计的 Visual Studio 资源管理器,相信我们,它将改变游戏规则。 在
委托在现代代码中无处不在;委托是一种类型,它表示对具有特定参数列表和返回类型的方法的引用。开发人员使用委托将方法作为参数传递给其他方法。您可能熟悉的一个例子是事件处理程序。处理程序是可以通过委托调
来自Sergey Tepliakov的 https://sergeyteplyakov.github.io/Blog/csharp/2024/06/14/Custom_Task_Schedule
GitHub Copilot 是一个改变游戏规则的人工智能助手,可以彻底改变您在 Visual Studio 中的编码流程。在我们的视频系列中,Bruno Capuano 探讨了这个智
在17.10预览版2中,我们刚刚发布了预览支持,可以直接在 Visual Studio 的工作文件中查看 GitHub 和 Azure DevOps 的拉取请求注释。作为开发者社区中最受欢迎的 G
MSBuild 是 .NET 开发体验的基本组成部分,但它对新手和有经验的开发人员都具有挑战性。为了帮助开发人员更好地理解他们的项目文件,并访问需要直接编辑项目文件的高级功能,我们发布了一个实验性
通过使用代码覆盖率功能,您可以发现您的测试需要改进的地方,并使您的软件更加健壮和可靠。在这篇文章中,我们将介绍我们在 Visual Studio Enterprise 2022 中引入的 Code
GitHub Copilot 简介 GitHub Copilot 是一个新工具,可以帮助您在人工智能的帮助下更快,更智能地编写代码。它可以建议代码补全,生成代码片段,甚至为您编写整个函数。Git
序:最近看了一篇谷歌工程师总结自己十年来所学到的软技能,感觉说的很棒,特地翻译成中文,希望能分享给更多的人,原文内容非常多,计划分几篇翻译出来,个人的水平有限,感兴趣的朋友也可以进去看英文原文。原文地
本文为翻译 。 原文地址: 2023 State of Web Components: Today's standards and a glimpse into the future. 。
任何 Web、桌面或移动开发人员都经常使用图像。你可以从 C#、HTML、XAML、CSS、C++、TypeScript 甚至代码注释中引用它们。有些图像是本地的,有些存在于线上或网络共享中,而其
在 Visual Studio 2022 中,Web 开发人员的主要场景之一是使用 ASP.NET Core 创建 Web API。在 Visual Studio 2022 17.6 的最新预览
.NET 在数年前就在语言和库中添加了 async/await。在那段时间里,它像野火一样蔓延开来,不仅在 .NET 生态系统中,而且在无数其他语言和框架中被复制。在 .NET 中也看到了大量的
原文 | Stephen Toub 翻译 | 郑子铭 Mono 到目前为止,我一直提到 "JIT"、"GC "和 "运行时&quo
原文 | Stephen Toub 翻译 | 郑子铭 原始类型和数值 (Primitive Types and Numerics) 我们已经看过了代码生成和GC,线程和矢量化,互操
原文 | Stephen Toub 翻译 | 郑子铭 同样,为了不做不必要的工作,有一个相当常见的模式出现在string.Substring和span.Slice等方法中。
原文 | Stephen Toub 翻译 | 郑子铭 New APIs 在.NET 7中,Regex得到了几个新的方法,所有这些方法都能提高性能。新的API的简单性可能也误导了为实
我是一名优秀的程序员,十分优秀!