表达式树练习:遍历对象属性

前言

前几天lwx在群里吐槽反射遍历对象属性/字段的性能问题,就想到了之前写戴森球MOD的时候了解到的表达式树,写点东西练练手。

MS的文档:表达式树 (C#)

以下只是本人的理解,如有错误,欢迎留言指正。

先看看效果

表达式树+缓存delegate反射+缓存PropertyInfo的性能测试:

1
2
3
4
5
6
7
BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19045.2311)
Intel Core i5-9300H CPU 2.40GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=7.0.100
[Host] : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2

Job=MediumRun Toolchain=InProcessNoEmitToolchain IterationCount=15
LaunchCount=2 WarmupCount=10
MethodMeanErrorStdDevGen0Allocated
GetPropertyValue0.9550 ns0.1874 ns0.2627 ns--
SetPropertyValue1.1957 ns0.2026 ns0.3033 ns--
GetPropertyValueReflection20.7207 ns1.0893 ns1.6304 ns0.005724 B
SetPropertyValueReflection41.7003 ns1.4820 ns2.2182 ns0.005724 B

实现

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static Func<T, TU> GetPropertyInternal<T, TU>(string propertyName)
{
ParameterExpression parameter = Expression.Parameter(typeof(T), "test");
UnaryExpression property = Expression.Convert(Expression.Property(parameter, propertyName), typeof(TU));
Expression<Func<T, TU>> expression = Expression.Lambda<Func<T, TU>>(property, parameter);
// Console.WriteLine(expression);
return expression.Compile();
}

public static Action<T, TU> SetPropertyInternal<T, TU>(string propertyName)
{
ParameterExpression parameter = Expression.Parameter(typeof(T), "obj");
MemberExpression property = Expression.Property(parameter, propertyName);
ParameterExpression parameter2 = Expression.Parameter(typeof(TU), "value");
BinaryExpression assign = Expression.Assign(property, Expression.Convert(parameter2, property.Type));
Expression<Action<T, TU>> expression = Expression.Lambda<Action<T, TU>>(assign, parameter, parameter2);
// Console.WriteLine(expression);
return expression.Compile();
}

翻译一下

1
2
GetPropertyInternal:  test => (TU)test.propertyName
SetPropertyInternal: (obj, value) => obj.property = (property的类型)value

看看内部实现

Compile():

1
2
3
4
public TDelegate Compile()
=> LambdaExpression.CanCompileToIL
? (TDelegate)LambdaCompiler.Compile((LambdaExpression)this)
: (TDelegate)new LightCompiler().CompileTop((LambdaExpression)this).CreateDelegate();

LambdaCompiler.Compile():

1
2
3
4
5
6
7
internal static Delegate Compile(LambdaExpression lambda)
{
lambda.ValidateArgumentCount();
LambdaCompiler lambdaCompiler = new LambdaCompiler(LambdaCompiler.AnalyzeLambda(ref lambda), lambda);
lambdaCompiler.EmitLambdaBody();
return lambdaCompiler.CreateDelegate();
}

.ctor():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private LambdaCompiler(AnalyzedTree tree, LambdaExpression lambda)
{
Type[] parameterTypes = LambdaCompiler.GetParameterTypes(lambda, typeof (Closure));
int num = Interlocked.Increment(ref LambdaCompiler.s_lambdaMethodIndex);
DynamicMethod dynamicMethod =
new DynamicMethod(lambda.Name ?? "lambda_method" + num.ToString(), lambda.ReturnType, parameterTypes, true);
this._tree = tree;
this._lambda = lambda;
this._method = (MethodInfo) dynamicMethod;
this._ilg = dynamicMethod.GetILGenerator();
this._hasClosureArgument = true;
this._scope = tree.Scopes[(object) lambda];
this._boundConstants = tree.Constants[lambda];
this.InitializeMethod();
}

