C# 高性能獲取對象屬性
目的:嘗試幾種不同的獲取對象屬性的方法,并對比他們的性能
1. 獲取屬性的幾種方法
1.1 準備
定義一個Person類,實例化一個對象,測試獲取對象的屬性。
public class Person
{
/// <summary>
/// 姓名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 英文名
/// </summary>
public string EnglishName { get; set; }
/// <summary>
/// 年齡
/// </summary>
public int Age { get; set; }
}
1.2 直接調用
為了把if判斷,放到for 循環外面,代碼寫的有些啰嗦。因為在千萬級別的循環里面,if判斷也會相對比較耗時,影響對比結果。
/// <summary>
/// 屬性訪問性能測試。
/// </summary>
public static string YuanShengTest(Person person, string fieldName)
{
string result = "";
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
if (fieldName == nameof(person.Name))
{
for (int i = 0; i < Times; i++)
{
string name = person.Name;
if (i == 0)
{
result = name;
}
}
}
else if (fieldName == nameof(person.EnglishName))
{
for (int i = 0; i < Times; i++)
{
string name = person.EnglishName;
if (i == 0)
{
result = name;
}
}
}
else
{
for (int i = 0; i < Times; i++)
{
int age = person.Age;
if (i == 0)
{
result = age.ToString();
}
}
}
stopwatch.Stop();
Console.WriteLine($"{result}==>普通屬性訪問耗時:{stopwatch.ElapsedMilliseconds}ms");
return result;
}
1.3 反射
動態獲取屬性,非常常用的方式,方便,代碼簡潔。但是每次獲取屬性,都調用PropertyInfo.GetValue() ,需要運行時進行完整的方法查找和參數驗證,在這里的幾個方法中,理論上是性能最差的。
/// <summary>
/// 反射性能測試
/// </summary>
public static string ReflectionTest(Person person, string fieldName)
{
string result = "";
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
var personType = typeof(Person); //person.GetType();
var personName = personType.GetProperty(fieldName);
for (int i = 0; i < Times; i++)
{
object? propertyValue = personName?.GetValue(person);
if (i == 0)
{
result = propertyValue.ToString();
}
}
stopwatch.Stop();
Console.WriteLine($"{result}==>反射屬性訪問耗時:{stopwatch.ElapsedMilliseconds}ms");
return result;
}
1.4 動態構建Lambda
動態構建Linq的Lambda表達式,然后編譯以后得到一個委托。首次運行成本高,需要編譯表達式。后續調用,編譯后的委托避免了,反射的運行時開銷,性能接近直接調用。
生成的猥瑣類似下面:
Func<Person, object> getName = p => p.Name;
原理圖:

