IL入门之旅(三)——Dump对象
Dump對象
??? 一個成熟的系統,都少不了一個強大的Log,而Log通常需要把當時的對象的很多信息記錄下來,因此Dump對象的功能在很多場合下都會使用到。
??? 那么來看看普通的Dump如何實現:
public class Foo {public string Bar { get; set; }public int FooBar { get; set; } } Foo foo = new Foo { Bar = "Bar", FooBar = 100, }; Trace.TraceInformation("Foo: Bar=" + foo.Bar + ",FooBar=" + foo.FooBar.ToString());??? 如此,就把Foo實例的內容記錄到Log中,但是,思考一下,如果有100多個地方需要記錄Foo對象,就需要寫100多遍這樣的代碼嗎?
??? 當然不會這么傻啦,利用擴展方法可以很簡單實現:
public static string Dump(this Foo foo) {return "Foo: Bar=" + foo.Bar + ",FooBar=" + foo.FooBar.ToString(); } Foo foo = new Foo { Bar = "Bar", FooBar = 100, }; Trace.TraceInformation(foo.Dump());??? 看起來是不是簡單多了,當時,如果有100個不同的類型需要Dump,那么就需要100多個擴展方法,并且需要經常性的維護之間的關系。
??? 別忘了,.net的還有強大的反射,來想想反射如何實現:
public static string Dump(this object obj) {return obj.GetType().Name + ": " + string.Join(",",(from p in obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)where p.GetGetMethod() != null && p.GetIndexParameters().Length == 0select p.Name + "=" + p.GetValue(obj, null)).ToArray()); }??? 如此簡單的就打造了一個近乎萬能的Dump方法,不過,別忘了反射的代價:性能。在大多數情況下,使用這種方式的性能損失是可以接受的,但是,如果在一個要求高性能的系統下,這樣的性能損失缺是需要深入思考的問題。
目標制定
??? 于是,本文的核心命題就變成尋找一個高性能的并且統一的Dumper。
??? 當然,限于篇幅,需要做明確要實現的Dump的實現范圍:
- 僅僅Dump編譯時已知的類型(為了最大限度的利用泛型的性能優勢)
- 僅僅Dump第一層公開實例屬性(如果支持Nest,會使問題復雜化)
- 需要支持null
- 需要支持結構體
- 需要支持可空類型
準備外殼
??? 那么首先準備一下Dump的外殼:
public static string Dump<T>(this T obj) {var writer = new StringWriter();DumpCore<T>(obj, writer, null);return writer.ToString(); }public static string Dump<T>(this T obj, string separator) {var writer = new StringWriter();DumpCore<T>(obj, writer, separator);return writer.ToString(); }public static void Dump<T>(this T obj, StringBuilder builder) {if (builder == null)throw new ArgumentNullException("builder");DumpCore(obj, new StringWriter(builder), null); }public static void Dump<T>(this T obj, StringBuilder builder, string separator) {if (builder == null)throw new ArgumentNullException("builder");DumpCore(obj, new StringWriter(builder), separator); }public static void Dump<T>(this T obj, TextWriter writer) {if (writer == null)throw new ArgumentNullException("writer");DumpCore(obj, writer, null); }public static void Dump<T>(this T obj, TextWriter writer, string separator) {if (writer == null)throw new ArgumentNullException("writer");DumpCore(obj, writer, separator); }??? 其中separator是用于連接屬性的分隔符。
??? 所有的Dump方法僅僅檢查一下參數,然后調用DumpCore方法,那么DumpCore方法如何實現哪?
??? 想想還是不太好辦啊,算了再轉嫁一次:
private static void DumpCore<T>(this T obj, TextWriter writer, string separator) {DumperImpl<T>.Action(writer, obj, separator ?? Environment.NewLine); }??? 現在從DumpCore變成了DumperImpl<T>了,然后這個類型怎么實現哪?
準備內核
??? 現在想想DumperImpl<T>的骨架:
private static class DumperImpl<T> {public readonly static Action<TextWriter, T, string> Action = CreateAction();private static Action<TextWriter, T, string> CreateAction(){throw new NotImplementedException();} }??? 這里利用靜態構造函數只會運行一次的特性,讓CLR幫助我們做同步。
??? 來看看CreateAction方法的實現,這個方法需要創建一個Action,第一個參數是TextWriter,用于寫入Dump的內容,第二個參數是T,也就是被Dump的對象,第三個參數是separator,用于分割內容屬性。
??? 當然這個Action不可能是現成的,所以需要一個DynamicMethod,于是代碼就變成了這樣:
private static Action<TextWriter, T, string> CreateAction() {DynamicMethod dm = new DynamicMethod(string.Empty, typeof(void),new Type[] { typeof(TextWriter), typeof(T), typeof(string) });var il = dm.GetILGenerator();// string temp;var temp = il.DeclareLocal(typeof(string));ProcessWhenObjIsNull(il);WriteProperties(il, temp);il.Emit(OpCodes.Ret);return (Action<TextWriter, T, string>)dm.CreateDelegate(typeof(Action<TextWriter, T, string>)); }??? 里面有2個方法需要處理,一個是ProcessWhenObjIsNull,用于處理對象是null的情況,第二個是WriteProperties,用于Dump對象的屬性。
??? 先來看看第一個,不過先想一下,T在什么情況下,obj可以是null:
- 首先,T是引用類型
- 其次,T是可空類型
??? 那么,也就是需要對這兩個情況需要添加null檢測。不過,首先定義一個null的輸出值和TextWriter.Write方法:
private const string NullLiterals = "(null)"; private static readonly MethodInfo TextWriter_Write =typeof(TextWriter).GetMethod("Write", new Type[] { typeof(string) });??? 于是,ProcessWhenObjIsNull的實現就是:
private static void ProcessWhenObjIsNull(ILGenerator il) {if (!typeof(T).IsValueType){// if (obj == null) { writer.Write(NullLiterals); return; }var NotNullLable = il.DefineLabel();il.Emit(OpCodes.Ldarg_1);il.Emit(OpCodes.Brtrue_S, NotNullLable);il.Emit(OpCodes.Ldarg_0);il.Emit(OpCodes.Ldstr, NullLiterals);il.Emit(OpCodes.Callvirt, TextWriter_Write);il.Emit(OpCodes.Ret);il.MarkLabel(NotNullLable);}else if (Nullable.GetUnderlyingType(typeof(T)) != null){// if (obj == null) { writer.Write(NullLiterals); return; }var NotNullLable = il.DefineLabel();il.Emit(OpCodes.Ldarg_1);il.Emit(OpCodes.Box, typeof(T));il.Emit(OpCodes.Brtrue_S, NotNullLable);il.Emit(OpCodes.Ldarg_0);il.Emit(OpCodes.Ldstr, NullLiterals);il.Emit(OpCodes.Callvirt, TextWriter_Write);il.Emit(OpCodes.Ret);il.MarkLabel(NotNullLable);} }??? 第一個if判斷T是否是值類型,如果不是值類型(即:引用類型)則需要判null,第二個判斷T是否是可空類型,如果是,則需要判null(利用可空類型為null時裝箱值為null的特性)。
??? 剩下一個WriteProperties才是難點,先想想c#怎么寫:
string propName = "Property"; writer.Write(propName + "="); object propValue = obj.Property; string temp; if (propValue != null) {temp = propValue.ToString(); } else {temp = "(null)"; } writer.Write(temp);??? 可以發現,Dump屬性分成2個部分,一個是寫屬性的名字,另一個是寫屬性的值。對了,別忘了還要寫separator。
??? 于是,方法的實現就是:
private static void WriteProperties(ILGenerator il, LocalBuilder temp) {foreach (var prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)){if (prop.GetIndexParameters().Length > 0)continue;var getMethod = prop.GetGetMethod();if (getMethod == null)continue;WriteHead(il, prop);var propCompletedLable = il.DefineLabel();WriteValue(il, temp, prop, getMethod, propCompletedLable);il.MarkLabel(propCompletedLable);WriteSeparator(il);} }??? 然后就是WriteHead(即:屬性名),WriteValue(屬性值),WriteSeparator(分隔符),這3個方法。
??? 其中,WriteHead和WriteSeparator方法比較簡單:
private static void WriteHead(ILGenerator il, PropertyInfo prop) {// writer.Write("%PropertyName%=");il.Emit(OpCodes.Ldarg_0);il.Emit(OpCodes.Ldstr, prop.Name + "=");il.Emit(OpCodes.Callvirt, TextWriter_Write); } private static void WriteSeparator(ILGenerator il) {// writer.Write(separator);il.Emit(OpCodes.Ldarg_0);il.Emit(OpCodes.Ldarg_2);il.Emit(OpCodes.Callvirt, TextWriter_Write); }??? 但是,WriteValue就比較復雜了,因為T可能是值類型,也可能是引用類型(在IL里面處理有區別),另外,屬性的value同樣有null的情況需要處理,另外有個性能優化,如果屬性的值類型重寫了ToString方法,就不要裝箱后再調用object.ToString。
private static readonly MethodInfo Object_ToString =typeof(object).GetMethod("ToString", Type.EmptyTypes); private static void WriteValue(ILGenerator il, LocalBuilder temp,PropertyInfo prop, MethodInfo getMethod, Label propCompletedLable) {LoadPropertyValue(il, getMethod);var propType = prop.PropertyType;ProcessWhenValueIsNull(il, propType, propCompletedLable);GetValueString(il, propType, temp);WriteValueString(il, temp); }private static void LoadPropertyValue(ILGenerator il, MethodInfo getMethod) {// var value = obj.%Property%;if (typeof(T).IsValueType){il.Emit(OpCodes.Ldarga, 1);il.Emit(OpCodes.Call, getMethod);}else{il.Emit(OpCodes.Ldarg_1);il.Emit(OpCodes.Callvirt, getMethod);} }private static void ProcessWhenValueIsNull(ILGenerator il, Type propType, Label propCompletedLable) {if (!propType.IsValueType){// if (value == null) { writer.Write(NullLiterals); } else ...var NotNullLable = il.DefineLabel();il.Emit(OpCodes.Dup);il.Emit(OpCodes.Brtrue_S, NotNullLable);il.Emit(OpCodes.Pop);il.Emit(OpCodes.Ldarg_0);il.Emit(OpCodes.Ldstr, NullLiterals);il.Emit(OpCodes.Callvirt, TextWriter_Write);il.Emit(OpCodes.Br, propCompletedLable);il.MarkLabel(NotNullLable);}else if (Nullable.GetUnderlyingType(propType) != null){// if (value == null) { writer.Write(NullLiterals); } else ...var NotNullLable = il.DefineLabel();il.Emit(OpCodes.Dup);il.Emit(OpCodes.Box, propType);il.Emit(OpCodes.Brtrue_S, NotNullLable);il.Emit(OpCodes.Pop);il.Emit(OpCodes.Ldarg_0);il.Emit(OpCodes.Ldstr, NullLiterals);il.Emit(OpCodes.Callvirt, TextWriter_Write);il.Emit(OpCodes.Br, propCompletedLable);il.MarkLabel(NotNullLable);} }private static void GetValueString(ILGenerator il, Type propType, LocalBuilder temp) {if (propType.IsValueType){// is override ToString methodvar toStringMethod = propType.GetMethod("ToString",BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly,null, Type.EmptyTypes, null);if (toStringMethod != null){// call ToString without boxing// %PropertyType% x;var x = il.DeclareLocal(propType);// x = value;il.Emit(OpCodes.Stloc, x);// temp = x.ToString();il.Emit(OpCodes.Ldloca, x);il.Emit(OpCodes.Call, toStringMethod);il.Emit(OpCodes.Stloc, temp);}else{// call ToString with boxing// temp = ((object)value).ToString();il.Emit(OpCodes.Box, propType);il.Emit(OpCodes.Callvirt, Object_ToString);il.Emit(OpCodes.Stloc, temp);}}else{// temp = value.ToString();il.Emit(OpCodes.Callvirt, Object_ToString);il.Emit(OpCodes.Stloc, temp);} }private static void WriteValueString(ILGenerator il, LocalBuilder temp) {// writer.Write(temp);il.Emit(OpCodes.Ldarg_0);il.Emit(OpCodes.Ldloc, temp);il.Emit(OpCodes.Callvirt, TextWriter_Write); }??? 終于,一個高性能的Dumper寫好了,雖然比起純反射版的代碼復雜了很多。不過,性能方面可以提高很多,接下來不妨測試一下吧。
性能測試
??? 為了測試這個高性能的Dumper到底能有多少性能優勢,使用了下面的測試代碼:
Foo foo = new Foo { Bar = "Bar", FooBar = 100, }; const int count = 1000000; Stopwatch sw = Stopwatch.StartNew(); for (int i = 0; i < count; i++) {foo.DumpByReflection(); } Console.WriteLine(sw.ElapsedMilliseconds); sw.Reset(); sw.Start(); for (int i = 0; i < count; i++) {foo.Dump(); }??? 其中DumpByReflection使用第一節中的純反射方式,來看看運行結果吧:
5795
906
??? 不快嘛,才6倍,為什么哪?再加一個對比測試:
sw.Reset(); sw.Start(); for (int i = 0; i < count; i++) {var temp = "Bar=" + foo.Bar + ", FooBar=" + foo.FooBar.ToString(); } Console.WriteLine(sw.ElapsedMilliseconds);??? 再看看速度:
5769
892
353
??? 拼字符串本身就用了353ms,難怪速度快不上去了,那么900ms-350ms,那還有450ms用到哪里去了?
??? 不妨再加一個對比測試:
sw.Reset(); sw.Start(); for (int i = 0; i < count; i++) {foo.Dump(TextWriter.Null); } Console.WriteLine(sw.ElapsedMilliseconds);??? 將內容Dump到TextWriter.Null,這樣就不會有字符串拼接帶來的性能影響,再來看看結果:
5778
894
352
291
??? Dumper本身花費的時間約300ms,Dumper另外使用的150ms在干什么哪?其中包括StringBuilder的擴容,還有StringWriter的包裝的額外代價。
??? 而反射本身花費的時間越5400ms,也就是9倍的時間,而拼接字符串約350ms,占到Dumper的1/3,反射的6%。
匿名類型
??? 之前的類型都是明確定義的類型,如果是匿名類型呢?
var foo = new { Bar = "Bar", FooBar = 100, };??? 再次運行,就會發現報錯了MethodAccessException,為什么哪?
??? 因為匿名類型被c#編譯器翻譯為內部類型,而DynamicMethod默認是在Assembly之外的,所以,訪問這個類型的方法是受限制的,因此需要修改一下DynamicMethod的聲明:
DynamicMethod dm = new DynamicMethod(string.Empty, typeof(void),new Type[] { typeof(TextWriter), typeof(T), typeof(string) }, typeof(T));? ? 完成修改后,再跑一下,完全正常了。這個重載和原來的有什么區別哪?最后一個typeof(T)的作用就是把這個動態方法聲明為T類型上的方法,因此,無論T是內部類型還是外部類型,對這個方法本身而言,都是可見的,因此繞過了CLR的檢查。
? ? 最后在來看看性能分析:
19395
889
353
291
??? 除了反射外,性能基本沒變,那么反射為什么會變慢哪?因為,訪問內部類型的方法需要經過安全檢查,這個額外的工作自然拖慢反射的性能。
總結
以上是生活随笔為你收集整理的IL入门之旅(三)——Dump对象的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Open Source Bing Map
- 下一篇: 小炒是什么意思?