Foreword
Continuing from the previous chapter, we discussed the new UnsafeAccessor
in .NET8, and accessed private members through UnsafeAccessor
, which greatly facilitated the writing of our code. Of course, we also talked about some of its current limitations, so what is its performance? Let’s actually test it today.
Test code
Without further ado, let’s get straight to the code. The code for this test is as follows:
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; usingPerfolizer.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 ))); } }
In the test code, we used BenchmarkDotNet
for testing. The test content includes:
-
UnsafeAccessor
: Use theUnsafeAccessor
attribute to access private members -
Reflection
: Use reflection to access private members -
Emit
: UseEmit
+ dynamic method to access private members -
ExpressionTrees
: Use expression trees + delegates to access private members -
Direct
: direct access to private members
The test results are shown in the figure below. It can be seen that the performance of using UnsafeAccessor is the best, followed by direct access to private members, and reflection is the worst. This is actually beyond my expectation, because I thought that its performance is at most similar to that of direct access to private members, but in fact its performance is better than direct access to private members. Of course, it may also be a statistical error, which is on the scale of 0.0000ns. Already very small.
Deep dive
I think everyone has a lot of questions after seeing this. In fact, the author himself also has a lot of questions after seeing this, mainly these two:
-
What made the .NET community want to join this API?
-
How does it access private members?
-
Why is the performance so good?
Reasons for new features
If we want to understand the things behind this function, then we must first find the Issues corresponding to this API. According to the specifications of the .NET community, all APIs need to submit Issues, and then go through API Review and multiple rounds of discussion and design before we start. development.
First, we locate the Issue. In the Issue, we can learn that this API is mainly used by frameworks such as System.Text.Json or EF Core that need to access private members, because currently they are all based on Emit dynamic code generation. Implemented, but Emit cannot be used in AOT. At this stage, only the slow reflection API can be used, so a zero-overhead private member access mechanism is urgently introduced.
https://github.com/dotnet/runtime/issues/86161
How to access private members?
Looking through the discussion of the entire API proposal Issue, we can find the specific implementation of the Issue, so if we want to understand the principles behind it, we need to jump to the corresponding Issue.
Here you can see that there is no generic implementation yet. Non-generic implementations have been implemented in the links below. One is for CoreCLR and the other is for Mono.
We are currently only focusing on CoreCLR, click on this Issue.
https://github.com/dotnet/runtime/issues/86161
You can see that this task is split into several parts, and they are all completed in one PR, including defining the UnsafeAccessor
feature, implementing it in JIT, and supporting it in NativeAOT. In addition, unit tests were written and effective diagnostic solutions were added.
So let’s take a look at what was done in this PR.
https://github.com/dotnet/runtime/pull/86932
Since the PR is very long, if you are interested, you can click in and take a look. Friends with less than 8GB of memory should be careful. To put it simply, this modification mainly consists of two parts. One is JIT-related modifications. JIT mainly supports the usage of UnsafeAccessor
and staticexternint
declaration functions and needs to support methods. The IL Body is empty, and then code is inserted for it according to the characteristics during JIT.
First, let’s look at JIT processing. This code mainly modifies jitinterface.cpp
. You can see that it calls the TryGenerateUnsafeAccessor
method:
This TryGenerateUnsafeAccessor
method is implemented in prestub.cpp
. This prestub.cpp
implements some pre-instrumentation operations, TryGenerateUnsafeAccessor
The code> method implementation is as follows:
It checks different enumerations of UnsafeAccessorKind
to prevent runtime crashes:
Then the GenerateAccessor
method is called to generate IL:
In GenerateAccessor
, Emit is used for code generation:
So from the perspective of JIT implementation, its core principle is Emit code generation, and there is not much special stuff.
In addition, regarding the implementation of NativeAOT, we first modified the NativeAotILProvider.cs
class. The main function of this class is to provide IL for JIT to pre-compile and use when NativeAot
is performed:
The key is also in the GenerateAccessor
method, where the corresponding IL code is generated:
To summarize, the implementation principle of UnsafeAccessor
still uses IL dynamic generation technology, but it is implemented within the JIT.
Why is the performance so good?
So why is its performance better than writing Emit ourselves in C# code? In fact, the reason is also obvious. There is a layer of DynamicMethod
delegate calls in the middle of the Emit code we wrote, which increases the overhead, and UnsafeAccessor
is directly a staticexternintGetValueUnsafe(A a);
method has no intermediate overhead, and its IL Body is small and can be inlined.
Summary
Through in-depth exploration of the new UnsafeAccessor
feature in .NET8, we have gained some enlightenment and understanding. First of all, the introduction of UnsafeAccessor
did not come out of nowhere, but came into being. It is to meet the needs of frameworks such as System.Text.Json or EF Core when accessing private members, because they are currently mostly based on Emit Dynamic code generation is implemented, but Emit cannot be used in an AOT environment and can only rely on the less efficient reflection API. Therefore, the introduction of UnsafeAccessor
provides us with a zero-overhead private member access mechanism.
In general, the introduction of UnsafeAccessor
undoubtedly adds a bright spot to the development of .NET. It not only improves the execution efficiency of the code, but also provides new possibilities for our programming methods. We look forward to seeing more innovations and breakthroughs like this in future .NET versions.
-Technical Group: Add the editor's WeChat and make a note to join the group Editor's WeChat: mm1552923 Public account: dotNet Programming Encyclopedia