EmitLambdaBody():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void EmitLambdaBody() => 
this.EmitLambdaBody((CompilerScope) null, false, this._lambda.TailCall
? LambdaCompiler.CompilationFlags.EmitAsTail
: LambdaCompiler.CompilationFlags.EmitAsNoTail);

private void EmitLambdaBody(
CompilerScope parent,
bool inlined,
LambdaCompiler.CompilationFlags flags)
{
this._scope.Enter(this, parent);
if (inlined)
{
for (int index = this._lambda.ParameterCount - 1; index >= 0; --index)
this._scope.EmitSet(this._lambda.GetParameter(index));
}
flags = LambdaCompiler.UpdateEmitExpressionStartFlag(flags, LambdaCompiler.CompilationFlags.EmitExpressionStart);
if (this._lambda.ReturnType == typeof (void))
this.EmitExpressionAsVoid(this._lambda.Body, flags);
else
this.EmitExpression(this._lambda.Body, flags);
if (!inlined)
this._ilg.Emit(OpCodes.Ret);
this._scope.Exit();
foreach (LabelInfo labelInfo in this._labelInfo.Values)
labelInfo.ValidateFinish();
}

CreateDelegate:

1
2
3
4
private Delegate CreateDelegate() => 
this._method.CreateDelegate(this._lambda.Type,
(object) new Closure(this._boundConstants.ToArray(),
(object[]) null));

大概流程:创建DynamicMethod实例、使用表达式树生成IL代码、创建委托并返回。

手搓DynamicMethod

知道了原理,我们可以自己搓一个DynamicMethod。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static readonly BindingFlags Flag = 
BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static;

public static Func<T, TU> GetPropertyInternalDynamic<T, TU>(string propertyName)
{
DynamicMethod dynamicMethod =
new DynamicMethod("MyGetPropertyDynamicMethod", typeof(TU), new[] { typeof(T) }, typeof(T), true);
ILGenerator ilGenerator = dynamicMethod.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Callvirt, typeof(T).GetProperty(propertyName, Flag)!.GetGetMethod(true)!);
ilGenerator.Emit(OpCodes.Ret);
return (Func<T, TU>)dynamicMethod.CreateDelegate(typeof(Func<T, TU>));
}

public static Action<T, TU> SetPropertyInternalDynamic<T, TU>(string propertyName)
{
DynamicMethod dynamicMethod = new DynamicMethod("MySetPropertyDynamicMethod",
typeof(void), new[] { typeof(T), typeof(TU) }, typeof(T), true);
ILGenerator ilGenerator = dynamicMethod.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Callvirt, typeof(T).GetProperty(propertyName, Flag)!.GetSetMethod(true)!);
ilGenerator.Emit(OpCodes.Ret);
return (Action<T, TU>)dynamicMethod.CreateDelegate(typeof(Action<T, TU>));
}

Field版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static Func<T, TU> GetFieldInternalDynamic<T, TU>(string fieldName)
{
DynamicMethod dynamicMethod =
new DynamicMethod("MyGetFieldDynamicMethod", typeof(TU), new[] { typeof(T) }, typeof(T), true);
ILGenerator ilGenerator = dynamicMethod.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldfld, typeof(T).GetField(fieldName, Flag)!);
ilGenerator.Emit(OpCodes.Ret);
return (Func<T, TU>)dynamicMethod.CreateDelegate(typeof(Func<T, TU>));
}

public static Action<T, TU> SetFieldInternalDynamic<T, TU>(string fieldName)
{
DynamicMethod dynamicMethod =
new DynamicMethod("MySetFieldDynamicMethod", typeof(void), new[] { typeof(T), typeof(TU) }, typeof(T), true);
ILGenerator ilGenerator = dynamicMethod.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Stfld, typeof(T).GetField(fieldName, Flag)!);
ilGenerator.Emit(OpCodes.Ret);
return (Action<T, TU>)dynamicMethod.CreateDelegate(typeof(Action<T, TU>));
}
MethodMeanErrorStdDevGen0Allocated
GetPropertyValue1.114 ns0.2108 ns0.3156 ns--
SetPropertyValue1.520 ns0.1166 ns0.1745 ns--
GetPropertyValueDynamic2.349 ns0.2195 ns0.3218 ns--
SetPropertyValueDynamic2.257 ns0.1629 ns0.2336 ns--