具體實現:
/// <summary>
/// lambda表達式性能測試
/// </summary>
public static string LambdaTest(Person person, string fieldName)
{
string result = "";
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
Type type = typeof(Person);
//構建表達式:Func<People, object> getName = p => p.Name;
var parameter = Expression.Parameter(type, "p");//參數m
PropertyInfo property = type.GetProperty(fieldName); //要訪問的屬性名
Expression expProperty = Expression.Property(parameter, property.Name); //p.Name
//變成表達式 m => m.Name
var propertyDelegateExpression = Expression.Lambda<Func<Person, object>>(
property.PropertyType.IsValueType ? Expression.Convert(expProperty, typeof(object)) : expProperty, //訪問的屬性如果是值類型,需要顯式轉成object
parameter);
var propertyDelegate = (Func<Person, object>)propertyDelegateExpression.Compile(); //編譯為委托
for (int i = 0; i < Times; i++)
{
object? propertyValue = propertyDelegate.Invoke(person);
if (i == 0)
{
result = propertyValue.ToString();
}
}
stopwatch.Stop();
Console.WriteLine($"{result}==>Lambda屬性訪問耗時:{stopwatch.ElapsedMilliseconds}ms");
return result;
}
```
### 1.5 Emit
Emit 在運行時動態生成IL代碼方法,首次運行以后,后續會很快。跟表達式樹的方式,非常類似,都是生成委托方法,后續不需要運行時查找方法和參數檢查。但是少了額外包裝,更快。缺點也很明顯,寫起來比較復雜。
原理圖:

```
/// <summary>
/// Emit 動態構建方法測試
/// </summary>
/// <param name="person"></param>
/// <param name="fieldName"></param>
/// <returns></returns>
private static string EmitTest(Person person, string fieldName)
{
string result = "";
Type type = typeof(Person);
var property = type.GetProperty(fieldName);
DynamicMethod method = new DynamicMethod("GetPropertyValue", typeof(object), new Type[] { type }, true);
ILGenerator il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0); //將索引0處的參數加載到求值堆棧上
il.Emit(OpCodes.Callvirt, property.GetGetMethod());
if (property.PropertyType.IsValueType)
{
il.Emit(OpCodes.Box, property.PropertyType);//值類型需要裝箱,因為返回類型是object
}
il.Emit(OpCodes.Ret);
Func<Person, object> fun = method.CreateDelegate(typeof(Func<Person, object>)) as Func<Person, object>;
Stopwatch stopwatch = Stopwatch.StartNew();
for (int i = 0; i < Times; i++)
{
object propertyValue = fun.Invoke(person);
if (i == 0)
{
result = propertyValue.ToString();
}
}
stopwatch.Stop();
Console.WriteLine($"{result}==>Emit屬性訪問耗時:{stopwatch.ElapsedMilliseconds}ms");
return result;
}
```
## 2. 耗時對比
用以下完整代碼,循環100000000 ,來一個耗時測試
```
internal class Program
{
public const int Times = 100000000;
static void Main(string[] args)
{
var person = new Person() { Name = "狗蛋", EnglishName = "GouDan", Age = 10 };
string[] fieldNames = new string[] { nameof(Person.Name), nameof(Person.EnglishName), nameof(Person.Age) };
foreach (var item in fieldNames)
{
TestGetFieldValue(person, item);
}
//BenchmarkRunner.Run<PropertyAccessBenchmark>();
Console.ReadLine();
}
/// <summary>
/// 測試獲取屬性值性能
/// </summary>
/// <param name="person"></param>
/// <returns></returns>
private static void TestGetFieldValue(Person person, string fieldName)
{
Console.WriteLine($"開始測試{fieldName}屬性的獲取*************");
YuanShengTest(person, fieldName);
ReflectionTest(person, fieldName);
LambdaTest(person, fieldName);
EmitTest(person, fieldName);
Console.WriteLine($"********************End*******************");
}
/// <summary>
/// 屬性訪問性能測試。
/// </summary>
public static string YuanShengTest(Person person, string fieldName)
{
string result = "";
Stopwatch stopwatch = Stopwatch.StartNew();
if (fieldName == nameof(person.Name))
{
for (int i = 0; i < Times; i++)
{
string name = person.Name;
if (i == 0)
{
result = name;
}
}
}
else if (fieldName == nameof(person.EnglishName))
{
for (int i = 0; i < Times; i++)
{
string name = person.EnglishName;
if (i == 0)
{
result = name;
}
}
}
else
{
for (int i = 0; i < Times; i++)
{
int age = person.Age;
if (i == 0)
{
result = age.ToString();
}
}
}
stopwatch.Stop();
Console.WriteLine($"{result}==>普通屬性訪問耗時:{stopwatch.ElapsedMilliseconds}ms");
return result;
}
/// <summary>
/// 反射性能測試
/// </summary>
public static string ReflectionTest(Person person, string fieldName)
{
string result = "";
var personType = typeof(Person); //person.GetType();
var personName = personType.GetProperty(fieldName);
Stopwatch stopwatch = Stopwatch.StartNew();
for (int i = 0; i < Times; i++)
{
object? propertyValue = personName?.GetValue(person);
if (i == 0)
{
result = propertyValue.ToString();
}
}
stopwatch.Stop();
Console.WriteLine($"{result}==>反射屬性訪問耗時:{stopwatch.ElapsedMilliseconds}ms");
return result;
}
/// <summary>
/// lambda表達式性能測試
/// </summary>
public static string LambdaTest(Person person, string fieldName)
{
string result = "";
Type type = typeof(Person);
//構建表達式:Func<People, object> getName = p => p.Name;
var parameter = Expression.Parameter(type, "p");//參數m
PropertyInfo property = type.GetProperty(fieldName); //要訪問的屬性名
Expression expProperty = Expression.Property(parameter, property.Name); //p.Name
//變成表達式 m => m.Name
var propertyDelegateExpression = Expression.Lambda<Func<Person, object>>(
property.PropertyType.IsValueType ? Expression.Convert(expProperty, typeof(object)) : expProperty, //訪問的屬性如果是值類型,需要顯式轉成object
parameter);
var propertyDelegate = (Func<Person, object>)propertyDelegateExpression.Compile(); //編譯為委托
Stopwatch stopwatch = Stopwatch.StartNew();
for (int i = 0; i < Times; i++)
{
object? propertyValue = propertyDelegate.Invoke(person);
if (i == 0)
{
result = propertyValue.ToString();
}
}
stopwatch.Stop();
Console.WriteLine($"{result}==>Lambda屬性訪問耗時:{stopwatch.ElapsedMilliseconds}ms");
return result;
}
/// <summary>
/// Emit 動態構建方法測試
/// </summary>
/// <param name="person"></param>
/// <param name="fieldName"></param>
/// <returns></returns>
private static string EmitTest(Person person, string fieldName)
{
string result = "";
Type type = typeof(Person);
var property = type.GetProperty(fieldName);
DynamicMethod method = new DynamicMethod("GetPropertyValue", typeof(object), new Type[] { type }, true);
ILGenerator il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0); //將索引0處的參數加載到求值堆棧上
il.Emit(OpCodes.Callvirt, property.GetGetMethod());
if (property.PropertyType.IsValueType)
{
il.Emit(OpCodes.Box, property.PropertyType);//值類型需要裝箱,因為返回類型是object
}
il.Emit(OpCodes.Ret);
Func<Person, object> fun = method.CreateDelegate(typeof(Func<Person, object>)) as Func<Person, object>;
Stopwatch stopwatch = Stopwatch.StartNew();
for (int i = 0; i < Times; i++)
{
object propertyValue = fun.Invoke(person);
if (i == 0)
{
result = propertyValue.ToString();
}
}
stopwatch.Stop();
Console.WriteLine($"{result}==>Emit屬性訪問耗時:{stopwatch.ElapsedMilliseconds}ms");
return result;
}
}
```
測試結果:

**獲取第一個屬性`Name`的時候,最快的居然不是直接獲取**,沒想明白。需要預熱?知道的大哥告訴一下。既然如此,再來一個基準測試看看結果。
## 3. 基準測試
### 3.1 安裝Nuget包
```
NuGet\InstallPackage BenchmarkDotNet Version 0.15.0
```
### 3.2 編寫`PropertyAccessBenchmark`類
```
using BenchmarkDotNet.Attributes;
using fanshe.Model;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
public class PropertyAccessBenchmark
{
private Person person;
private PropertyInfo property;
private Func<Person, object> lambdaDelegate;
private Func<Person, object> emitDelegate;
[Params("Name", "EnglishName", "Age")]
public string FieldName { get; set; }
[GlobalSetup]
public void Setup()
{
person = new Person { Name = "狗蛋", EnglishName = "GouDdan", Age = 10 };
//反射
var type = typeof(Person);
property = type.GetProperty(FieldName); //要訪問的屬性名
//構建表達式:Func<People, object> getName = p => p.Name;
//參數p
var parameter = Expression.Parameter(type, "p");
//要訪問的屬性p.Name
Expression expProperty = Expression.Property(parameter, property.Name);
//變成表達式 m => m.Name
var propertyDelegateExpression = Expression.Lambda<Func<Person, object>>(
property.PropertyType.IsValueType ? Expression.Convert(expProperty, typeof(object)) : expProperty, //訪問的屬性如果是值類型,需要顯式轉成object
parameter);
lambdaDelegate = (Func<Person, object>)propertyDelegateExpression.Compile(); //編譯為委托
// Emit
DynamicMethod method = new DynamicMethod("GetPropertyValue", typeof(object), new Type[] { type }, true);
ILGenerator il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Callvirt, property.GetGetMethod());
if (property.PropertyType.IsValueType)
{
il.Emit(OpCodes.Box, property.PropertyType);
}
il.Emit(OpCodes.Ret);
emitDelegate = (Func<Person, object>)method.CreateDelegate(typeof(Func<Person, object>));
}
[Benchmark]
public object YuanSheng()
{
return person.EnglishName;
//return FieldName switch
//{
// "Name" => person.Name,
// "EnglishName" => person.EnglishName,
// "Age" => person.Age,
// _ => null
//};
}
[Benchmark]
public object Reflection()
{
return property.GetValue(person);
}
[Benchmark]
public object Lambda()
{
return lambdaDelegate(person);
}
[Benchmark]
public object Emit()
{
return emitDelegate(person);
}
}
```
### 3.3 調用`PropertyAccessBenchmark`
```
BenchmarkRunner.Run<PropertyAccessBenchmark>();
```
### 3.3 Release生成,運行測試

結果:**直接訪問(YuanSheng) > 表達式樹(Lambda) ≈ Emit > 反射(Reflection)**
Emit 略微慢于表達式樹(Lambda),這是沒想到的。應該是Emit代碼,什么地方沒有優化好。Emit 不怎么會,歡迎大佬指導!

浙公網安備 33010602011771號