Note: phiên bản Tiếng Việt của bài này ở link dưới.

https://duongnt.com/dynamic-code-generation-vie

After writing a previous article about FastMember, I was inspired to learn more about dynamic code generation. It is said that getting into the realm of meta-programming with tools like Expression or ILGenerator is like talking with Cthulhu. But at least for me, the journey was fun.

In today’s article, we will compare the performance of dynamic code generation and reflection in two simple use cases; creating an instance of a class and invoking its methods (Spoiler: code generation is much faster). You can download the sample code from the link below.

https://github.com/duongntbk/DynamicCodeGenerationSample

Our final goal

As we know, we can use the Reflection API to dynamically create an instance of a class like this.

var assemblyPath = "<Path to dll file>";
var assembly = Assembly.LoadFrom(assemblyPath);

var className = "<Class name>";
var type = assembly.GetType(typeName);
var instance = Activator.CreateInstance(type); // The target class must have a parameterless constructor

And we can call a method on that instance like this.

var methodName = "<Method name>"
var methodInfo = type.GetMethod(methodName);

var arguments = new object[]
{
    // list of method arguments
}
var result = methodInfo.Invoke(instance, arguments); // result is of type object

Or for a method that returns void.

methodInfo.Invoke(instance, arguments);

Note that GetMethod only works if our method is not overloaded.

The code above works fine if we only use them occasionally, but Activator.CreateInstance and methodInfo.Invoke are quite slow. By using dynamic code generation, we will generate the necessary IL code by ourselves and hopefully get better performance.

Find out what IL code we need

To make it clear, we don’t want to regenerate all IL (Intermediate Language) code from scratch, that would be madness. Instead, we will find out what IL code the compiler generates and try to replicate that. Our test class is below.

public class TestClass
{
    private string _firstName = "Default first name";
    private string _lastName = "Default last name";

    public void SetName(string firstName, string lastName)
    {
        _firstName = firstName;
        _lastName = lastName;
    }

    public string GetLastName() => _lastName;

    public string Introduce(string first, string last) => $"The name is {last}, {first} {last}.";
}

Next, we will define the following method in our main program.

// Since we will create our class dynamically,
// the return type is `object` instead of `TestClass`
public static object CreateClass() => new TestClass();

Then we will find the IL code of the method above. There are many tools to get IL code, but today we will use ildasm, which comes with Visual Studio.

  • Build our code to generate a dll file.
  • From Visual Studio, open the Developer Command Prompt.
  • Type ildasm then press Enter.
  • From the GUI of ildasm, click Open, navigate to our debug folder then select the dll file above.
  • In the navigation tree, find our CreateClass method and double-click it. You should then see this screen.

Create instance with dynamic code generation

We will create a delegate so that we can reuse it when we want to create an instance. Because in today’s article, we only care about classes which have a parameterless constructor, our delegate will have type Func<object>. The first step is to load the assembly and get the type info, this is similar to when we were using the Reflection API.

var assembly = Assembly.LoadFrom(assemblyPath);
var type = assembly.GetType(typeName);

Next, let’s take a look at our IL code. The important path is this.

IL_0000:  newobj     instance void DynamicCodeGenerationSample.TestClass::.ctor()
IL_0005:  ret

The IL code here is actually very simple. It’s just a newobj instruction (allocate an uninitialized object and call ctor) and a ret (return from method) instruction. You can check the list of IL instructions here. Because we want to create a delegate, we need to call a method called DynamicMethod in the System.Reflection.Emit namespace.

var dynCtor = new DynamicMethod($"{type.FullName}_ctor", type, Type.EmptyTypes, true);

The meaning of each parameter is below.

  • $"{type.FullName}_ctor": this is the name of our delegate. We can name it however we want, but it’s best to use a descriptive name.
  • type: the return type of our delegate, which is TestClass in our case. We retrieved it earlier from the assembly using its type name. Similar to the Reflection API, we actually return a TestClass instance here.
  • Type.EmptyTypes: the list of parameters for our delegate. Because our delegate takes no parameter, we use Type.EmptyTypes here.
  • true: setting this flag to true will skip the visibility check on types and members.

We also need an IL generator.

var il = dynCtor.GetILGenerator();

Because our TestClass class only has a parameterless constructor, we use Type.EmptyTypes again when retrieving constructor info.

var ctorInfo = type.GetConstructor(Type.EmptyTypes);

The next step is to emit the two IL instructions newobj and ret (we may want to check that ctorInfo is not null first).

il.Emit(OpCodes.Newobj, ctorInfo); // remember to pass ctorInfo here
il.Emit(OpCodes.Ret);

The last step is to create our delegate and cast it to the correct type (which is Func<object>).

var ctorDelegate = (Func<object>)dynCtor.CreateDelegate(typeof(Func<object>));

We can then use ctorDelegate when we want to create an instance of TestClass.

var instance = ctorDelegate();

The type of instance is object just like when we call Activator.CreateInstance, but we actually have a TestClass object.

Call a parameterless method that returns a string

The first method we try to call is GetLastName. The IL code to call that method is the same with the method below.