代码量、可读性和性能都比表达式树差一些。

创建 GetGetMethod 和 GetSetMethod 委托

这部分是在写DynamicMethod的时候才想到的。

1
2
3
4
5
6
7
8
9
10
11
public static readonly Func<Test, EnumTest> GetMethod 
= Property.GetGetMethod(true)!.CreateDelegate<Func<Test, EnumTest>>();

public static readonly Action<Test, EnumTest> SetMethod
= Property.GetSetMethod(true)!.CreateDelegate<Action<Test, EnumTest>>();

[Benchmark]
public EnumTest GetPropertyValueDelegate() => GetMethod(Test);

[Benchmark]
public void SetPropertyValueDelegate() => SetMethod(Test, EnumTest.A);
MethodMeanErrorStdDevMedianGen0Allocated
GetPropertyValueDynamic1.4607 ns0.1061 ns0.1588 ns1.3569 ns--
SetPropertyValueDynamic1.9177 ns0.0622 ns0.0931 ns1.9246 ns--
GetPropertyValueDelegate1.6995 ns0.1242 ns0.1859 ns1.6376 ns--
SetPropertyValueDelegate1.9390 ns0.1100 ns0.1646 ns1.9166 ns--

性能和DynamicMethod差不多,但是代码量少了很多。

总结

废话

表达式树和DynamicMethod都可以用来生成IL代码,但表达式树的可读性和性能都优于DynamicMethod。

修改原有基于反射的代码时,可以优先考虑使用GetGetMethod委托,修改代码量最少,效率也不错。

性能测试

完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Toolchains.InProcess.NoEmit;

// ReSharper disable MemberCanBeInternal, UnusedMember.Local, MemberCanBePrivate.Global
// ReSharper disable PropertyCanBeMadeInitOnly.Global, UnusedAutoPropertyAccessor.Global

#pragma warning disable CS8618

public static partial class Program
{
public static void Main(string[] args)
{
BenchmarkRunner.Run(typeof(BenchmarkTest));
}
}

public static partial class Program
{
public static Action<T, TU> SetPropertyInternal<T, TU>(string propertyName)
{
ParameterExpression parameter = Expression.Parameter(typeof(T), "obj");
MemberExpression property = Expression.Property(parameter, propertyName);
ParameterExpression parameter2 = Expression.Parameter(typeof(TU), "value");
BinaryExpression assign = Expression.Assign(property, Expression.Convert(parameter2, property.Type));
Expression<Action<T, TU>> expression = Expression.Lambda<Action<T, TU>>(assign, parameter, parameter2);
//Console.WriteLine(expression);
return expression.Compile();
}

public static Func<T, TU> GetPropertyInternal<T, TU>(string propertyName)
{
ParameterExpression parameter = Expression.Parameter(typeof(T), "test");
UnaryExpression property = Expression.Convert(Expression.Property(parameter, propertyName), typeof(TU));
Expression<Func<T, TU>> expression = Expression.Lambda<Func<T, TU>>(property, parameter);
//Console.WriteLine(expression);
return expression.Compile();
}

public static readonly BindingFlags Flag
= BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static;

public static Func<T, TU> GetPropertyInternalDynamic<T, TU>(string propertyName)
{
DynamicMethod dynamicMethod =
new DynamicMethod("MyGetPropertyDynamicMethod", typeof(TU), new[] { typeof(T) }, typeof(T), true);
ILGenerator ilGenerator = dynamicMethod.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Callvirt, typeof(T).GetProperty(propertyName, Flag)!.GetGetMethod(true)!);
ilGenerator.Emit(OpCodes.Ret);
return (Func<T, TU>)dynamicMethod.CreateDelegate(typeof(Func<T, TU>));
}

