.NET Core/.NET Framework 的 System.Reflection.Emit 命名空间为我们提供了动态生成 IL 代码的能力。利用这项能力,我们能够在运行时生成一段代码/一个方法/一个类/一个程序集。

大家都知道反射的性能很差,通过缓存反射调用的方法则能够大幅提升性能。Emit 为我们提供了这项能力,我们能够在运行时生成一段代码,替代使用反射动态调用的代码,以提升性能。


 

我们在解决什么问题?

之前我写过一篇创建委托以大幅度提高反射调用的性能的方法,不过此方法适用于预先知道方法参数和返回值类型的情况。如果我们在编译期不知道类型,那么它就行不通了。(原因?注意到那篇文章中返回的委托有类型强转吗?也就是说需要编译期确定类型,即便是泛型。)

例如,我们在运行时得到一个对象,希望为这个对象的部分或全部属性赋值;此对象的类型和属性类型在编译期全部不可知(就算是泛型也没有)。

class SomeClass
{
[DefaultValue("walterlv")]
public string SomeProperty { get; set; }
}

众所周知的反射能够完成这个目标,但它不是本文讨论的重点;因为一旦这样的方法会被数万数十万甚至更多次调用的时候,反射将造成性能灾难。

既然反射不行,通过反射的创建委托也不行,那还有什么方法?

  1. 使用表达式树(不是本文重点)
  2. 使用 Emit(本文)

如果事先不能知道类型,那么只能每次通过反射去动态的调用,于是才会耗费大量的性能。如果我们能够在运行时动态地生成一段调用方法,那么这个调用方法将可以缓存下来供后续重复调用。如果我们使用 Emit,那么生成的方法与静态编写的代码是一样的,于是就能获得普通方法的性能。

为了实现动态地设置未知类型未知属性的值,我决定写出如下方法:

static void SetPropertyValue(object @this, object value)
{
((类的类型) @this).属性名称 = (属性的类型) value;
}

不用考虑编译问题了,这段代码是肯定编译不过的。方法是一个静态方法,传入两个参数——类型的实例和属性的新值;方法内部为实例中某个属性赋新值。

类的类型、属性名称和属性的类型是编译期不能确定,但可以在运行时确定的;如果此生成的方法会被大量调用,那么性能优势将极其明显。

快速编写 Emit

为了快速编写和调试 Emit,我们需要 ReSharper 全家桶:

  • ReSharper - 用于实时查看 IL 代码
  • dotPeek - 免费,用于查看我们使用 Emit 生成的代码,便于对比分析

相比于原生 Visual Studio,有此工具帮助的情况下,IL 的编写速度和调试速度将得到质的提升。(当然,利用这些工具依然只是手工操作,存在瓶颈;如果你阅读完本文之后找到或编写一个新的工具,更快,欢迎与我探讨。)

ReSharper 提供了 IL Viewer 窗格,从菜单依次进入 ReSharper->Windows->IL Viewer 可以打开。

打开后立即可以看到我们当前正在编写的代码的 IL,而且还能高亮光标所在的代码块。(如果你的 IL Viewer 中没有代码或没有高亮,编译一遍项目即可。)

我们要做的,就是得知 SetPropertyValue 在编译后将得到什么样的 IL 代码,这样我们才能编写出正确的 IL 生成代码来。于是编写这些辅助代码:

namespace Walterlv.Demo
{
class Program
{
static void Main(string[] args)
{
var instance = new TempClass();
SetPropertyValue(instance, "test");
} static void SetPropertyValue(object @this, object value)
{
((TempClass) @this).TempProperty = (string) value;
}
} public class TempClass
{
public string TempProperty { get; set; }
}
}

编译之后去 IL Viewer 中看 SetPropertyValue 的 IL 代码:

.method private hidebysig static void
SetPropertyValue(
object this,
object 'value'
) cil managed
{
.maxstack 8 // [14 9 - 14 10]
IL_0000: nop // [15 13 - 15 63]
IL_0001: ldarg.0 // this
IL_0002: castclass Walterlv.Demo.TempClass
IL_0007: ldarg.1 // 'value'
IL_0008: castclass [System.Runtime]System.String
IL_000d: callvirt instance void Walterlv.Demo.TempClass::set_TempProperty(string)
IL_0012: nop // [16 9 - 16 10]
IL_0013: ret } // end of method Program::SetPropertyValue

将这段 IL 代码抄下来。怎么抄呢?看下面我抄的代码,你应该能够很容易看出里面一一对应的关系。

