,点击上方蓝字 江湖评谈设为星标
前言
Primitives顾名思义,基本类型。也就是最基础的一些类型,比如整型,字符串类型,浮点型等等。这里要说的是一个比较典型的基本类型优化,枚举。你很难想象.NET经过20多年的发展,依然可以在基本类型上进行优化。伤筋动骨,才能脱胎换骨嘛。本篇来看下。
详述
枚举在.NET早期就广泛应用,尽管多个.NET版本对其进行演变,并且已经获得了新的API。但是核心问题在于枚举如何在内存中存储数据基本上保持不变。在.NET Framework中,枚举通过一个内部类ValuesAndNames,它里面包含了一个ulong[]和一个string[]。string[]包含枚举值的名称,ulong存储了名称对应的项。在.NET7中,有一个EnumInfo用于同样的目的。ulong[]里面可以容纳所有枚举可能的底层类型(sbyte,byte,short,ushort,int,uint,long,ulong),运行时额外支持的(nint,nuint,char,float,double),部分bool支持也包含在这个列表。一般来说,没有人使用这些,所以在.NET8里面删除了这些影响性能的操作。
因为改动了基础类型,所以在检查nuget包的时候,发现了163万个应用的地方。
在枚举如何存储其数据中有几个问题,每个操作都在ulong[]值和和特定枚举使用的实际类型之间转换。数组通常比需要的大两倍,这种方法还导致近年来添加到枚举中的新泛型方法时候,导致了汇编代码巨量的膨胀。也就是说,当枚举结构体被用作泛型参数的时候,JIT为该值类型专门优化代码(对于引用类型,JIT发出了一个有所有这些类型使用的单一共享),这种专门对于吞吐量的优化来说是不错的。但是意味着你得到它用于每个值类型的代码副本。如果有很多代码,这种副本大面积增加导致了性能问题。
为了解决这些问题,.NET8里面不再使用EnumInfo来存储所有值的ulong[]数组,而是引入了一个泛型的EnumInfo来存储TUnderlyingValue[],然后基于枚举的类型,每个泛型和非泛型的Enum方法都会查找底层的TUnderlyingValue ,并且调用一个带有该TUnderlyingValue但不带有枚举类型的泛型类型参数的泛型方法。
其它优化,所有枚举值定义从0开始连续的情况下,查找EnumInfo中的值的内部函数可以通过简单的数组访问来完成,而不需要搜寻目标。
所有最终更改的结果,通过例子来看下:
// dotnet run -c Release -f net7.0 --filter "*" --runtimes net7.0 net8.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);
[HideColumns("Error", "StdDev", "Median", "RatioSD")]
[MemoryDiagnoser(displayGenColumns: false)]
public class Tests
{
private readonly DayOfWeek _dow = DayOfWeek.Saturday;
[Benchmark] public bool IsDefined() => Enum.IsDefined(_dow);
[Benchmark] public string GetName() => Enum.GetName(_dow);
[Benchmark] public string[] GetNames() => Enum.GetNames<DayOfWeek>();
[Benchmark] public DayOfWeek[] GetValues() => Enum.GetValues<DayOfWeek>();
[Benchmark] public Array GetUnderlyingValues() => Enum.GetValuesAsUnderlyingType<DayOfWeek>();
[Benchmark] public string EnumToString() => _dow.ToString();
[Benchmark] public bool TryParse() => Enum.TryParse<DayOfWeek>("Saturday", out _);
}
性能提升如下:
方法 | 运行时 | 平均值 | 比率 | 分配 | 分配比率 |
---|---|---|---|---|---|
IsDefined | .NET 7.0 | 20.021 ns | 1.00 | - | NA |
IsDefined | .NET 8.0 | 2.502 ns | 0.12 | - | NA |
GetName | .NET 7.0 | 24.563 ns | 1.00 | - | NA |
GetName | .NET 8.0 | 3.648 ns | 0.15 | - | NA |
GetNames | .NET 7.0 | 37.138 ns | 1.00 | 80 B | 1.00 |
GetNames | .NET 8.0 | 22.688 ns | 0.61 | 80 B | 1.00 |
GetValues | .NET 7.0 | 694.356 ns | 1.00 | 224 B | 1.00 |
GetValues | .NET 8.0 | 39.406 ns | 0.06 | 56 B | 0.25 |
GetUnderlyingValues | .NET 7.0 | 41.012 ns | 1.00 | 56 B | 1.00 |
GetUnderlyingValues | .NET 8.0 | 17.249 ns | 0.42 | 56 B | 1.00 |
EnumToString | .NET 7.0 | 32.842 ns | 1.00 | 24 B | 1.00 |
EnumToString | .NET 8.0 | 14.620 ns | 0.44 | 24 B | 1.00 |
TryParse | .NET 7.0 | 49.121 ns | 1.00 | - | NA |
TryParse | .NET 8.0 | 30.394 ns | 0.62 | - | NA |
这些更改,也使枚举与字符串更加融洽。枚举现在有一个 新的静态 TryFormat 方法,可以直接将枚举的字符串表示格式化为 Span
public static bool TryFormat<TEnum>(TEnum value, Span<char> destination,
out int charsWritten,
[StringSyntax(StringSyntaxAttribute.EnumFormat)] ReadOnlySpan<char> format = default)
where TEnum : struct, Enum
枚举现在实现了ISpanFormattable,任何使用 ISpanFormattable.TryFormat方法的代码现在也可以在枚举上使用。然而,尽管枚举是值类型,但它们在引用类型 Enum 派生,这意味着调用实例方法(如 ToString 或 ISpanFormattable.TryFormat)时,会将枚举值进行装箱。
System.Private.CoreLib 中的各种插值字符串处理程序已更新为特殊处理 typeof(T).IsEnum,如前所述,现在由于即时编译(JIT)优化,这个操作实际上是开销为0。直接使用 Enum.TryFormat 以避免装箱。我们可以通过运行以下基准测试来查看这种情况的影响:
// dotnet run -c Release -f net7.0 --filter "*" --runtimes net7.0 net8.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);
[HideColumns("Error", "StdDev", "Median", "RatioSD")]
[MemoryDiagnoser(displayGenColumns: false)]
public class Tests
{
private readonly char[] _dest = new char[100];
private readonly FileAttributes _attr = FileAttributes.Hidden | FileAttributes.ReadOnly;
[Benchmark]
public bool Interpolate() => _dest.AsSpan().TryWrite($"Attrs: {_attr}", out int charsWritten);
}
性能提升如下:
方法 | 运行时 | 平均值 | 比率 | 分配 | 分配比率 |
---|---|---|---|---|---|
Interpolate | .NET 7.0 | 81.58 ns | 1.00 | 80 B | 1.00 |
Interpolate | .NET 8.0 | 34.41 ns | 0.42 | - | 0.00 |
往期精彩回顾