// To be consistent with Reflection code, we will call GetLastName on an instance of type object
public static string CallGetFirstName(object instance) => ((TestClass)instance).GetLastName();

With ildasm, we can see that the IL code looks like this.

IL_0000:  ldarg.0
IL_0001:  castclass  DynamicCodeGenerationSample.TestClass
IL_0006:  callvirt   instance string DynamicCodeGenerationSample.TestClass::GetLastName()
IL_000b:  ret

We will create a delegate with type Func<object, string>, the object parameter is actually an instance of type TestClass. When we call a non-static method, the current instance is automatically passed to that method as the first argument. Similar to the previous section, the first step is to load the assembly and get the type info. And because we are calling a method, we need the MethodInfo as well.

var assembly = Assembly.LoadFrom(assemblyPath);
var type = assembly.GetType(typeName);
var methodInfo = type.GetMethod("GetLastName");

The code to create the DynamicMethod object and the IL generator is below.

var dynInk = new DynamicMethod($"{type.FullName}_{methodInfo.Name}_Ink", typeof(string), new[] { typeof(object) }, true);
var il = dynInk.GetILGenerator();

Some noticeable parameters are below.

  • typeof(string): because the return type of our delegate is string.
  • new[] { typeof(object) }: because our delegate only has one parameter of type object.

The first instruction we need to emit is ldarg.0. This instruction will load instance, which is the first argument, on the stack.

il.Emit(OpCodes.Ldarg_0);

Next, we cast instance to TestClass type.

il.Emit(OpCodes.Castclass, type);

In the IL code snippet above, we can see that the next instruction is callvirt. But because we know that GetLastName is not a virtual method, we can replace it with a call instruction.

il.EmitCall(OpCodes.Call, methodInfo, null); // remember to pass methodInfo here

The last instruction is a ret to return our result.

il.Emit(OpCodes.Ret);

Just like in the previous section, we create our delegate and cast it to the correct type (which is Func<object, string>).

var inkDelegate = (Func<object, string>)dynInk.CreateDelegate(typeof(Func<object, string>));

We can then use the delegate above to call GetLastName on an instance of type TestClass.

var instance = ctorDelegate();
var lastName = inkDelegate(instance);
// lastName is instance._lastName

Call a method with two parameters that returns void

Our next target is SetName. Because we need to pass both the first and last name but there is no return value, our delegate looks like this: Action<object, string, string>. We use a helper method to find the necessary IL code.

public static void CallSetName(object instance, string first, string last) =>
    ((TestClass)instance).SetName(first, last);

The IL code is below.

IL_0000:  ldarg.0
IL_0001:  castclass  DynamicCodeGenerationSample.TestClass
IL_0006:  ldarg.1
IL_0007:  ldarg.2
IL_0008:  callvirt   instance void DynamicCodeGenerationSample.TestClass::SetName(string,
                                                                                  string)
IL_000d:  nop
IL_000e:  ret

We still need to load the assembly, get type info and get method info just like in the previous section, I will omit that part for brevity. The step to create the DynamicMethod object and the IL generator is a bit different though.

var parameterTypes = new[]
{
    typeof(object),
    typeof(string),
    typeof(string)
};
var dynInk = new DynamicMethod($"{type.FullName}_{method.Name}_Ink", null, parameterTypes, true);
var il = dynInk.GetILGenerator();

Because our delegate is a Action<object, string, string>, we made the following change.

  • null: our delegate doesn’t return anything, the return type is null.
  • parameterTypes: this is an array of Type with three elements to match <object, string, string>.

The step to load the instance parameter and cast it to TestClass type stays the same.

il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Castclass, type);

The next step is to emit ldarg.1 and ldarg.2 to load the next two parameters (the two string) on the stack. And just like in the previous section, we can replace the callvirt instruction with a call instruction.

il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.EmitCall(OpCodes.Call, methodInfo, null); // remember to pass methodInfo here

The last two instructions are nop (do nothing) and ret. Of course, we won’t bother emitting a nop instruction, so only the ret instruction is left.

il.Emit(OpCodes.Ret);

We can then create and use our delegate.

var inkDelegate = (Action<object, string, string>)dynInk.CreateDelegate(typeof(Action<object, string, string>));

var instance = ctorDelegate();
inkDelegate(instance, "new first name", "new last name");
// instance._firstName is now "new first name",
// and instance._lastName is "new last name"

Call a method with two parameters that returns a string

Our last target is Introduce, but for this method we will mimic the Reflection API. Recall that when we use methodInfo.Invoke, we pass all arguments together as an object array, and the return value is also an object. We will do the same and create a delegate with the following type Func<object, object[], object>.

These are our arguments as an object array.

var arguments = new object[] { "James", "Bond" }

The code to create the DynamicMethod object and the IL generator now becomes this.

var parameterTypes = new[] { typeof(object), typeof(object[]) };
var dynInk = new DynamicMethod($"{type.FullName}_{methodInfo.Name}_Ink", typeof(object), parameterTypes, true);
var il = dynInk.GetILGenerator();