public static Action<object, object> CreatePropertySetter(PropertyInfo propertyInfo)
{
var declaringType = propertyInfo.DeclaringType;
var propertyType = propertyInfo.PropertyType; // 创建一个动态方法,参数依次为方法名、返回值类型、参数类型。
// 对应着 IL 中的
// .method private hidebysig static void
// SetPropertyValue(
// ) cil managed
var method = new DynamicMethod("<set_Property>", typeof(void), new[] {typeof(object), typeof(object)});
var il = method.GetILGenerator(); // 定义形参。注意参数位置从 1 开始——即使现在在写静态方法。
// 对应着 IL 中的
// object this,
// object 'value'
method.DefineParameter(1, ParameterAttributes.None, "this");
method.DefineParameter(2, ParameterAttributes.None, "value"); // 用 Emit 生成 IL 代码。
// 对应着 IL 中的各种操作符。
il.Emit(OpCodes.Nop);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Castclass, declaringType);
il.Emit(OpCodes.Ldarg_1);
// 注意:下一句代码会在文章后面被修改。
il.Emit(OpCodes.Castclass, propertyType);
il.Emit(OpCodes.Callvirt, propertyInfo.GetSetMethod());
il.Emit(OpCodes.Nop);
il.Emit(OpCodes.Ret); // 为生成的动态方法创建调用委托,返回返回这个委托。
return (Action<object, object>) method.CreateDelegate(typeof(Action<object, object>));
}

现在我们用下面新的代码替换之前写在 Main 中直接赋值的代码:

static void Main(string[] args)
{
// 测试代码。
var instance = new TempClass();
var propertyInfo = typeof(TempClass).GetProperties().First();
// 调用 Emit 核心代码。
var setValue = QuickEmit.CreatePropertySetter(propertyInfo);
// 测试生成的核心代码能否正常工作。
setValue(instance, "test");
}

直接运行,在 setValue 之后我们查看 instanceTempProperty 属性的值,可以发现已经成功修改了。大功告成

快速调试和修改 Emit

才没有大功告成呢

试试把 TempProperty 的类型改为 int。把测试代码中传入的 "test" 字符串换成数字 5。运行看看:


▲ 为什么会崩溃?!

崩溃提示是“操作可能造成运行时的不稳定”。是什么造成了运行时的不稳定呢?难道是我们写的 IL 不对?

现在开始利用 dotPeek 进行 IL 的调试

我们编写另外一个方法,用于将我们的生成的 IL 代码输出到 dll 文件。

public static void OutputPropertySetter(PropertyInfo propertyInfo)
{
var declaringType = propertyInfo.DeclaringType;
var propertyType = propertyInfo.PropertyType; // 准备好要生成的程序集的信息。
var assemblyName = new AssemblyName("Temp");
var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Save);
var module = assembly.DefineDynamicModule(assemblyName.Name, assemblyName.Name + ".dll");
var type = module.DefineType("Temp", TypeAttributes.Public);
var method = type.DefineMethod("<set_Property>",
MethodAttributes.Static - MethodAttributes.Public, CallingConventions.Standard,
typeof(void), new[] { typeof(object), typeof(object) });
var il = method.GetILGenerator(); // 跟之前一样生成 IL 代码。
method.DefineParameter(1, ParameterAttributes.None, "this");
method.DefineParameter(2, ParameterAttributes.None, "value"); il.Emit(OpCodes.Nop);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Castclass, declaringType);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Castclass, propertyType);
il.Emit(OpCodes.Callvirt, propertyInfo.GetSetMethod());
il.Emit(OpCodes.Nop);
il.Emit(OpCodes.Ret); // 将 IL 代码输出到程序的同级目录下。
type.CreateType();
assembly.Save($"{assemblyName.Name}.dll");
}

同样的,作为对照,我们在我们的测试程序中也修改那个参考代码。

static void SetPropertyValue(object @this, object value)
{
// 注意!注意!string 已经换成了 int。
((TempClass) @this).TempProperty = (int) value;
}

重新生成可以得到一个 exe,调用新写的 OutputPropertySetter 可以得到 Temp.dll。于是我们的输出目录下现在存在两个程序集:

将他们都拖进 dotPeek 中,然后在顶部菜单 Windows->IL Viewer 中打开 IL 显示窗格。

发现什么了吗?是的!对于结构体,用的是拆箱!!!而不是强制类型转换。

知道有了拆箱,于是就能知道应该怎样改了,生成 IL 的代码中 Castclass 部分应该根据条件进行判断:

var castingCode = propertyInfo.PropertyType.IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass;
il.Emit(castingCode, propertyType);

现在运行,即可正常通过。如果你希望拥有完整的代码,可以自行将以上两句替换掉此前注释说明了 注意:下一句代码会在文章后面被修改。 的地方。

更进一步

  • 如果要 Emit 的代码中存在 if-else 这样的非顺序结构怎么办?阅读 使用 Emit 生成 IL 代码 - 吕毅 可以了解做法。
  • 我们可以用 intdouble 类型的属性赋值,但在本例代码中却不可行,如何解决这种隐式转换的问题?

如果你尝试编写了 Emit 的代码,那么上面的问题应该难不倒你。

总结

  1. 通过 Emit,我们能够在运行时动态生成 IL 代码,以解决反射动态调用方法造成的大量性能损失。
  2. 通过 ReSharper 插件,我们可以实时查看生成的 IL 代码。
  3. 我们可以将 Emit 生成的代码输出到程序集文件。
  4. 通过 dotPeek,我们可以查看程序集中类型和方法的 IL 代码。

参考资料

