C#的System.Reflection.Emit教程 - 显著改善反射性能

第一部分:Emit介绍

Emit 是开发者在掌握反射的使用后,进阶需要的知识,它能显著的改善因反射带来的性能影响

Emit 是一种动态生成IL代码的技术,通过使用 System.Reflection.Emit 命名空间中的类,可以在运行时创建和修改程序集、类型和方法。

Emit 技术通常用于解决需要在运行时动态生成代码的特定场景,例如在ORM(对象关系映射)框架中动态创建实体类,或者在AOP(面向切面编程)中动态创建代理类。

使用 Emit 技术的步骤通常包括以下几个步骤:
1、创建一个动态程序集(AssemblyBuilder)。
2、在程序集中创建一个动态模块(ModuleBuilder)。
3、在模块中创建一个动态类型(TypeBuilder)。
4、在类型中创建动态方法(MethodBuilder)。
5、使用ILGenerator编写实际的IL代码,包括加载、存储和计算等操作。
6、完成IL代码的生成后,使用CreateType方法将动态类型创建为实际的类型。
7、最后,通过反射或创建委托或其他方式,可以在运行时调用动态生成的方法。

更通常情况下,使用创建动态方法更常见,即从第4步开始,使用 DynamicMethod 直接创建动态方法。

第二部分:构建动态程序集(AssemblyBuilder)

程序集是.NET中的基本部署单位,它包含了可执行代码、资源、元数据等信息,是.NET应用程序的基本组成单元之一。

静态程序集(即程序集持久化)不同,动态程序集是在运行时生成的,使得我们可以根据需要动态地构建和加载程序集。

在C#中,操作程序集的核心类是:AssemblyBuilder

在这过程,我们可以使用 AssemblyName 来定义程序集的名称和版本等信息。

AssemblyName assemblyName = new AssemblyName("MyDllName") { Version = new Version("1.0.0.0") };

.NET Core 代码:通过 AssemblyBuilder.DefineDynamicAssembly 来创建动态程序集对象:

AssemblyBuilder ab = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);

AssemblyBuilderAccess 在 .Net Core 系列中的属性:

  • AssemblyBuilderAccess.Run 表示程序集可被执行,但不能被保存。  
  • AssemblyBuilderAccess.Collect 表示程序集可以被卸载并且内存会被回收

可以看到,目前版本是不支持持久化功能。

第三部分:构建模块(Module)

模块是动态程序集中的基本单位,它类似于一个独立的代码单元,可以包含类型方法字段等成员。在动态程序集中,模块扮演着组织代码和实现代码复用的关键角色。一个程序集可以包含一个或多个模块,这种模块化的设计有助于提高代码的可维护性和可扩展性。

通过 AssemblyBuilderDefineDynamicModule方法,即可获得 ModuleBuilder(后续章节会通过它,来添加类型、方法、字段等成员到模块中,从而构建出所需的模块结构)。

AssemblyBuilder ab = ......
ModuleBuilder mb = ab.DefineDynamicModule("一个名称");

第四部分:构建类型(Type)

在动态生成代码的过程中,构建类型(Type)是至关重要的一步。通过使用 Emit 中的 TypeBuilder,我们可以定义和创建各种类型,包括类、结构体和接口。

// 定义类型
TypeBuilder tb = mb.DefineType("MyClass", TypeAttributes.Public);
// 定义接口
TypeBuilder tb = mb.DefineType("MyNameSpace.MyInterface", TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Interface);
// 定义结构体
TypeBuilder tb = mb.DefineType("MyNameSpace.MyStruct", TypeAttributes.SequentialLayout | TypeAttributes.Public | TypeAttributes.Sealed, typeof(ValueType));
// 定义枚举
EnumBuilder eb = mb.DefineEnum("MyNameSpace.MyEnum", TypeAttributes.Public, typeof(int));
// 定义抽象类
TypeBuilder tb = mb.DefineType("MyNameSpace.MyClassBase", TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Class);


