前言
書接上回,我們討論了在.NET8中新增的UnsafeAccessor
,並且通過UnsafeAccessor
訪問了私有成員,這極大的方便了我們代碼的編寫,當然也聊到了它當前存在的一些侷限性,那麼它的性能到底如何?我們今天就來實際測試一下。
測試代碼
話不多說,直接上代碼,本次測試代碼如下:
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
using Perfolizer.Horology;
[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class AccessBenchmarks
{
public static readonly A TestInstance = new();
public static readonly Action<A, int> SetDelegate;
public static readonly Func<A, int> GetDelegate;
public static readonly PropertyInfo ValueProperty;
public static readonly MethodInfo SetValueMethod;
public static readonly MethodInfo GetValueMethod;
public static readonly Func<A, int> GetValueExpressionFunc;
public static readonly Action<A, int> SetValueExpressionAction;
static AccessBenchmarks()
{
TestInstance = new();
ValueProperty = typeof(A).GetProperty("Value");
SetValueMethod = ValueProperty.GetSetMethod();
GetValueMethod = ValueProperty.GetGetMethod();
SetDelegate = CreateSetDelegate();
GetDelegate = CreateGetDelegate();
GetValueExpressionFunc = CreateGetValueExpressionFunc();
SetValueExpressionAction = CreateSetValueExpressionAction();
}
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_Value")]
static extern int GetValueUnsafe(A a);
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_Value")]
static extern void SetValueUnsafe(A a, int value);
[Benchmark]
public void UnsafeAccessor()
{
SetValueUnsafe(TestInstance, 10);
var value = GetValueUnsafe(TestInstance);
}
[Benchmark]
public void Reflection()
{
SetValueMethod.Invoke(TestInstance, new object[] { 10 });
var value = GetValueMethod.Invoke(TestInstance, new object[] { });
}
[Benchmark]
public void Emit()
{
SetDelegate(TestInstance, 10);
var value = GetDelegate(TestInstance);
}
[Benchmark]
public void ExpressionTrees()
{
SetValueExpressionAction(TestInstance, 10);
var value = GetValueExpressionFunc(TestInstance);
}
[Benchmark]
public void Direct()
{
TestInstance.Value = 10;
var value = TestInstance.Value;
}
private static Action<A, int> CreateSetDelegate()
{
var dynamicMethod = new DynamicMethod("SetValue", null, new[] { typeof(A), typeof(int) }, typeof(A));
var ilGenerator = dynamicMethod.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.EmitCall(OpCodes.Call, SetValueMethod, null);
ilGenerator.Emit(OpCodes.Ret);
return (Action<A, int>)dynamicMethod.CreateDelegate(typeof(Action<A, int>));
}
private static Func<A, int> CreateGetDelegate()
{
var dynamicMethod = new DynamicMethod("GetValue", typeof(int), new[] { typeof(A) }, typeof(A));
var ilGenerator = dynamicMethod.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.EmitCall(OpCodes.Call, GetValueMethod, null);
ilGenerator.Emit(OpCodes.Ret);
return (Func<A, int>)dynamicMethod.CreateDelegate(typeof(Func<A, int>));
}
private static Func<A, int> CreateGetValueExpressionFunc()
{
var instance = Expression.Parameter(typeof(A), "instance");
var getValueExpression = Expression.Lambda<Func<A, int>>(
Expression.Property(instance, ValueProperty),
instance);
return getValueExpression.Compile();
}
private static Action<A, int> CreateSetValueExpressionAction()
{
var instance = Expression.Parameter(typeof(A), "instance");
var value = Expression.Parameter(typeof(int), "value");
var setValueExpression = Expression.Lambda<Action<A, int>>(
Expression.Call(instance, ValueProperty.GetSetMethod(true), value),
instance, value);
return setValueExpression.Compile();
}
}
public class A
{
public int Value { get; set; }
}
public class Program
{
public static void Main()
{
Console.WriteLine(AccessBenchmarks.TestInstance);
var summary = BenchmarkRunner.Run<AccessBenchmarks>(DefaultConfig.Instance.WithSummaryStyle(new SummaryStyle(
cultureInfo: null, // use default
printUnitsInHeader: true,
printUnitsInContent: true,
sizeUnit: SizeUnit.B,
timeUnit: TimeUnit.Nanosecond,
printZeroValuesInContent: true,
ratioStyle: RatioStyle.Trend // this will print the ratio column
)));
}
}
在測試代碼中,我們使用了BenchmarkDotNet
來進行測試,測試的內容包括:
UnsafeAccessor
:使用UnsafeAccessor
特性來訪問私有成員Reflection
:使用反射訪問私有成員Emit
:使用Emit
+動態方法訪問私有成員ExpressionTrees
:使用表達式樹+委託來訪問私有成員Direct
:直接訪問私有成員
測試結果如下圖所示,可以看到使用UnsafeAccessor的性能是最好的,其次是直接訪問私有成員,最差的是反射。這其實是出乎我的意料的,因爲我認爲它最多和直接訪問私有成員的性能差不多,但是實際上它的性能比直接訪問私有成員還要好,當然也有可能是統計的誤差,0.0000ns這個尺度已經非常小了。
深入探究
看到這裏我想大家都有很多疑問,實際上作者本人看到這裏也是有很多的疑問,主要是這兩個:
- 是什麼原因讓.NET社區想加入這個API?
- 它是如何做到訪問私有成員的?
- 爲什麼性能會這麼好?
新增功能的原因
如果要了解這個功能背後的東西,那麼我們首先就要找到對應這個API的Issues,按照.NET社區的規範,所有的API都需要提交Issues,然後經過API Review,多輪討論設計以後,纔會開始開發。
首先我們定位到Issue是這一個,在Issue中我們可以瞭解到這個API主要是爲了給System.Text.Json或EF Core這種需要訪問私有成員的框架使用,因爲目前它們都是基於Emit動態代碼生成實現的,但是Emit不能在AOT中使用,現階段只能使用慢速的反射API,所以迫切引入了一種零開銷的私有成員訪問機制。
https://github.com/dotnet/runtime/issues/86161
如何做到訪問私有成員?
翻閱一下整個API提案Issue的討論,我們可以找到具體實現的Issue,所以我們要了解它背後的原理的話,就需要跳轉到對應的Issue。
在這裏可以看到目前還沒有做泛型的實現,非泛型的已經在下面鏈接中實現了,一個是爲CoreCLR做的實現,另外一個是爲Mono做的實現。
我們目前只關注CoreCLR,點開這個Issue。
https://github.com/dotnet/runtime/issues/86161
可以看到將這個任務拆成了幾個部分,他們都在在一個PR中完成的,其中包括定義了UnsafeAccessor
特性,在JIT中的實現,以及NativeAOT中進行了支持,另外編寫了單元測試加入了有效的診斷方案。
那麼來看看這個PR裏面做了什麼吧。
https://github.com/dotnet/runtime/pull/86932
由於PR非常的長,大家有興趣可以點進去看看,低於8GB內存的小夥伴就要小心了。簡單的來說這次修改主要就是兩塊地方,一塊是JIT相關的修改,JIT這裏主要是支持UnsafeAccessor
和static extern int
聲明函數的用法,需要支持方法的IL Body爲空,然後在JIT時根據特性爲它插入代碼。
首先我們來看JIT的處理,這塊代碼主要就是修改了jitinterface.cpp
,可以看到它調用了TryGenerateUnsafeAccessor
方法:
這個TryGenerateUnsafeAccessor
方法實現在prestub.cpp
中,這個prestub.cpp
實現了一些預插樁的操作,TryGenerateUnsafeAccessor
方法實現如下所示:
它針對UnsafeAccessorKind
的不同枚舉做了校驗,防止出現運行時崩潰的情況:
然後調用了GenerateAccessor
方法來生成IL:
在GenerateAccessor
裏面就是使用Emit進行代碼生成:
所以從JIT的實現來看,它其實核心原理就是Emit代碼生成,並沒有太多特殊的東西。
另外是關於NativeAOT的實現,首先修改了NativeAotILProvider.cs
這個類,這個類的主要作用就是在進行NativeAot
的時候提供IL給JIT預先編譯使用:
關鍵也是在GenerateAccessor
方法裏面,在這裏生成了對應的IL代碼:
總結一下,UnsafeAccessor
實現原理還是使用的IL動態生成技術,只不過它是在JIT內部實現的。
爲什麼性能這麼好?
那麼它爲什麼性能要比我們在C#代碼中自己寫Emit要更好呢?其實原因也是顯而易見的,我們自己編寫的Emit代碼中間有一層DynamicMethod
的委託調用,增加了開銷,而UnsafeAccessor
它直接就是一個static extern int GetValueUnsafe(A a);
方法,沒有中間開銷,而且它IL Body很小,可以被內聯。
總結
通過對.NET8中新增的UnsafeAccessor
特性的深入探究,我們得到了一些啓示和理解。首先,UnsafeAccessor
的引入並非無中生有,而是應運而生,它是爲了滿足System.Text.Json或EF Core這類框架在訪問私有成員時的需求,因爲它們目前大多基於Emit動態代碼生成實現,但在AOT環境中無法使用Emit,只能依賴於效率較低的反射API。因此,UnsafeAccessor
的引入,爲我們提供了一種零開銷的私有成員訪問機制。
總的來說,UnsafeAccessor
的引入無疑爲.NET的發展增添了一抹亮色,它不僅提升了代碼的執行效率,也爲我們的編程方式提供了新的可能。我們期待在未來的.NET版本中,看到更多這樣的創新和突破。