.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. Devops 到底是什么?

    Devops 到底是什么? 过去一年以来,一批来自欧美的.不墨守陈规的系统管理员和开发人员一直在谈论一个新概念:DevOps.DevOps就是开发(Development)和运维(Operations ...

  2. cocos2d-js入门一

    决定搞cocos2d-js,但发现官网已经没有独立的js了,lua,现在全部整合到cocos2d-x中了. win7+cocos2d-x 3.8 由于之前搭建了vs2012 +python平台 ,此时 ...

  3. (转)浅谈SQL Server 对于内存的管理

    简介 理解SQL Server对于内存的管理是对于SQL Server问题处理和性能调优的基本,本篇文章讲述SQL Server对于内存管理的内存原理. 二级存储(secondary storage) ...

  4. Template、ItemsPanel、ItemContainerStyle、ItemTemplate (部分内容有待验证)

    以下摘自“CSDN”的某人博客,部分内容有待验证,需注意“辨别学之....” 1.Template是指控件的样式 在WPF中所有继承自contentcontrol类的控件都含有此属性,(继承自Fram ...

  5. Spring框架中,在工具类或者普通Java类中调用service或dao

    spring注解的作用: 1.spring作用在类上的注解有@Component.@Responsity.@Service以及@Controller:而@Autowired和@Resource是用来修 ...

  6. iOS JavaScriptCore使用

    iOS JavaScriptCore使用 JavaScriptCore是iOS7引入的新功能,JavaScriptCore可以理解为一个浏览器的运行内核,使用JavaScriptCore可以使用nat ...

  7. Highcharts 测量图;Highcharts 圆形进度条式测量图;Highcharts 时钟;Highcharts 双轴车速表;Highcharts 音量表(VU Meter)

    Highcharts 测量图 配置 chart.type 配置 配置 chart 的 type 为 'gauge' .chart.type 描述了图表类型.默认值为 "line". ...

  8. 016PHP基础知识——流程控制(四)

    <?php /** * 流程控制(四) do...while * do{ 代码段 * }while(){ * } * 特点:最少会执行一次代码段 */ /*$i=5; do{ echo $i; ...

  9. java中容器的学习与理解

    以前一直对于java中容器的概念不理解,虽然学习过,但始终没有认真理解过,这几天老师提出了这样一个问题,你怎么理解java中的容器.瞬间就蒙了.于是各种搜资料学习了一下,下面是我学习后整理出来的的一些 ...

  10. maven_01_简介及安装

    一.简介 Maven主要服务于基于Java平台的项目构建.依赖管理和项目信息管理 何为构建 除了编写源代码,我们每天有相当一部分时间花在了编译.运行单元测试.生成文档.打包和部署等烦琐且不起眼的工作上 ...