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 isTestClass
in our case. We retrieved it earlier from the assembly using its type name. Similar to the Reflection API, we actually return aTestClass
instance here.Type.EmptyTypes
: the list of parameters for our delegate. Because our delegate takes no parameter, we useType.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 isstring
.new[] { typeof(object) }
: because our delegate only has one parameter of typeobject
.
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 ofType
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
andldc.i4.1
will push the number0
and1
on the stack, andldelem.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
butIntroduce
has twostring
parameters, we need to cast each argument fromobject
tostring
. This is done by two additionalcastclass
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.
One Thought on “Dynamic code generation in C#”