原文地址:https://segmentfault.com/a/11...
本篇内容主要讲如何优化获取(设置)对象字段(属性)的反射性能。
先上结论:
1.反射性能很差,比直接调用慢成百上千倍。
2.反射性能优化的思路是绕开反射。
3.反射性能优化方式有:字典缓存(Dictionary Cache)、委托(Delegate.CreateDelegate)、Expression.Lamda、IL.Emit、指针(IntPtr) 等各种方式。
4.性能排名:直接调用 > (IL.Emit == Expression) > 委托 > 指针 > 反射
5.通用性排名:反射 > (委托 == 指针) > (IL.Emit == Expression) > 直接调用
6.综合排名:(委托 == 指针) > (IL.Emit == Expression) > 反射 > 直接调用
IL.Emit本是性能最优方案,但可惜IL2CPP模式不支持JIT 所有动态生成代码的功能都不好用 故IL.Emit无法使用、Expression.Lamda也是同样的原因不可用。
退而求其次选用性能和通用性都不错的委托,由于委托只能优化方法无法用于字段,所以又加上了指针用于操作字段。
性能测试:
以设置对象的属性Name为测试条件(obj.Name = "A")
SetValue方法都调整为通用的SetValue(object target, object value)状态。
除了反射,所有调用方法都创建委托后再调用,省略查找委托缓存的步骤。
在Release模式下,循环多次调用,查看耗时。
如果直接调用耗时约为:1ms,其他方式耗时约为:
直接调用 | IL.Emit | Expression | 委托(Delegate) | 反射(MethodInfo) |
---|---|---|---|---|
1ms | 1.3ms | 1.3ms | 2.1ms | 91.7ms |
指针只操作字段, 不参与该项测试
如果测试条件改为每次获取或设置值都需要通过Dictionary从缓存中查找字段所对应的委托方法后调用,那么时间将大大增加,大部分时间都浪费在了字典查找上,但这也更贴近我们正常使用的情况。
直接调用= | IL.Emit | Expression | 委托(Delegate) | 反射(MethodInfo) |
---|---|---|---|---|
1ms | 8.8ms | 8.8ms | 9.7ms | 192.5ms |
设置string字段测试:
直接调用= | IL.Emit | Expression | 指针 | 反射(MethodInfo) |
---|---|---|---|---|
1ms | 8ms | 8ms | 9ms | 122ms |
委托只操作属性, 不参与该项测试
从测试结果可以看出
直接调用 > IL.Emit(Expression与IL一致) > 委托(指针与委托互补)> 反射
基本上只要能绕开反射,性能都会有明显的提升。
IL.Emit
优点:性能非常高。
缺点:不支持IL2CPP。
IL.Emit通用工具类:
用法:
var getName = ILUtil.CreateGetValue(type, "Name");
var setName = ILUtil.CreateSetValue(type, "Name");
setName(obj, "A");
string name = (string)getName(obj);
代码:
#region Intro
// Purpose: Reflection Optimize Performance
// Author: ZhangYu
// LastModifiedDate: 2022-06-11
#endregion
using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Collections.Generic;
/// IL.Emit工具
public static class ILUtil {
private static Type[] NoTypes = new Type[] { };
/// IL动态添加创建新对象方法
public static Func
Expression.Lamda
优点:性能非常高。
缺点:不支持IL2CPP。
和IL.Emit一样。
Expression通用工具类:
用法:
var wrapper = ExpressionUtil.GetPropertyWrapper(type, "Name");
wrapper.SetValue(obj, "A");
string name = (string)wrapper.GetValue(obj);
代码:
using System;
using System.Reflection;
using System.Linq.Expressions;
using System.Collections.Generic;
///
/// Expression.Lamda 反射加速工具
/// ZhangYu 2022-06-11
///
public static class ExpressionUtil {
public class PropertyWrapper {
public Func GetValue { get; private set; }
public Action SetValue { get; private set; }
public PropertyWrapper(Func getValue, Action setValue) {
GetValue = getValue;
SetValue = setValue;
}
}
public static Func CreateGetProperty(Type type, string propertyName) {
var objectObj = Expression.Parameter(typeof(object), "obj");
var classObj = Expression.Convert(objectObj, type);
var classFunc = Expression.Property(classObj, propertyName);
var objectFunc = Expression.Convert(classFunc, typeof(object));
Func getValue = Expression.Lambda>(objectFunc, objectObj).Compile();
return getValue;
}
public static Action CreateSetProperty(Type type, string propertyName) {
var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
var objectObj = Expression.Parameter(typeof(object), "obj");
var objectValue = Expression.Parameter(typeof(object), "value");
var classObj = Expression.Convert(objectObj, type);
var classValue = Expression.Convert(objectValue, property.PropertyType);
var classFunc = Expression.Call(classObj, property.GetSetMethod(), classValue);
var setProperty = Expression.Lambda>(classFunc, objectObj, objectValue).Compile();
return setProperty;
}
private static Dictionary> cache = new Dictionary>();
public static PropertyWrapper GetPropertyWrapper(Type type, string propertyName) {
// 查找一级缓存
Dictionary wrapperDic = null;
if (!cache.TryGetValue(type, out wrapperDic)) {
wrapperDic = new Dictionary();
cache.Add(type, wrapperDic);
}
// 查找二级缓存
PropertyWrapper wrapper = null;
if (!wrapperDic.TryGetValue(propertyName, out wrapper)) {
var getValue = CreateGetProperty(type, propertyName);
var setValue = CreateSetProperty(type, propertyName);
wrapper = new PropertyWrapper(getValue, setValue);
wrapperDic.Add(propertyName, wrapper);
}
return wrapper;
}
public static void ClearCache() {
cache.Clear();
}
}
委托(Delegate)
优点:性能高,通用性强(IL2CPP下也可使用)。
缺点:只能操作方法,无法直接操作字段
委托通用工具类:
用法:
var wrapper = DelegateUtil.GetPropertyWrapper(type, "Name");
wrapper.SetValue(obj, "A");
string name = (string)wrapper.GetValue(obj);
代码:
using System;
using System.Reflection;
using System.Collections.Generic;
public interface IPropertyWrapper {
object GetValue(object obj);
void SetValue(object obj, object value);
}
public class PropertyWrapper : IPropertyWrapper {
private Func getter;
private Action setter;
public PropertyWrapper(PropertyInfo property) {
getter = Delegate.CreateDelegate(typeof(Func), property.GetGetMethod()) as Func;
setter = Delegate.CreateDelegate(typeof(Action), property.GetSetMethod()) as Action;
}
public V GetValue(T obj) {
return getter(obj);
}
public void SetValue(T obj, V value) {
setter(obj, value);
}
public object GetValue(object obj) {
return GetValue((T)obj);
}
public void SetValue(object obj, object value) {
SetValue((T)obj, (V)value);
}
}
/// 委托工具类 ZhangYu 2022-06-10
public static class DelegateUtil {
private static Dictionary> cache = new Dictionary>();
/// 获取属性包装器
public static IPropertyWrapper GetPropertyWrapper(Type type, string propertyName) {
if (string.IsNullOrEmpty(propertyName)) throw new Exception("propertyName is null");
// 查找类型
Dictionary proCache = null;
if (!cache.TryGetValue(type, out proCache)) {
proCache = new Dictionary();
cache.Add(type, proCache);
}
// 查找属性
IPropertyWrapper wrapper = null;
if (!proCache.TryGetValue(propertyName, out wrapper)) {
PropertyInfo property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
if (property == null) throw new Exception(type.Name + " type no property:" + propertyName);
Type wrapperType = typeof(PropertyWrapper<,>).MakeGenericType(type, property.PropertyType);
wrapper = Activator.CreateInstance(wrapperType, property) as IPropertyWrapper;
proCache.Add(propertyName, wrapper);
}
return wrapper;
}
/// 清理缓存
public static void ClearCache() {
cache.Clear();
}
}
指针
优点:性能高,通用性好(IL2CPP下也可使用)。
缺点:暂时不知道怎样用指针操作属性(方法)。
指针通用工具类:
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Runtime.InteropServices;
#if !ENABLE_MONO
using Unity.Collections.LowLevel.Unsafe;
#endif
/// 指针工具 ZhangYu 2022-06-10
public static class PointerUtil {
public class FieldWrapper {
public string fieldName;
public Type fieldType;
public bool isValueType;
public TypeCode typeCode;
public int offset;
private static bool is64Bit = Environment.Is64BitProcess;
#region Instance Function
/// 获取字段值
public unsafe object GetValue(object obj) {
// 检查参数
CheckObjAndFieldName(obj, fieldName);
// 查找对象信息
Type type = obj.GetType();
// 获取值
IntPtr ptr = GetPointer(obj) + offset;
object value = null;
switch (typeCode) {
case TypeCode.Boolean:
value = *(bool*)ptr;
break;
case TypeCode.Char:
value = *(char*)ptr;
break;
case TypeCode.SByte:
value = *(sbyte*)ptr;
break;
case TypeCode.Byte:
value = *(byte*)ptr;
break;
case TypeCode.Int16:
value = *(short*)ptr;
break;
case TypeCode.UInt16:
value = *(ushort*)ptr;
break;
case TypeCode.Int32:
value = *(int*)ptr;
break;
case TypeCode.UInt32:
value = *(uint*)ptr;
break;
case TypeCode.Int64:
value = *(long*)ptr;
break;
case TypeCode.UInt64:
value = *(ulong*)ptr;
break;
case TypeCode.Single:
value = *(float*)ptr;
break;
case TypeCode.Double:
value = *(double*)ptr;
break;
case TypeCode.Decimal:
value = *(decimal*)ptr;
break;
case TypeCode.DateTime:
value = *(DateTime*)ptr;
break;
case TypeCode.DBNull:
case TypeCode.String:
case TypeCode.Object:
if (isValueType) {
value = GetStruct(ptr, fieldType);
} else {
value = GetObject(ptr);
}
break;
default:
break;
}
return value;
}
/// 给字段赋值
public unsafe void SetValue(object obj, object value) {
// 检查参数
CheckObjAndFieldName(obj, fieldName);
// 查找对象信息
Type type = obj.GetType();
// 获取值
IntPtr ptr = GetPointer(obj) + offset;
switch (typeCode) {
case TypeCode.Boolean:
*(bool*)ptr = (bool)value;
break;
case TypeCode.Char:
*(char*)ptr = (char)value;
break;
case TypeCode.SByte:
*(sbyte*)ptr = (sbyte)value;
break;
case TypeCode.Byte:
*(byte*)ptr = (byte)value;
break;
case TypeCode.Int16:
*(short*)ptr = (short)value;
break;
case TypeCode.UInt16:
*(ushort*)ptr = (ushort)value;
break;
case TypeCode.Int32:
*(int*)ptr = (int)value;
break;
case TypeCode.UInt32:
*(uint*)ptr = (uint)value;
break;
case TypeCode.Int64:
*(long*)ptr = (long)value;
break;
case TypeCode.UInt64:
*(ulong*)ptr = (ulong)value;
break;
case TypeCode.Single:
*(float*)ptr = (float)value;
break;
case TypeCode.Double:
*(double*)ptr = (double)value;
break;
case TypeCode.Decimal:
*(decimal*)ptr = (decimal)value;
break;
case TypeCode.DateTime:
*(DateTime*)ptr = (DateTime)value;
break;
case TypeCode.DBNull:
case TypeCode.String:
case TypeCode.Object:
if (isValueType) {
SetStruct(ptr, value);
} else {
SetObject(ptr, value);
}
break;
default:
break;
}
}
#endregion
#region Static Fucntion
/// 获取对象地址
private unsafe static IntPtr GetPointer(object obj) {
#if ENABLE_MONO
TypedReference tr = __makeref(obj);
return *(IntPtr*)*((IntPtr*)&tr + 1);
#else
ulong gcHandle;
IntPtr ptr = (IntPtr)UnsafeUtility.PinGCObjectAndGetAddress(obj, out gcHandle);
UnsafeUtility.ReleaseGCObject(gcHandle);
return ptr;
#endif
}
/// 将对象的地址设置到目标地址,有类型判定和引用计数,推荐在堆上操作
private unsafe static void SetObject(IntPtr ptr, object value) {
#if ENABLE_MONO
object tmp = "";
TypedReference tr = __makeref(tmp);
if (is64Bit) {
long* p = (long*)&tr + 1;
*p = (long)ptr;
__refvalue(tr, object) = value;
} else {
int* p = (int*)&tr + 1;
*p = (int)ptr;
__refvalue(tr, object) = value;
}
#else
UnsafeUtility.CopyObjectAddressToPtr(value, (void*)ptr);
#endif
}
/// 设置Struct
private static void SetStruct(IntPtr ptr, object value) {
Marshal.StructureToPtr(value, ptr, true);
}
/// 获取对象
private unsafe static object GetObject(IntPtr ptr) {
#if ENABLE_MONO
object tmp = "";
TypedReference tr = __makeref(tmp);
if (is64Bit) {
long* p = (long*)&tr + 1;
*p = (long)ptr;
return __refvalue(tr, object);
} else {
int* p = (int*)&tr + 1;
*p = (int)ptr;
return __refvalue(tr, object);
}
#else
return UnsafeUtility.ReadArrayElement((void*)ptr, 0);
#endif
}
/// 获取Struct
private static object GetStruct(IntPtr ptr, Type type) {
return Marshal.PtrToStructure(ptr, type);
}
/// 检查参数
private static void CheckObjAndFieldName(object obj, string fieldName) {
if (obj == null) throw new Exception("obj can't be null");
if (string.IsNullOrEmpty(fieldName)) throw new Exception("FieldName can't be null or empty");
}
/// 获取字段地址偏移量
public unsafe static int GetFieldOffset(FieldInfo field) {
return *(short*)(field.FieldHandle.Value + 24);
}
#endregion
}
/// 缓存
private static Dictionary> cache = new Dictionary>();
public static FieldWrapper GetFieldWrapper(Type type, string fieldName) {
// 查找一级缓存
Dictionary wrapperDic = null;
if (!cache.TryGetValue(type, out wrapperDic)) {
wrapperDic = new Dictionary();
cache.Add(type, wrapperDic);
}
// 查找二级缓存
FieldWrapper wrapper = null;
if (!wrapperDic.TryGetValue(fieldName, out wrapper)) {
FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance);
if (field == null) throw new Exception(type.Name + " field is null:" + fieldName);
wrapper = new FieldWrapper();
wrapper.fieldName = fieldName;
wrapper.fieldType = field.FieldType;
wrapper.isValueType = field.FieldType.IsValueType;
wrapper.typeCode = Type.GetTypeCode(field.FieldType);
wrapper.offset = FieldWrapper.GetFieldOffset(field);
wrapperDic.Add(fieldName, wrapper);
}
return wrapper;
}
/// 清理缓存数据
public static void ClearCache() {
cache.Clear();
}
}
优化方案选择:
优化方案这么多到底用哪个? 有最好的吗? (可以参考以下建议)
1.首选IL.Emit方案,性能很好,唯一的缺点是不兼容IL2CPP模式。
2.次选委托(Delegate.CreateDelegate)方案,在IL2CPP模式下用于替代IL.Emit,由于委托只能操作方法,不能操作字段,所以尽量写成{get; set;}的形式,如果有字段需要操作,那么再加上指针方案(IntPtr)。
终极优化手段:
如果以上手段你都尝试过了,依然不满意,作为一个成年人你想鱼和熊掌二者兼得,性能要最高的,通用性也要最好的,还要方便易用,你可以尝试代码生成方案,这个方案唯一的缺点就是你需要不少的时间去做代码生成工具和相关自动化工具的编写,开发成本颇高,可能要一直根据需求长期维护,属于把难题留给了自己,把方便留给了别人,前人栽树后人乘凉,做不好凉凉,做好了后人会:"听我说谢谢你,因为有你,温暖了四季..."。
或者你是个技术大佬,喜欢研究技术,这点东西就是个小菜儿,以下是代码生成方案简述:
代码生成的方案:
优点:性能最高,通用性好(IL2CPP下也可使用)
缺点:实现复杂,费时间。
通过Unity强大的Editor 自己写一套代码生成工具,把相关需要调用字段(属性)的功能都用代码生成。
比如我现在正在写一个二进制的数据格式,要实现对象序列化和反序列化的功能,在转换对象的过程中需要调用对象的字段(属性),假设当前操作的对象类型为:
【UserInfo】
public class UserInfo {
public int id;
public string name;
}
代码生成的序列化类则为:
【UserInfoSerializer】
public static class UserInfoSerializer {
/// 序列化
public static byte[] Serialize(UserInfo obj) {
if (obj == null) return new byte[0];
BinaryWriter writer = new BinaryWriter();
writer.WriteInt32(obj.id);
writer.WriteUnicode(obj.name);
return writer.ToBytes();
}
/// 反序列化
public static UserInfo Deserialize(byte[] bytes) {
if (bytes == null) return null;
UserInfo obj = new UserInfo();
if (bytes.Length == 0) return obj;
BinaryReader reader = new BinaryReader(bytes);
obj.id = reader.ReadInt32();
obj.name = reader.ReadUnicode();
return obj;
}
}
测试代码:
private void SerializeTest() {
// 初始化对象
UserInfo user = new UserInfo();
user.id = 1001;
user.name = "A";
// 序列化
byte[] bytes = UserInfoSerializer.Serialize(user);
// 反序列化
UserInfo u = UserInfoSerializer.Deserialize(bytes);
// 输出数据
print(u.id); // 1001
print(u.name); // "A"
}
测试代码通过,这个代码生成方案是行得通的,由于是具体类型直接调用,没有任何反射、字典查找、类型转换和装箱拆箱的消耗,性能极好(因为这就是直接调用),接下来就是要做易用性和自动化处理。
我们在Unity Editor中需要实现的功能大致如下:
在Unity中,选中一个我们需要序列化的类,右键菜单 > 生成序列化帮助类,以便调用。
这种生成最好别对原来的代码进行任何修改,每次生成只参考目标类,生成序列化类,不对目标类进行改动,这样重新生成的时候只要覆盖生成类就好。
总结:
有时候看似最笨,最麻烦的方案,反而是最好的,也许这就是返璞归真,重剑无锋,大巧不工吧。
想要最佳的性能,直接调用才是最好的方案,如何解决直接调用的问题,用代码生成,Unity有着强大的Editor和各种接口,如果有时间和信心,可以尝试一下代码生成方案,但因为可能消耗很多时间,这里就不推荐了,各位根据自己的喜好选择就好。总之先做完,再完善。