public static Action<T, TU> SetPropertyInternalDynamic<T, TU>(string propertyName)
{
DynamicMethod dynamicMethod =
new DynamicMethod("MySetPropertyDynamicMethod", typeof(void), new[] { typeof(T), typeof(TU) }, typeof(T), true);
ILGenerator ilGenerator = dynamicMethod.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Callvirt, typeof(T).GetProperty(propertyName, Flag)!.GetSetMethod(true)!);
ilGenerator.Emit(OpCodes.Ret);
return (Action<T, TU>)dynamicMethod.CreateDelegate(typeof(Action<T, TU>));
}

public static Func<T, TU> GetFieldInternalDynamic<T, TU>(string fieldName)
{
DynamicMethod dynamicMethod =
new DynamicMethod("MyGetFieldDynamicMethod", typeof(TU), new[] { typeof(T) }, typeof(T), true);
ILGenerator ilGenerator = dynamicMethod.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldfld, typeof(T).GetField(fieldName, Flag)!);
ilGenerator.Emit(OpCodes.Ret);
return (Func<T, TU>)dynamicMethod.CreateDelegate(typeof(Func<T, TU>));
}

public static Action<T, TU> SetFieldInternalDynamic<T, TU>(string fieldName)
{
DynamicMethod dynamicMethod =
new DynamicMethod("MySetFieldDynamicMethod", typeof(void), new[] { typeof(T), typeof(TU) }, typeof(T), true);
ILGenerator ilGenerator = dynamicMethod.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Stfld, typeof(T).GetField(fieldName, Flag)!);
ilGenerator.Emit(OpCodes.Ret);
return (Action<T, TU>)dynamicMethod.CreateDelegate(typeof(Action<T, TU>));
}
}

[Config(typeof(AntiVirusFriendlyConfig))]
[MemoryDiagnoser]
public class BenchmarkTest
{
private static readonly Test Test = new() { T = "S", X = EnumTest.B };

public static readonly Func<Test, EnumTest> GetPropertyInternal
= Program.GetPropertyInternal<Test, EnumTest>("X");

public static readonly Action<Test, EnumTest> SetPropertyInternal
= Program.SetPropertyInternal<Test, EnumTest>("X");

public static readonly Func<Test, EnumTest> GetPropertyInternalDynamic
= Program.GetPropertyInternalDynamic<Test, EnumTest>("X");

public static readonly Action<Test, EnumTest> SetPropertyInternalDynamic
= Program.SetPropertyInternalDynamic<Test, EnumTest>("X");

public static readonly BindingFlags Flag
= BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static;

private static readonly PropertyInfo Property = typeof(Test).GetProperty("X", Flag)!;

public static readonly Func<Test, EnumTest> GetMethod
= Property.GetGetMethod(true)!.CreateDelegate<Func<Test, EnumTest>>();

public static readonly Action<Test, EnumTest> SetMethod
= Property.GetSetMethod(true)!.CreateDelegate<Action<Test, EnumTest>>();

[Benchmark]
public EnumTest GetPropertyValue() => GetPropertyInternal(Test);

[Benchmark]
public void SetPropertyValue() => SetPropertyInternal(Test, EnumTest.A);

[Benchmark]
public EnumTest GetPropertyValueDynamic() => GetPropertyInternalDynamic(Test);

[Benchmark]
public void SetPropertyValueDynamic() => SetPropertyInternalDynamic(Test, EnumTest.A);

[Benchmark]
public EnumTest GetPropertyValueDelegate() => GetMethod(Test);

[Benchmark]
public void SetPropertyValueDelegate() => SetMethod(Test, EnumTest.A);

[Benchmark]
public EnumTest GetPropertyValueReflection() => (EnumTest)Property.GetValue(Test)!;

[Benchmark]
public void SetPropertyValueReflection() => Property.SetValue(Test, EnumTest.A);
}