tb.CreateType();
// 定义字段 
// FieldAttributes.Public| FieldAttributes.Static| FieldAttributes.InitOnly
tb.DefineField("ID", typeof(int), FieldAttributes.Public);
tb.DefineField("Name", typeof(string), FieldAttributes.Public);
// 定义属性 "Name",类型为 int
PropertyBuilder propertyBuilder = tb.DefineProperty("Name", PropertyAttributes.None, typeof(int), null);

// 定义属性的 getter 方法
MethodBuilder getterMethodBuilder = tb.DefineMethod("get_Name", MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual, typeof(int), Type.EmptyTypes);
propertyBuilder.SetGetMethod(getterMethodBuilder);

 // 定义属性的 setter 方法
MethodBuilder setterMethodBuilder = tb.DefineMethod("set_Name", MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual, null, new Type[] { typeof(int) });
propertyBuilder.SetSetMethod(setterMethodBuilder);

//定义方法 GetMyName
MethodBuilder getMyName = tb.DefineMethod("GetMyName", MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual, typeof(string), new Type[] { typeof(int) });

第五部分:动态生成方法(MethodBuilder 与 DynamicMethod)

当我们涉及到在运行时生成和定义方法时,便需要使用到C#中的两个关键类之一:MethodBuilderDynamicMethod

MethodBuilder

如果你需要构建整个类型(包括字段、属性、方法等),那么按第一部分讲的:1-7 依次创建下来, 在第4步就可以使用其创建方法。

特点:

  • 绑定到类型:MethodBuilder 创建的方法是属于某个类型的一部分,因此只能通过该类型的实例或静态引用来调用。
  • 方法签名:需要指定方法名称、参数类型和返回类型。

DynamicMethod

DynamicMethod 则更加灵活。它允许在运行时生成和执行方法,而无需创建动态程序集或动态类型来容纳该方法。

优点:

  • 不绑定到类型:DynamicMethod 不属于特定的类型,因此可以在任何上下文中调用。
  • 性能比MethodBuilder更高
  • 可以直接调用dynamicMethod.CreateDelegate()调用委托

两者的区别:

DynamicMethod:

  • DynamicMethod类允许在运行时生成和执行方法,而无需创建动态程序集或动态类型来容纳该方法。
  • 由即时(JIT)编译器生成的可执行代码在DynamicMethod对象被回收时也会被回收。
  • 动态方法是生成和执行少量代码的最有效方式。
  • 如果需要动态创建一个或多个方法,应使用 DynamicMethod

MethodBuilder:

  • MethodBuilder用于在动态程序集中创建方法。
  • 如果要创建整个类型(包括字段、属性、方法等),则需要先创建一个动态程序集(AssemblyBuilder),然后在其中创建一个模块(ModuleBuilder),最后再创建一个或多个类型(TypeBuilder)。
  • 若要在这些类型中创建方法,可以使用MethodBuilder

总结:

  • 如果是生成动态程序集,包括创建动态类,那么使用 MethodBuilder
  • 如果只是定义动态方法供调用,使用 DynamicMethod,因为它不用定义整个程序集,直接起手就是方法。
  • 使用委托调用方法:MethodBuilderDynamicMethod 都支持,但 DynamicMethod 直接提供 CreateDelegate 方法,方便起手调用。

第五部分:动态生成IL代码(ILGenerator)

通过ILGenerator,开发人员可以在运行时创建和修改方法体内的IL指令,实现动态方法的生成和优化。

在ILGenerator方法中有两种方法

  • 指令方法:il.Emit(OpCodes.Ret), 用于执行IL指令。
  • 辅助方法:il.EmitWriteLine("hello world!")等, 是基于IL指令封装的一些常用方法,方便调用,不过辅助方法目前很少。

第六部分:IL 指令(难点与核心)

工具: 在线运行程序,且可以查看生成的IL代码 https://sharplab.io/

