zoukankan      html  css  js  c++  java
  • 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 == 0
             select 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 method
            var 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

        除了反射外,性能基本没变,那么反射为什么会变慢哪?因为,访问内部类型的方法需要经过安全检查,这个额外的工作自然拖慢反射的性能。

  • 相关阅读:
    钩子函数和回调函数
    Vue.js的坑
    数据库清空表中的数据
    chrome jsonView插件安装
    PostgreSQL数据的导出导入
    PostgreSQL9.6.2的WINDOWS下安装
    HEXO+Github,搭建属于自己的博客
    Vue.js 入门指南之“前传”(含sublime text 3 配置)
    win系统下nodejs安装及环境配置
    Vue.js学习网址
  • 原文地址:https://www.cnblogs.com/vwxyzh/p/1719339.html
Copyright © 2011-2022 走看看