在上一篇中我們列舉了一些反射的常規的使用,這一篇我們將介紹一些關於關於反射的高級屬性,這些包括創建對反射的性能的總結以及如何優化反射性能,以及通過InvokeMember的方法如何去調用反射等等,通過對這些內容的逐步熟悉,我們會對整個反射有一個更加深入的了解與認識,在文章的最后我們會附上一個小DEMO從而供以后查閱使用,通過對比幾種不同的方法,我們來分別看各種方式進行操作的性能的差異,從而方便后續做出正確的選擇,在下面的例子中我們將比較:直接訪問花費的時間、采用EmitSet花費的時間、純反射花費的時間、采用泛型委托花費的時間以及采用通用接口花費的時間,從而就這幾種方式來做一組對比。
首先我們需要封裝一個Model從而供后面的代碼進行調用
public class OrderInfo
{
public int OrderID { get; set; }
public DateTime OrderDate { get; set; }
public decimal SumMoney { get; set; }
public string Comment { get; set; }
public bool Finished { get; set; }
public int Add(int a, int b)
{
return a + b;
}
}
1 首先是設置屬性時進行的對比
在進行最終的數據對比之前我們來看看這里面涉及到的幾種調用方式分別表示什么?
A 直接訪問花費時間
這個非常容易理解,就是直接new一個OrderInfo對象,然后對立面的各種屬性進行讀寫操作,典型的面向對象的寫法,這個是很好理解的,這里就不再贅述。
B EmitSet花費時間
這里需要看我們設置OrderInfo的屬性值的時候是通過下面的這句代碼來完成的:
SetValueDelegate setter2 = DynamicMethodFactory.CreatePropertySetter(propInfo);
這里SetValueDelegate是一個委托,后面的工廠方法用於創建這個委托
public static SetValueDelegate CreatePropertySetter(PropertyInfo property)
{
if( property == null )
{
throw new ArgumentNullException("property");
}
if( !property.CanWrite )
{
return null;
}
MethodInfo setMethod = property.GetSetMethod(true);
DynamicMethod dm = new DynamicMethod("PropertySetter", null,
new Type[] { typeof(object), typeof(object) },
property.DeclaringType, true);
ILGenerator il = dm.GetILGenerator();
if( !setMethod.IsStatic )
{
il.Emit(OpCodes.Ldarg_0);
}
il.Emit(OpCodes.Ldarg_1);
EmitCastToReference(il, property.PropertyType);
if( !setMethod.IsStatic && !property.DeclaringType.IsValueType )
{
il.EmitCall(OpCodes.Callvirt, setMethod, null);
}
else
{
il.EmitCall(OpCodes.Call, setMethod, null);
}
il.Emit(OpCodes.Ret);
return (SetValueDelegate)dm.CreateDelegate(typeof(SetValueDelegate));
}
這段代碼確實不太容易理解,我們取其中的關鍵的部分來進行解釋
DynamicMethod:定義並表示一種可編譯、執行和丟棄的動態方法。丟棄的方法可用於垃圾回收。具體的實例請參考這里。
dm.GetILGenerator():為該方法返回一個具有默認 MSIL 流大小(64 字節)的 Microsoft 中間語言 (MSIL) 生成器。
il.Emit:將指定的指令放到指令流上。
il.EmitCall:將 call 或 callvirt 指令放到 Microsoft 中間語言 (MSIL) 流上,以便調用 varargs 方法。關於Call和Callvirt的區別,參考其定義:
Call:調用由傳遞的方法說明符指示的方法。
Callvirt:對對象調用后期綁定方法,並且將返回值推送到計算堆棧上。
在調用這些方法后,完成動態方法並創建一個可用於執行該方法的委托,通過這些委托來完成對OrderInfo這個對象的屬性的賦值的過程。
C 純反射花費的時間
這項就更好理解了,首先通過PropertyInfo propInfo = typeof(OrderInfo).GetProperty("OrderID");來獲取到PropertyInfo對象,然后調用propInfo.SetValue(testObj, 123, null);來直接進行反射賦值,當然這種方法在進行大量的賦值時性能較差,這個可以通過后面的比較來進行說明。
D 泛型委托花費的時間
這種方法通過定義一個泛型類SetterWrapper<TTarget, TValue>,然后進行實例化,最后調用SetValue進行賦值。在SetterWrapper的構造函數中通過Delegate.CreateDelegate創建一個委托,委托對應的方法是MethodInfo m = propertyInfo.GetSetMethod(true);這種方法由於在實例化時確定了TTarget, TValue的類型,所以在最終的賦值的時候就沒有了類型轉換時的裝箱和拆箱的操作,所以相對純反射來說更快,但是相對於Emit這種直接操作MSIL方法來說,效果可能要差一些。
public class SetterWrapper<TTarget, TValue> : ISetValue
{
private Action<TTarget, TValue> _setter;
public SetterWrapper(PropertyInfo propertyInfo)
{
if( propertyInfo == null )
throw new ArgumentNullException("propertyInfo");
if( propertyInfo.CanWrite == false )
throw new NotSupportedException("屬性不支持寫操作。");
MethodInfo m = propertyInfo.GetSetMethod(true);
_setter = (Action<TTarget, TValue>)Delegate.CreateDelegate(typeof(Action<TTarget, TValue>), null, m);
}
public void SetValue(TTarget target, TValue val)
{
_setter(target, val);
}
void ISetValue.Set(object target, object val)
{
_setter((TTarget)target, (TValue)val);
}
}
E 通用接口花費時間
采用通用接口這種方式,能在一定程度上簡化調用的方式,但缺點也是非常明顯的,首先我們來看看接口中采用的定義:
public interface ISetValue
{
void Set(object target, object val);
}
由於接口中定義的Set方法都是采用的Object作為參數,這在通用性上確實比較好,但是在由於傳入的參數不可能都是Object類型,所以當傳入值類型時就需要進行類型轉換,進行拆箱操作,所以在進行大量操作時不可避免會有性能的損失,這個可以通過后面代碼運行的時間來判斷其優劣。
另外由於ISetValue這一接口是在泛型類SetterWrapper<TTarget, TValue>中繼承並實現的,所以在創建泛型類實例的時候,需要定義泛型類型,最終的實現是通過下面的代碼來實現的
Type instanceType = typeof(SetterWrapper<,>).MakeGenericType(propertyInfo.DeclaringType, propertyInfo.PropertyType); return (ISetValue)Activator.CreateInstance(instanceType, propertyInfo);
public static ISetValue CreatePropertySetterWrapper(PropertyInfo propertyInfo)
{
if(propertyInfo == null)
{
throw new ArgumentNullException("propertyInfo");
}
if(propertyInfo.CanWrite == false)
{
throw new NotSupportedException("屬性不支持寫操作。");
}
MethodInfo mi = propertyInfo.GetSetMethod(true);
if( mi.GetParameters().Length >1)
{
throw new NotSupportedException("不支持構造索引器屬性的委托。");
}
if( mi.IsStatic )
{
Type instanceType = typeof(StaticSetterWrapper<>).MakeGenericType(propertyInfo.PropertyType);
return (ISetValue)Activator.CreateInstance(instanceType, propertyInfo);
}
else
{
Type instanceType = typeof(SetterWrapper<,>).MakeGenericType(propertyInfo.DeclaringType, propertyInfo.PropertyType);
return (ISetValue)Activator.CreateInstance(instanceType, propertyInfo);
}
}
F:FastSet花費時間
這個實現方式在本質上是和通用接口實現方式一模一樣的,唯一不同的是第一次創建實例后會緩存在一個Hashtable中,這樣下次再獲取當前實例的時候多了一個遍歷Hashtable的過程了,所以最終會有一定的時間花費,其它的和通用接口的實現方法無異。
G:FastSet2花費時間
這個部分和FastSet比較起來的差別就是最終采用的是EmitSet的方式來實現的賦值過程,所以沒有類型轉換而且最終賦值的過程是直接對MSIL進行操作的,所以最后的時間也是比較短的。
在介紹完這些內容后,我們通過下面的幾個方法來進行10萬次的重復實驗過程,從而比較他們之間的差別,從而對整體有個直觀的比較。
static void Test_SetProperty(int count)
{
OrderInfo testObj = new OrderInfo();
PropertyInfo propInfo = typeof(OrderInfo).GetProperty("OrderID");
Console.Write("直接訪問花費時間: ");
Stopwatch watch1 = Stopwatch.StartNew();
for( int i = 0; i < count; i++ )
{
testObj.OrderID = 123;
}
watch1.Stop();
Console.WriteLine(watch1.Elapsed.ToString());
SetValueDelegate setter2 = DynamicMethodFactory.CreatePropertySetter(propInfo);
Console.Write("EmitSet花費時間: ");
Stopwatch watch2 = Stopwatch.StartNew();
for( int i = 0; i < count; i++ )
{
setter2(testObj, 123);
}
watch2.Stop();
Console.WriteLine(watch2.Elapsed.ToString());
Console.Write("純反射花費時間: ");
Stopwatch watch3 = Stopwatch.StartNew();
for( int i = 0; i < count; i++ )
{
propInfo.SetValue(testObj, 123, null);
}
watch3.Stop();
Console.WriteLine(watch3.Elapsed.ToString());
Console.Write("泛型委托花費時間: ");
SetterWrapper<OrderInfo, int> setter3 = new SetterWrapper<OrderInfo, int>(propInfo);
Stopwatch watch4 = Stopwatch.StartNew();
for(int i = 0; i < count; i++)
{
setter3.SetValue(testObj, 123);
}
watch4.Stop();
Console.WriteLine(watch4.Elapsed.ToString());
Console.Write("通用接口花費時間: ");
ISetValue setter4 = GetterSetterFactory.CreatePropertySetterWrapper(propInfo);
Stopwatch watch5 = Stopwatch.StartNew();
for( int i = 0; i < count; i++ )
{
setter4.Set(testObj, 123);
}
watch5.Stop();
Console.WriteLine(watch5.Elapsed.ToString());
propInfo.FastSetValue(testObj, 123);
Console.Write("FastSet花費時間: ");
Stopwatch watch6 = Stopwatch.StartNew();
for( int i = 0; i < count; i++ )
propInfo.FastSetValue(testObj, 123);
watch6.Stop();
Console.WriteLine(watch6.Elapsed.ToString());
propInfo.FastSetValue2(testObj, 123);
Console.Write("FastSet2花費時間: ");
Stopwatch watch6b = Stopwatch.StartNew();
for( int i = 0; i < count; i++ )
propInfo.FastSetValue2(testObj, 123);
watch6b.Stop();
Console.WriteLine(watch6b.Elapsed.ToString());
Hashtable table = new Hashtable();
table[propInfo] = new object();
Console.Write("Hashtable花費時間: ");
Stopwatch watch7 = Stopwatch.StartNew();
for( int i = 0; i < count; i++ )
{
object val = table[propInfo];
}
watch7.Stop();
Console.WriteLine(watch7.Elapsed.ToString());
Console.WriteLine("-------------------");
Console.WriteLine("純反射花費時間({0}) / 直接訪問花費時間({1}) = {2}",
watch3.Elapsed.ToString(),
watch1.Elapsed.ToString(),
watch3.Elapsed.TotalMilliseconds / watch1.Elapsed.TotalMilliseconds);
Console.WriteLine("純反射花費時間({0})/ 通用接口花費時間({1}) = {2}",
watch3.Elapsed.ToString(),
watch5.Elapsed.ToString(),
watch3.Elapsed.TotalMilliseconds / watch5.Elapsed.TotalMilliseconds);
Console.WriteLine("純反射花費時間({0}) / FastSet花費時間({1}) = {2}",
watch3.Elapsed.ToString(),
watch6.Elapsed.ToString(),
watch3.Elapsed.TotalMilliseconds / watch6.Elapsed.TotalMilliseconds);
}
通過下面的截圖,我們能夠清晰看出每種訪問方式之間的差異,了解了這些差異后我們能夠在進行反射時采用一種最合理的方式來使用,同時我們也需要加深對Emit這種技術的理解,從而在改善軟件性能方面有自己的方法。
上面的示例僅僅是對OrderInfo的某一個屬性進行賦值的操作時所作出的對比,在讀取和創建類型實例的時候結果也是相同的,今天就介紹到這里,最后分享整個軟件的DEMO,如果有需要可以點擊進行下載。