public class Test
{
internal string T { get; set; }

internal EnumTest X { get; set; }

private string Y { get; set; }
}

public enum EnumTest : long
{
A = 1,
B = 2,
C = 3
}

public class AntiVirusFriendlyConfig : ManualConfig
{
public AntiVirusFriendlyConfig()
{
AddJob(Job.MediumRun.WithToolchain(InProcessNoEmitToolchain.Instance));
}
}
MethodMeanErrorStdDevMedianGen0Allocated
GetPropertyValue0.4234 ns0.2270 ns0.3398 ns0.3894 ns--
SetPropertyValue0.2943 ns0.1207 ns0.1807 ns0.2732 ns--
GetPropertyValueDynamic1.4607 ns0.1061 ns0.1588 ns1.3569 ns--
SetPropertyValueDynamic1.9177 ns0.0622 ns0.0931 ns1.9246 ns--
GetPropertyValueDelegate1.6995 ns0.1242 ns0.1859 ns1.6376 ns--
SetPropertyValueDelegate1.9390 ns0.1100 ns0.1646 ns1.9166 ns--
GetPropertyValueReflection19.4623 ns0.9508 ns1.4230 ns19.5583 ns0.005724 B
SetPropertyValueReflection37.7056 ns1.6422 ns2.4580 ns36.9589 ns0.005724 B

言归正传

一个简单的对象属性比较器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class ObjectComparer<T>
{
#region Privite Fields

private Dictionary<Type, Func<T, T, bool>> CustomTypeComparers { get; } = new();

private Dictionary<string, Func<T, T, bool>> DefaultComparers { get; } = new();

private readonly PropertyInfo[] _propertyInfos;

private static Func<T, T, bool> GetDefaultComparer(PropertyInfo propertyInfo)
{
var parameter = Expression.Parameter(typeof(T), "test");
var property = Expression.Property(parameter, propertyInfo);
var parameter2 = Expression.Parameter(typeof(T), "test2");
var property2 = Expression.Property(parameter2, propertyInfo);
Expression body = Expression.Equal(property, property2);
Expression<Func<T, T, bool>> expression = Expression.Lambda<Func<T, T, bool>>(body, parameter, parameter2);
return expression.Compile();
}

#endregion

public BindingFlags Flag { get; }

#region .ctor

public ObjectComparer(BindingFlags propertiesBindingFlags)
{
Flag = propertiesBindingFlags;
_propertyInfos = typeof(T).GetProperties(Flag);
foreach (var property in _propertyInfos) DefaultComparers[property.Name] = GetDefaultComparer(property);
}

public ObjectComparer()
: this(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static)
{

}

#endregion

public void AddCustomTypeComparer<TU>(Expression<Func<TU, TU, bool>> comparer)
{
var parameter = Expression.Parameter(typeof(T), "test");
var parameter2 = Expression.Parameter(typeof(T), "test2");
var body = comparer.Body;
Expression<Func<T, T, bool>> expression = Expression.Lambda<Func<T, T, bool>>(body, parameter, parameter2);
CustomTypeComparers[typeof(TU)] = expression.Compile();
}

public bool Compare(T obj1, T obj2)
{
foreach (var property in _propertyInfos)
{
if (CustomTypeComparers.TryGetValue(property.PropertyType, out Func<T, T, bool>? comparer))
{
if (!comparer(obj1, obj2)) return false;
}
else
{
if (!DefaultComparers[property.Name](obj1, obj2)) return false;
}
}

return true;
}
}

(不知道有啥用,但是挺好玩的

说起BindingFlags,之前被它坑过好几次(因为类库默认使用了public instance property,而我使用了internal instance property)

我自己写东西的时候习惯用 BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static,这样就不会有什么问题了。