在.NET平台上,IL(Intermediate Language)是一种中间语言。它是由高级语言(如C#、VB.NET等)编译而成的一种低级语言表示形式。IL 代码被保存在 .net 程序集中,并由公共语言运行时(CLR)执行。

C#--编译-->IL--CLR-->机器码

通过学习和理解IL代码,可以更深入地了解.NET程序的内部工作原理,为程序的优化和调试提供帮助。

IL 指令的结构和格式

官方文档中查看IL指令: https://learn.microsoft.com/zh-cn/dotnet/api/system.reflection.emit.opcodes?view=net-8.0

IL 指令由操作码(OpCode)和操作数(Operand)两部分组成。

  • 操作码:表示要执行的操作,如加载数据、执行算术运算、跳转等。
  • 操作数:提供给操作码的参数,用于指定要操作的数据或执行的具体行为。

IL指令通常以字节形式存储在 .net 程序集中,CLR在执行时会逐条解释和执行这些指令。

A、 加载和存储指令示例

  • ldarg.0:将第一个参数加载到栈顶。
  • ldloc.1:将第二个本地变量加载到栈顶。
  • stloc.2:将栈顶的值存储到第三个本地变量中。

B、 算术和逻辑指令示例

  • add:将栈顶两个值相加。
  • mul:将栈顶两个值相乘。
  • and:对栈顶两个值执行按位与操作。

C、 控制流指令示例

  • br label:无条件跳转到标记为label的位置。
  • beq label:如果相等则跳转到标记为label的位置。
  • call method:调用指定的方法。

D、 对象模型指令示例

  • newobj:创建一个新对象实例。
  • ldfld:将对象字段的值加载到栈顶。
  • callvirt method:调用虚方法。

E、 方法调用指令示例

  • call method:调用静态方法。
  • callvirt method:调用实例方法或虚方法。
  • ret:从当前方法返回。

第七部分:实战项目

/// <summary>
/// 设置字段的值, 委托方法
/// </summary>
/// <typeparam name="TObj"></typeparam>
/// <typeparam name="TField"></typeparam>
/// <param name="fieldinfo"></param>
/// <returns></returns>
private static Action<TObj, TValue> CreateSetFieldDelegate<TObj, TValue>(FieldInfo fieldinfo)
{
    // 创建动态方法
    // 动态方法的名称 SetField
    // 一个 Type 对象,用于指定动态方法的返回类型 typeof(void)
    // 指定动态方法的参数类型的 Type 对象数组 [typeof(TObj), typeof(TValue)]
    // 一个 Module,表示动态方法将与之逻辑关联的模块 typeof(Program).Module
    var method = new DynamicMethod("SetField", typeof(void), [typeof(TObj), typeof(TValue)], typeof(Program).Module);

    // 返回一个 IL 生成器,该生成器可用于构造动态方法的主体。
    ILGenerator il = method.GetILGenerator();
    // il.Emit() 将指令放到实时 (JIT) 编译器的 Microsoft 中间语言 (MSIL) 流上。
    il.Emit(OpCodes.Ldarg_0); // 将第一个参数加载到栈顶。
    il.Emit(OpCodes.Ldarg_1); // 将第二个本地变量加载到栈顶。

    // Stfld指令,需要先接收一个对象(指针)Ldarg_0,在接收一个值Ldarg_1,再将Ldarg_0的fieldinfo设置成新的值Ldarg_1
    il.Emit(OpCodes.Stfld, fieldinfo); 

    il.Emit(OpCodes.Ret); // 从当前方法返回,并将返回值(如果存在)从被调用方的计算堆栈推送到调用方的计算堆栈上。

    // 调用CreateDelegate()创建动态方法的委托
    // 该委托方法需要传入两个参数 TObj + TValue
    return (Action<TObj, TValue>)method.CreateDelegate(typeof(Action<TObj, TValue>));
}

// 使用
public MyClass { int i = 10; }
FieldInfo _field = typeiof(MyClass).getField("i");
Action<MyClass, int> actions = CreateSetFieldDelegate<MyClass, int>(_field); // 生成委托方法,实际actions方法指向的是SetField方法

MyClass _cls = new MyClass();
actions(_cls, 100); // 调用委托方法 _cls.i = 100

第八部分:性能优化与注意事项

扩展阅读

Emit 入门教程系列

此处评论已关闭