如何快速编写和调试 Emit 生成 IL 的代码的更多相关文章

  1. 使用 Emit 生成 IL 代码

    .NET Core/.NET Framework 的 System.Reflection.Emit 命名空间为我们提供了动态生成 IL 代码的能力.利用这项能力,我们能够在运行时生成一段代码/一个方法 ...

  2. Emit 自动生成IL代码,注入代码

    Spring 框架中的注入代码,以及自动生成对接口的实现,则根据il代码注入 Emit学习(1)-Emit概览 一.Emit概述 Emit,可以称为发出或者产生.在Framework中,与Emit相关 ...

  3. VsCode编写和调试.NET Core

    本文转自:https://www.cnblogs.com/Leo_wl/p/6732242.html 阅读目录 使用VsCode编写和调试.NET Core项目 回到目录 使用VsCode编写和调试. ...

  4. Windows服务的快速搭建与调试(C#图解)

    Windows服务的快速搭建与调试(C#图解)   目录 一.什么是Windows 服务? 二.创建Windows 服务与安装/卸载批处理. 三.调试Windows 服务. 正文 一.什么是Windo ...

  5. 转:在VS2010下编译、调试和生成mex文件

    最近帮人调了一个程序,是网上公开的代码,利用matlab与c++混合编程做三维模型关键点检测,发现他们可以用VS2010编译.调试.生成mexw32文件,因此觉得之前在Matlab上利用mex命令真是 ...

  6. Emmet:HTML/CSS代码快速编写神器

    本文来源:http://www.iteye.com/news/27580    ,还可参考:http://www.w3cplus.com/tools/emmet-cheat-sheet.html Em ...

  7. OC编写使用调试器

    OC编写使用调试器 编写代码免不了,Bug.那么Debug就是程序员的必备技能了.本文和大家一起探讨,如何在应用开发编写代码过程中,使用日志项消息:以及使用动作.条件.迭代控制增强断点. 记录信息 在 ...

  8. 快速编写HTML,CSS代码的有力工具Emmet插件

    Emmet 是一个编辑器插件,它以一种简写的语法规则可用于快速编写html或css文档内容,它支持多种编辑器. 从官网:http://emmet.io/ 可下载各个编辑器的插件.notepad++ 插 ...

  9. Emmet:HTML/CSS代码快速编写神器(转)

    Emmet的前身是大名鼎鼎的Zen coding,如果你从事Web前端开发的话,对该插件一定不会陌生.它使用仿CSS选择器的语法来生成代码,大大提高了HTML/CSS代码编写的速度,比如下面的演示: ...

随机推荐

  1. 监控控制台是否运行的bat

    @echo offrem set secs=5set srvname="TRS.Export.Scheduler.exe" echo.echo ================== ...

  2. mysql字符编码的设置以及mysql中文乱码的解决方法

    查看字符编码 首先,将中文插入到数据库乱码是因为没有将数据库编码设置为支持中文的编码,mysql的默认编码是Latin1,不支持中文,应该设置为utf8查看自己的数据库编码是否已设置好,进入数据库,输 ...

  3. 开机启动服务(ftp、apache、mysql)

    当linux服务器开机时,会将 /etc/rc.d/rc.local 中的指令全部执行一遍, 因此将相应服务的启动指令放到该shell脚本中即可实现开机启动效果; 在 /etc/rc.d/rc.loc ...

  4. FontAwesome::Sass(5.x版)使用帮助。

    FontAwesome::Sass(5.x版) https://fontawesome.com/icons?d=gallery&m=free 只能使用免费的. 在app/assets/styl ...

  5. JSON 参考文档

    1.JSON字符串转换为JSON对象 var obj = JSON.parse(str); 2.JSON对象转化为JSON字符串 var str = JSON.stringify(obj); 对此有一 ...

  6. linux exec和xargs的区别

    -exec     1.参数是一个一个传递的,传递一个参数执行一次,效率低     2.文件名有空格等特殊字符也能处理 -xargs      1.一次将参数传给命令,可以使用-n控制参数个数     ...

  7. Kafka、RabbitMQ、RocketMQ、ActiveMQ 17 个方面综合对比

    本文将从,Kafka.RabbitMQ.ZeroMQ.RocketMQ.ActiveMQ 17 个方面综合对比作为消息队列使用时的差异.(欢迎加入Java程序员群:630441304,一起学习交流会) ...

  8. nyoj20——有向无环图深搜模板

    吝啬的国度 时间限制:1000 ms  |  内存限制:65535 KB 难度:3   描述 在一个吝啬的国度里有N个城市,这N个城市间只有N-1条路把这个N个城市连接起来.现在,Tom在第S号城市, ...

  9. spring boot 中logback多环境配置

    spring boot 配置logback spring boot自带了log打印功能,使用的是Commons logging 具体可以参考spring boot log 因此,我们只需要在resou ...

  10. zoj 2976 Light Bulbs(暴力枚举)

    Light Bulbs Time Limit: 2 Seconds      Memory Limit: 65536 KB Wildleopard had fallen in love with hi ...