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对象相关推荐
- 图神经网络(GNN)入门之旅(三)-拉普拉斯矩阵与GCN
知乎专栏:图神经网络 第三篇: https://zhuanlan.zhihu.com/p/344005023
- Pascal游戏开发入门(三):游戏对象管理
Pascal游戏开发入门(三):游戏对象管理 游戏中有很多类对象,例如:角色,敌人,NPC,陷阱,子弹,门等等.跟踪并处理它们之间的交互是一个有难度的事情.为了尽可能简化并使之容易维护,本节将尝试使用 ...
- MATLAB深度学习入门之旅
目录 1. 简介 2. 使用预训练网络:使用已创建和训练后的网络进行分类 2.1 课程示例-识别一些图像中的对象 2.1.1 任务1:读取图像 2.1.2 任务2:显示图像 2.2 进行预测 2. ...
- 【笨木头Unity】入门之旅010(完结):Demo之四处找死(五)_UI
UI是游戏里必不可少的元素,在Unity里添加UI是比较轻松的事情,但要玩好它,可就不那么轻松了. 没关系,先入门. 笨木头花心贡献,啥?花心?不,是用心. 转载请注明,原文地址:http://www ...
- 打怪升级之小白的大数据之旅(三十一)<JavaSE总结>
打怪升级之小白的大数据之旅(三十) JavaSE总结 引言 Java这只小怪物我们已经练级差不多了,明天我们将进入新的旅程了,所以,我要对前面的整个JavaSE知识点进行总结,就像积攒够了经验升级一样 ...
- dump java 内存_Java如何dump对象的内存
Java如何dump对象的内存 这篇文章介绍如何使用java的Unsafe类来打印对象的内容. 基本步骤和C/C++类似,先获取对象的地址,然后打印出地址的内存内容. 假设我们定义一个class内容如 ...
- matlab入门之旅,MATLAB 入门之旅学习笔记
MATLAB 入门之旅学习笔记 https://matlabacademy.mathworks.com/R2019a/cn/portal.html?course=gettingstarted 1.概述 ...
- 变量的三重属性_TypeScript基础入门 - 变量声明(三)
转载地址TypeScript基础入门 - 变量声明(三)www.gowhich.com 项目实践仓库 https://github.com/durban89/typescript_demo.gitt ...
- python三大器,Python 入门之 Python三大器 之 生成器
Python 入门之 Python三大器 之 生成器 1.生成器 (1)什么是生成器? 核心:生成器的本质就是一个迭代器 迭代器是Python自带的 生成器程序员自己写的一种迭代器 def func( ...
最新文章
- python组成不重复的三位数是多少_Python输出由1,2,3,4组成的互不相同且无重复的三位数...
- 判定两棵二叉树是否相似以及左右子树交换、层次编号
- C 盘FAT32变为 RAW 格式
- LeetCode——Same Tree(判断两棵树是否相同)
- 华三交换机mode是什么意思_POE交换机150米、长距离250米传输是什么意思?
- TensorFlow入门--张量的定义与基本运算
- 基于观察者模式——创建显示天气数据
- vs如何运行外部 C++ 文件
- C# pictureBox桌面大小自适应 大小自适应 窗体居中
- C# 2.0中泛型编程初级入门
- JMeter 录制脚本
- opengl导入obj模型
- Essay Writing Guide
- 综述:人工智能、数据科学、机器学习
- 查找二叉排序树的双亲节点,并输出路径
- 怎么调整gif表情包的比例?
- php 兼容火狐,PHP_CSS兼容IE与火狐浏览器超强兼容代码,如何让你写的代码更兼容火狐 - phpStudy...
- python全栈需要学习什么_python全栈是什么意思
- 机器学习——朴素贝叶斯(Naive Bayes)详解及其python仿真
- php用户抽奖次数怎么做,获取用户剩余抽奖次数
热门文章
- vscode 在标签的src引入别名路径_从零开始 - VSCode 插件运行机制
- 【Java】ArrayList 列表的泛型
- rssi室内定位算法原理_智慧定位系统之蓝牙网关在室内定位技术的原理浅析
- android+多米音乐+自动播放,android 高仿多米音乐播放器
- java 求交集 算法_Java计算交集,差集,并集的方法示例
- mysql cluster自动安装_MySQL Cluster 安装
- 用c语言做教学课程安排,C语言入门课程安排
- pythonpop方法桐柏到郑州大_python脚本之一键移动自定格式文件方法实例
- apache 编译安装php mysql_编译安装APACHE+PHP+MYSQL
- 面试题总结14 动态规划