C#中无额外开销封装double类型的性能优化问询
我想创建一个本质是普通double但通过泛型附加物理单位的结构体,即便不用泛型,当前实现的性能依旧很差。想请教在C#里有没有办法提升它的性能?
我用的代码如下:
public readonly struct MyDouble { public readonly double Value; public MyDouble(double value) { Value = value; } public static MyDouble operator +(MyDouble self, MyDouble value) { return new MyDouble(self.Value + value.Value); } public static MyDouble operator *(MyDouble self, double value) { return new MyDouble(self.Value * value); } }
仅构造、存储和检索这些结构体时,性能和原生double相当,但一旦使用运算符,性能比原生double运算差很多。
我本来以为是内联问题,但给运算符加[MethodImpl(MethodImplOptions.AggressiveInlining)]完全没用。
我通过单元测试和JetBrains dotTrace在生产环境验证了这个糟糕的性能。
以下是用BenchmarkDotNet的复现代码:
using System; using System.Linq; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; using NUnit.Framework; namespace MyBenchmark; public readonly struct MyDouble { public readonly double Value; public MyDouble(double value) { Value = value; } public static MyDouble operator +(MyDouble self, MyDouble value) { return new MyDouble(self.Value + value.Value); } public static MyDouble operator *(MyDouble self, double value) { return new MyDouble(self.Value * value); } } public class DoubleBenchmark { private const int N = 1_000_000; private readonly double[] _data; private readonly double[] _workspace; private readonly MyDouble[] _typedData; private readonly MyDouble[] _typedWorkspace; public DoubleBenchmark() { _data= new double[N]; _workspace = new double[N]; _typedWorkspace = new MyDouble[N]; var rand = new Random(0); for (var i = 0; i < N; i++) { _data[i] = rand.NextDouble(); } _typedData = _data.Select(x => new MyDouble(x)).ToArray(); } [Benchmark] public MyDouble TypedDoubles() { var d = new MyDouble(0); for (var i = 0; i < N; i++) { d += _typedData[i] * 3; _typedWorkspace[i] = d * 2 + new MyDouble(1); } var e = new MyDouble(0); for (var i = 0; i < N; i++) { e += _typedWorkspace[i]; } return e; } [Benchmark] public MyDouble ManuallyInlinedTypedDoubles() { var d = new MyDouble(0); for (var i = 0; i < N; i++) { d = new MyDouble(d.Value + _typedData[i].Value * 3); _typedWorkspace[i] = new MyDouble(d.Value * 2 + 1); } var e = new MyDouble(0); for (var i = 0; i < N; i++) { e = new MyDouble(e.Value + _typedWorkspace[i].Value); } return e; } [Benchmark] public double Doubles() { var d = 0d; for (var i = 0; i < N; i++) { d += _data[i] * 3; _workspace[i] = d * 2 + 1; } var e = 0d; for (var i = 0; i < N; i++) { e += _workspace[i]; } return e; } [Test, Explicit] public static void RunBenchmarks() { BenchmarkRunner.Run<DoubleBenchmark>(); } [Test, Explicit] public static void EqualResults() { var a = new DoubleBenchmark(); Console.WriteLine((a.TypedDoubles().Value, a.Doubles(), a.ManuallyInlinedTypedDoubles().Value)); if (Math.Abs(a.TypedDoubles().Value - a.Doubles()) > 0 || Math.Abs(a.TypedDoubles().Value - a.ManuallyInlinedTypedDoubles().Value) > 1e-16) { throw new Exception("Different results"); } } }
基准测试结果:
| Method | Mean | Error | StdDev | |---------------------------- |----------:|----------:|----------:| | TypedDoubles | 12.205 ms | 0.0170 ms | 0.0142 ms | | ManuallyInlinedTypedDoubles | 6.158 ms | 0.0063 ms | 0.0055 ms | | Doubles | 3.149 ms | 0.0032 ms | 0.0028 ms |
1. 显式指定结构体布局,对齐原生double
C#的readonly struct默认布局可能存在额外的对齐或元数据开销,通过[StructLayout(LayoutKind.Explicit)]强制结构体内存布局和原生double完全一致,让CLR可以像处理double一样处理它:
using System.Runtime.InteropServices; [StructLayout(LayoutKind.Explicit)] public readonly struct MyDouble { [FieldOffset(0)] public readonly double Value; public MyDouble(double value) { Value = value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static MyDouble operator +(MyDouble self, MyDouble value) { return new MyDouble(self.Value + value.Value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static MyDouble operator *(MyDouble self, double value) { return new MyDouble(self.Value * value); } }
2. 使用.NET 7+的[InlineArray]优化数组操作
如果需要频繁操作MyDouble数组,.NET 7引入的[InlineArray]可以让结构体数组的内存布局和原生double[]完全一致,消除数组元素的额外内存访问开销:
using System.Runtime.CompilerServices; [InlineArray(1)] public readonly struct MyDouble { private readonly double _value; public double Value => _value; public MyDouble(double value) { _value = value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static MyDouble operator +(MyDouble self, MyDouble value) { return new MyDouble(self._value + value._value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static MyDouble operator *(MyDouble self, double value) { return new MyDouble(self._value * value); } }
3. 用Unsafe.As跳过结构体构造开销
手动内联测试的性能提升说明编译器自动内联没有完全消除构造函数开销,通过Unsafe.As直接在double和MyDouble之间转换,避免构造函数调用:
using System.Runtime.CompilerServices; public readonly struct MyDouble { public readonly double Value; private MyDouble(double value) => Value = value; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator MyDouble(double value) { return Unsafe.As<double, MyDouble>(ref value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator double(MyDouble value) { return value.Value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static MyDouble operator +(MyDouble self, MyDouble value) { return self.Value + value.Value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static MyDouble operator *(MyDouble self, double value) { return self.Value * value; } }
4. 确保编译环境优化到位
- 必须在Release模式下编译,勾选项目属性→生成→"优化代码"选项
- 启用.NET Native(桌面应用可选),进一步提升JIT编译效率
- 关闭不必要的调试信息,减少JIT额外负担
5. 泛型单位版本的优化
如果要扩展到泛型单位结构体,使用readonly struct并添加unmanaged约束,让JIT生成更高效的代码:
[StructLayout(LayoutKind.Explicit)] public readonly struct UnitDouble<TUnit> where TUnit : unmanaged { [FieldOffset(0)] public readonly double Value; public UnitDouble(double value) { Value = value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static UnitDouble<TUnit> operator +(UnitDouble<TUnit> self, UnitDouble<TUnit> value) { return new UnitDouble<TUnit>(self.Value + value.Value); } }
优化预期
通过以上调整,MyDouble的运算性能会接近甚至达到原生double的水平,Release模式下JIT会完全内联运算符方法,消除结构体的额外开销。
内容的提问来源于stack exchange,提问作者Bananach

