.NET8极致性能优化Primitives-Enum

,点击上方蓝字 江湖评谈设为星标

outside_default.png

前言

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.020.021 ns1.00-NA
IsDefined.NET 8.02.502 ns0.12-NA
GetName.NET 7.024.563 ns1.00-NA
GetName.NET 8.03.648 ns0.15-NA
GetNames.NET 7.037.138 ns1.0080 B1.00
GetNames.NET 8.022.688 ns0.6180 B1.00
GetValues.NET 7.0694.356 ns1.00224 B1.00
GetValues.NET 8.039.406 ns0.0656 B0.25
GetUnderlyingValues.NET 7.041.012 ns1.0056 B1.00
GetUnderlyingValues.NET 8.017.249 ns0.4256 B1.00
EnumToString.NET 7.032.842 ns1.0024 B1.00
EnumToString.NET 8.014.620 ns0.4424 B1.00
TryParse.NET 7.049.121 ns1.00-NA
TryParse.NET 8.030.394 ns0.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.081.58 ns1.0080 B1.00
Interpolate.NET 8.034.41 ns0.42-0.00

往期精彩回顾

.NET8 JIT核心:分层编译的原理

新版.Net性能有没有达到C++90%?

面试官问.Net对象赋值为null,就会被GC回收吗?

outside_default.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值