IL入门之旅(三)——Dump对象
Dump對(duì)象
??? 一個(gè)成熟的系統(tǒng),都少不了一個(gè)強(qiáng)大的Log,而Log通常需要把當(dāng)時(shí)的對(duì)象的很多信息記錄下來,因此Dump對(duì)象的功能在很多場(chǎng)合下都會(huì)使用到。
??? 那么來看看普通的Dump如何實(shí)現(xiàn):
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實(shí)例的內(nèi)容記錄到Log中,但是,思考一下,如果有100多個(gè)地方需要記錄Foo對(duì)象,就需要寫100多遍這樣的代碼嗎?
??? 當(dāng)然不會(huì)這么傻啦,利用擴(kuò)展方法可以很簡(jiǎn)單實(shí)現(xiàn):
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());??? 看起來是不是簡(jiǎn)單多了,當(dāng)時(shí),如果有100個(gè)不同的類型需要Dump,那么就需要100多個(gè)擴(kuò)展方法,并且需要經(jīng)常性的維護(hù)之間的關(guān)系。
??? 別忘了,.net的還有強(qiáng)大的反射,來想想反射如何實(shí)現(xiàn):
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()); }??? 如此簡(jiǎn)單的就打造了一個(gè)近乎萬能的Dump方法,不過,別忘了反射的代價(jià):性能。在大多數(shù)情況下,使用這種方式的性能損失是可以接受的,但是,如果在一個(gè)要求高性能的系統(tǒng)下,這樣的性能損失缺是需要深入思考的問題。
目標(biāo)制定
??? 于是,本文的核心命題就變成尋找一個(gè)高性能的并且統(tǒng)一的Dumper。
??? 當(dāng)然,限于篇幅,需要做明確要實(shí)現(xiàn)的Dump的實(shí)現(xiàn)范圍:
- 僅僅Dump編譯時(shí)已知的類型(為了最大限度的利用泛型的性能優(yōu)勢(shì))
- 僅僅Dump第一層公開實(shí)例屬性(如果支持Nest,會(huì)使問題復(fù)雜化)
- 需要支持null
- 需要支持結(jié)構(gòu)體
- 需要支持可空類型
準(zhǔn)備外殼
??? 那么首先準(zhǔn)備一下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方法僅僅檢查一下參數(shù),然后調(diào)用DumpCore方法,那么DumpCore方法如何實(shí)現(xiàn)哪?
??? 想想還是不太好辦啊,算了再轉(zhuǎn)嫁一次:
private static void DumpCore<T>(this T obj, TextWriter writer, string separator) {DumperImpl<T>.Action(writer, obj, separator ?? Environment.NewLine); }??? 現(xiàn)在從DumpCore變成了DumperImpl<T>了,然后這個(gè)類型怎么實(shí)現(xiàn)哪?
準(zhǔn)備內(nèi)核
??? 現(xiàn)在想想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();} }??? 這里利用靜態(tài)構(gòu)造函數(shù)只會(huì)運(yùn)行一次的特性,讓CLR幫助我們做同步。
??? 來看看CreateAction方法的實(shí)現(xiàn),這個(gè)方法需要?jiǎng)?chuàng)建一個(gè)Action,第一個(gè)參數(shù)是TextWriter,用于寫入Dump的內(nèi)容,第二個(gè)參數(shù)是T,也就是被Dump的對(duì)象,第三個(gè)參數(shù)是separator,用于分割內(nèi)容屬性。
??? 當(dāng)然這個(gè)Action不可能是現(xiàn)成的,所以需要一個(gè)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個(gè)方法需要處理,一個(gè)是ProcessWhenObjIsNull,用于處理對(duì)象是null的情況,第二個(gè)是WriteProperties,用于Dump對(duì)象的屬性。
??? 先來看看第一個(gè),不過先想一下,T在什么情況下,obj可以是null:
- 首先,T是引用類型
- 其次,T是可空類型
??? 那么,也就是需要對(duì)這兩個(gè)情況需要添加null檢測(cè)。不過,首先定義一個(gè)null的輸出值和TextWriter.Write方法:
private const string NullLiterals = "(null)"; private static readonly MethodInfo TextWriter_Write =typeof(TextWriter).GetMethod("Write", new Type[] { typeof(string) });??? 于是,ProcessWhenObjIsNull的實(shí)現(xiàn)就是:
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);} }??? 第一個(gè)if判斷T是否是值類型,如果不是值類型(即:引用類型)則需要判null,第二個(gè)判斷T是否是可空類型,如果是,則需要判null(利用可空類型為null時(shí)裝箱值為null的特性)。
??? 剩下一個(gè)WriteProperties才是難點(diǎn),先想想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);??? 可以發(fā)現(xiàn),Dump屬性分成2個(gè)部分,一個(gè)是寫屬性的名字,另一個(gè)是寫屬性的值。對(duì)了,別忘了還要寫separator。
??? 于是,方法的實(shí)現(xiàn)就是:
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個(gè)方法。
??? 其中,WriteHead和WriteSeparator方法比較簡(jiǎn)單:
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就比較復(fù)雜了,因?yàn)門可能是值類型,也可能是引用類型(在IL里面處理有區(qū)別),另外,屬性的value同樣有null的情況需要處理,另外有個(gè)性能優(yōu)化,如果屬性的值類型重寫了ToString方法,就不要裝箱后再調(diào)用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); }??? 終于,一個(gè)高性能的Dumper寫好了,雖然比起純反射版的代碼復(fù)雜了很多。不過,性能方面可以提高很多,接下來不妨測(cè)試一下吧。
性能測(cè)試
??? 為了測(cè)試這個(gè)高性能的Dumper到底能有多少性能優(yōu)勢(shì),使用了下面的測(cè)試代碼:
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使用第一節(jié)中的純反射方式,來看看運(yùn)行結(jié)果吧:
5795
906
??? 不快嘛,才6倍,為什么哪?再加一個(gè)對(duì)比測(cè)試:
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用到哪里去了?
??? 不妨再加一個(gè)對(duì)比測(cè)試:
sw.Reset(); sw.Start(); for (int i = 0; i < count; i++) {foo.Dump(TextWriter.Null); } Console.WriteLine(sw.ElapsedMilliseconds);??? 將內(nèi)容Dump到TextWriter.Null,這樣就不會(huì)有字符串拼接帶來的性能影響,再來看看結(jié)果:
5778
894
352
291
??? Dumper本身花費(fèi)的時(shí)間約300ms,Dumper另外使用的150ms在干什么哪?其中包括StringBuilder的擴(kuò)容,還有StringWriter的包裝的額外代價(jià)。
??? 而反射本身花費(fèi)的時(shí)間越5400ms,也就是9倍的時(shí)間,而拼接字符串約350ms,占到Dumper的1/3,反射的6%。
匿名類型
??? 之前的類型都是明確定義的類型,如果是匿名類型呢?
var foo = new { Bar = "Bar", FooBar = 100, };??? 再次運(yùn)行,就會(huì)發(fā)現(xiàn)報(bào)錯(cuò)了MethodAccessException,為什么哪?
??? 因?yàn)槟涿愋捅籧#編譯器翻譯為內(nèi)部類型,而DynamicMethod默認(rèn)是在Assembly之外的,所以,訪問這個(gè)類型的方法是受限制的,因此需要修改一下DynamicMethod的聲明:
DynamicMethod dm = new DynamicMethod(string.Empty, typeof(void),new Type[] { typeof(TextWriter), typeof(T), typeof(string) }, typeof(T));? ? 完成修改后,再跑一下,完全正常了。這個(gè)重載和原來的有什么區(qū)別哪?最后一個(gè)typeof(T)的作用就是把這個(gè)動(dòng)態(tài)方法聲明為T類型上的方法,因此,無論T是內(nèi)部類型還是外部類型,對(duì)這個(gè)方法本身而言,都是可見的,因此繞過了CLR的檢查。
? ? 最后在來看看性能分析:
19395
889
353
291
??? 除了反射外,性能基本沒變,那么反射為什么會(huì)變慢哪?因?yàn)?#xff0c;訪問內(nèi)部類型的方法需要經(jīng)過安全檢查,這個(gè)額外的工作自然拖慢反射的性能。
總結(jié)
以上是生活随笔為你收集整理的IL入门之旅(三)——Dump对象的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Open Source Bing Map
- 下一篇: 小炒是什么意思?