In the world of C#, dynamically invoking methods can be quite a challenge, especially when dealing with generic methods where the type parameters are unknown at compile time. Although this topic is quite old, it remains relevant today due to the different approaches available for achieving dynamic method invocation. In this post, we’ll delve into three primary methods: reflection, expression trees, and the use of IL emit, comparing their performance and usability.
Understanding the Options
DotNet provides 3 options to invoke methods dynamically. Literally:
Reflection: Utilizes the MethodInfo class to invoke methods dynamically.
Expression Trees: Uses expression trees to compile a delegate for method invocation.
Emit: Utilizes the ILGenerator to create dynamic methods at runtime.
Historically, reflection has been viewed as the simplest but the slowest option among all available methods, while emit tends to be the fastest. To compare real performances in modern C#, I created three test cases using a sample class with various methods: TestReflection, TestExpression, and TestEmit.
Here’s a simple version of the class that contains generic methods for testing:
public class Sample
{
public void TestDirectCall(Type type)
{
GenericMethod<string>();
GenericMethodWithArg<string>(42);
StaticMethod<string>();
StaticMethodWithArg<string>(6);
}
public void TestReflection(Type type)
{
CallViaReflection.CallGenericMethod(this, type);
CallViaReflection.CallGenericMethod(this, type, 42);
CallViaReflection.CallStaticMethod(type);
CallViaReflection.CallStaticMethod(type, 6);
}
public void TestExpression(Type type)
{
CallViaExpression.CallGenericMethod(this, type);
CallViaExpression.CallGenericMethod(this, type, 42);
CallViaExpression.CallStaticMethod(type);
CallViaExpression.CallStaticMethod(type, 6);
}
public void TestEmit(Type type)
{
CallViaEmit.CallGenericMethod(this, type);
CallViaEmit.CallGenericMethod(this, type, 42);
CallViaEmit.CallStaticMethod(type);
CallViaEmit.CallStaticMethod(type, 6);
}
public void T()
{
StaticMethod<string>();
}
public void GenericMethod<T>()
{
}
public void GenericMethodWithArg<T>(int someValue)
{
}
public static void StaticMethod<T>()
{
}
public static void StaticMethodWithArg<T>(int someValue)
{
}
}Call Methods Dynamically via Reflection
Reflection utilizes the MethodInfo class to invoke methods dynamically. A caching mechanism improves performance by storing method references in a private variable.
public static class CallViaReflection
{
private static readonly Cache<MethodInfo> cache = new();
public static void CallGenericMethod(Sample sample, Type genericType)
{
var callDelegate = GetDelegate(
nameof(Sample.GenericMethod),
BindingFlags.Instance | BindingFlags.Public,
genericType);
callDelegate.Invoke(sample, null);
}
public static void CallGenericMethod(Sample sample, Type genericType, int someValue)
{
var callDelegate = GetDelegate(
nameof(Sample.GenericMethodWithArg),
BindingFlags.Instance | BindingFlags.Public,
genericType,
typeof(int));
callDelegate.Invoke(sample, new object[] { someValue });
}
public static void CallStaticMethod(Type genericType)
{
var callDelegate = GetDelegate(
nameof(Sample.StaticMethod),
BindingFlags.Static | BindingFlags.Public,
genericType);
callDelegate.Invoke(null, null);
}
public static void CallStaticMethod(Type genericType, int someValue)
{
var callDelegate = GetDelegate(
nameof(Sample.StaticMethodWithArg),
BindingFlags.Static | BindingFlags.Public,
genericType,
typeof(int));
callDelegate.Invoke(null, new object[] { someValue });
}
private static MethodInfo GetDelegate(
string methodName, BindingFlags bindingFlags,
Type genericType, params Type[] arguments)
{
if (cache.TryGet(methodName, genericType, out var concreteMethodInfo))
return concreteMethodInfo;
var sampleType = typeof(Sample);
MethodInfo genericMethodInfo = sampleType.GetMethod(methodName, bindingFlags)!;
concreteMethodInfo = genericMethodInfo.MakeGenericMethod(genericType);
cache.Add(methodName, genericType, concreteMethodInfo);
return concreteMethodInfo;
}
}Call Methods Dynamically via Expression Trees
Expression trees compile a delegate for method invocation. This approach is often harder than reflection, because of the following
- Expression trees represent code as data, allowing for more complex scenarios like dynamic composition of code. This requires a solid understanding of how expressions are structured and manipulated.
- You need to manually build the expression tree by defining parameters, method calls, and handling various expressions. This often involves more boilerplate code.
- While expression trees can be compiled to delegates, understanding when and how to do this effectively adds complexity. Developers must consider factors like caching delegates for performance.
Here’s a class that builds expression trees for calling methods dynamically. Pay attention on the cache variable. Caching is important here; otherwise, performance will slow down because of reflection under hood.
public static class CallViaReflection
{
private static readonly Cache<MethodInfo> cache = new();
public static void CallGenericMethod(Sample sample, Type genericType)
{
var callDelegate = GetDelegate(nameof(Sample.GenericMethod), BindingFlags.Instance | BindingFlags.Public, genericType);
callDelegate.Invoke(sample, null);
}
public static void CallGenericMethod(Sample sample, Type genericType, int someValue)
{
var callDelegate = GetDelegate(nameof(Sample.GenericMethodWithArg), BindingFlags.Instance | BindingFlags.Public, genericType, typeof(int));
callDelegate.Invoke(sample, new object[] { someValue });
}
public static void CallStaticMethod(Type genericType)
{
var callDelegate = GetDelegate(nameof(Sample.StaticMethod), BindingFlags.Static | BindingFlags.Public, genericType);
callDelegate.Invoke(null, null);
}
public static void CallStaticMethod(Type genericType, int someValue)
{
var callDelegate = GetDelegate(nameof(Sample.StaticMethodWithArg), BindingFlags.Static | BindingFlags.Public, genericType, typeof(int));
callDelegate.Invoke(null, new object[] { someValue });
}
private static MethodInfo GetDelegate(string methodName, BindingFlags bindingFlags, Type genericType, params Type[] arguments)
{
if (cache.TryGet(methodName, genericType, out var concreteMethodInfo))
return concreteMethodInfo;
var sampleType = typeof(Sample);
MethodInfo genericMethodInfo = sampleType.GetMethod(methodName, bindingFlags)!;
concreteMethodInfo = genericMethodInfo.MakeGenericMethod(genericType);
cache.Add(methodName, genericType, concreteMethodInfo);
return concreteMethodInfo;
}
}Call Methods Dynamically via Emit
This method utilizes IL emit for the fastest method invocation. This approach is always the most complex comparing with the previous at first glance. However, I would recommend using ildasm to disassemble all necessary MSIL instruction, in order to write correct IL instructions.
public static class CallViaEmit
{
private static readonly Cache<Delegate> cache = new();
public static void CallGenericMethod(this Sample sample, Type genericType)
{
var callDelegate = GetDynamicMethod(
nameof(Sample.GenericMethod),
BindingFlags.Instance | BindingFlags.Public,
genericType);
((Action<Sample>)callDelegate).Invoke(sample);
}
public static void CallGenericMethod(this Sample sample, Type genericType, int someValue)
{
var callDelegate = GetDynamicMethod(
nameof(Sample.GenericMethodWithArg),
BindingFlags.Instance | BindingFlags.Public,
genericType);
((Action<Sample, int>)callDelegate).Invoke(sample, someValue);
}
public static void CallStaticMethod(Type genericType)
{
var callDelegate = GetDynamicMethod(nameof(Sample.StaticMethod), BindingFlags.Static | BindingFlags.Public, genericType);
((Action)callDelegate).Invoke();
}
public static void CallStaticMethod(Type genericType, int someValue)
{
var callDelegate = GetDynamicMethod(
nameof(Sample.StaticMethodWithArg),
BindingFlags.Static | BindingFlags.Public,
genericType);
((Action<int>)callDelegate).Invoke(someValue);
}
private static Delegate GetDynamicMethod(string methodName, BindingFlags bindingFlags, Type genericType)
{
if (cache.TryGet(methodName, genericType, out var callDelegate))
return callDelegate;
var genericMethodInfo = typeof(Sample).GetMethod(methodName, bindingFlags)!;
var concreteMethodInfo = genericMethodInfo.MakeGenericMethod(genericType);
var dynamicMethod = new DynamicMethod("DynamicCall", null, new[] { typeof(Sample) });
var il = dynamicMethod.GetILGenerator();
il.Emit(OpCodes.Ldarg_0); // Load 'sample' onto the stack
il.EmitCall(concreteMethodInfo.IsStatic ? OpCodes.Call : OpCodes.Callvirt, concreteMethodInfo, null);
il.Emit(OpCodes.Ret);
callDelegate = dynamicMethod.CreateDelegate(typeof(Action<Sample>));
cache.Add(methodName, genericType, callDelegate);
return callDelegate;
}
}
Let’s Test!
It’s time to run real tests. Here’s my test class.
[TestFixture]
public class SampleTests
{
private const int Iterations = 10000000;
[Test]
public void TestDirectCall()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestDirectCall(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods directly took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
[Test]
public void TestReflection()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestReflection(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods dynamically via " +
"reflection took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
[Test]
public void TestExpressionTree()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestExpression(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods dynamically via " +
"expression tree took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
[Test]
public void TestEmit()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestEmit(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods dynamically via " +
"emit took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
}Results
After running the tests, here are the results on my local machine:
- Emit: 2939 milliseconds
- Expression Trees: 3910 milliseconds
- Reflection: 6381 milliseconds
The results clearly show that emit remains the most efficient method for dynamic invocation. Expression trees are twice faster than reflection.
Conclusion
The choice of method depends on your specific needs:
- If you need to simply call a method and do not care about performance, reflection is a way to go, because it’s simple and straightforward.
- If you require simplicity and decent performance, expression trees is a great choice, that provides a perfect balance between complexity and performance.
- For performance-critical applications, emitting IL is the way to go, despite the complexity it introduces.
For further exploration, the complete source code can be found on my GitHub.
Leave a Reply