The helper method to find the necessary IL code is below.

public static object CallIntroduce(object instance, object[] arguments) =>
    ((TestClass)instance).Introduce((string)arguments[0], (string)arguments[1]);

Which produces the following IL code.

IL_0000:  ldarg.0
IL_0001:  castclass  DynamicCodeGenerationSample.TestClass
IL_0006:  ldarg.1
IL_0007:  ldc.i4.0
IL_0008:  ldelem.ref
IL_0009:  castclass  [System.Runtime]System.String
IL_000e:  ldarg.1
IL_000f:  ldc.i4.1
IL_0010:  ldelem.ref
IL_0011:  castclass  [System.Runtime]System.String
IL_0016:  callvirt   instance string DynamicCodeGenerationSample.TestClass::Introduce(string,
                                                                                      string)
IL_001b:  ret

Compared to the IL code in the previous two sections, we can see a few differences.

  • There are three more instructions to read elements from the object array. ldc.i4.0 and ldc.i4.1 will push the number 0 and 1 on the stack, and ldelem.ref will use the number on top of the stack as index to load the corresponding element from the array.
  • Because the array is of type object but Introduce has two string parameters, we need to cast each argument from object to string. This is done by two additional castclass instructions.

From the IL code above, it’s tempting to write something like this.

il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Castclass, type);

il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Ldelem_Ref);
il.Emit(OpCodes.Castclass, typeof(string));
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Ldelem_Ref);
il.Emit(OpCodes.Castclass, typeof(string));

And that code will indeed work. However, that means we’ve hard-coded our delegate to call a method with two parameters of type string. If so, why bother passing arguments as object[]? Instead, we will use a loop to handle each argument in the object array like this.

il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Castclass, type);

var methodParameters = methodInfo.GetParameters();
var paramLen = arguments.Length;
for (var i = 0; i < paramLen; i++)
{
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Ldc_I4_S, i);
    il.Emit(OpCodes.Ldelem_Ref);
    il.Emit(OpCodes.Castclass, methodParameters[i].ParameterType);
}

Notice that instead of ldc.i4.0 and ldc.i4.1, we use ldc.i4.s and an argument to push array indexes on the stack. Also, we use methodInfo.GetParameters().ParameterType to retrieve the type of method parameters instead of hard-coding them to string.

Then we can emit the other instructions as usual.

il.EmitCall(OpCodes.Call, methodInfo, null);
il.Emit(OpCodes.Ret);

All that’s left to do is create and use our delegate.

var inkDelegate = (Func<object, object[], object>)dynInk.CreateDelegate(typeof(Func<object, object[], object>));

var instance = ctorDelegate();
var result = inkDelegate(instance, arguments);
Console.WriteLine(result) // Print "The name is Bond, James Bond."

Value type and boxing

Our implementation to call Introduce method above only works if the return value is a reference type. To see the problem if the return value is a value type, let’s modify Introduce method a little.

public DateTime Introduce(string first, string last) => DateTime.Now;

Now when we execute inkDelegate(instance, arguments), the following error will occur.

Fatal error. Internal CLR error. (0x80131506)

The reason is because we are trying to return a DateTime, which is a value type, as an object, a reference type. Normally, the compiler sees this and performs a boxing to "box" that DateTime into an object, but when building the IL code, we did not perform this step. To fix this problem, we need to add a box instruction before ret.

if(methodInfo.ReturnType.IsValueType)
{
    il.Emit(OpCodes.Box, methodInfo.ReturnType);
}

With this check, we can handle methods which return a value type as well as methods which return a reference type.

Some benchmarks

In the sample code, I included a benchmark project to compare the performance of dynamic code generation and the Reflection API. You can find it at the following link.

https://github.com/duongntbk/DynamicCodeGenerationSample

This is the result of object creation.

Method Mean Error StdDev Median Gen 0 Gen 1 Gen 2 Allocated
Reflection_CreateInstance 37.999 ns 0.8597 ns 1.364 ns 37.8111 ns 0.0051 32 B
DynamicGenerator_CreateInstance 9.297 ns 1.0219 ns 2.849 ns 8.1900 ns 0.0051 32 B
Static_CreateInstance 8.955 ns 0.4365 ns 1.259 ns 8.6350 ns 0.0051 32 B

And this is the result of method invocation. Note that I only tested the Introduce method.

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
Reflection_Invoke 302.55 ns 5.654 ns 4.721 ns 0.0329 208 B
DynamicGenerator_Invoke 99.21 ns 2.107 ns 5.838 ns 0.0267 168 B
Static_Invoke 98.71 ns 3.990 ns 10.989 ns 0.0267 168 B

We can see that in both cases, dynamic code generation is much faster than the Reflection API and is comparable to static C# code.

Conclusion

Dynamic code generation has much better performance than the Reflection API, yet it can keep a similar syntax. I’ve developed this idea and created a library to dynamically create objects and invoke methods. Please check out the FastAndFaster library here.

A software developer from Vietnam and is currently living in Japan.

One Thought on “Dynamic code generation in C#”

Leave a Reply