It is easy to call a method, just call it directly, everyone knows it. Secondly, you can also use reflection. However, the performance of a reflection call will be much lower than a direct call - at least in terms of absolute time. Although this is a well-known phenomenon, let’s write a program to verify it. For example, we are now creating a new Console application and writing the simplest Call method.
class Program
{
static void Main(string[] args)
{
}
public void Call(object o1, object o2, object o3) { }
}
The Call method accepts three object parameters without any implementation, so we can make the test focus on method calls rather than the method implementation itself. So we started writing test code to compare the performance gap between direct call and reflective call:
static void Main(string[] args)
{
int times = 1000000;
Program program = new Program();
object[] parameters = new object[] { new object(), new object(), new object() };
(null, null, null); // force JIT-compile
Stopwatch watch1 = new Stopwatch();
();
for (int i = 0; i < times; i++)
{
(parameters[0], parameters[1], parameters[2]);
}
();
( + " (Directly invoke)");
MethodInfo methodInfo = typeof(Program).GetMethod("Call");
Stopwatch watch2 = new Stopwatch();
();
for (int i = 0; i < times; i++)
{
(program, parameters);
}
();
( + " (Reflection invoke)");
("Press any key to continue...");
();
}
The execution results are as follows:
00:00:00.0119041 (Directly invoke)
00:00:04.5527141 (Reflection invoke)
Press any key to continue...
Judging from the time taken by each call of one million times, the two have an order of magnitude gap in performance. Therefore, many frameworks will try to use some advanced alternatives to improve performance in scenarios where reflection must be utilized. For example, use CodeDom to generate code and compile dynamically, or use Emit to write IL directly. However, since .NET 3.5 released new features related to Expression, we have developed a more convenient and intuitive solution in the above situations.
Friends who know the related features of Expression may know that an object of type <TDelegate> will get a delegate object of type TDelegate after calling its Compile method, and calling a delegate object is almost the same as directly calling a method. So, for the above situation, what kind of Delegate object should we get? In order for the solution to be generic enough, we must unify the various signature methods into the same delegate type, as follows:
public Func<object, object[], object> GetVoidDelegate()
{
Expression<Action<object, object[]>> exp = (instance, parameters) =>
((Program)instance).Call(parameters[0], parameters[1], parameters[2]);
Action<object, object[]> action = ();
return (instance, parameters) =>
{
action(instance, parameters);
return null;
};
}
As mentioned above, we get a delegate of Func<object, object[], object>, which means it accepts an object type and object[] parameter, and returns a result of an object type - etc. Have you found that this signature is exactly the same as the Invoke method of MethodInfo type? But it is gratifying that the performance of calling this delegation is much higher than that of calling it through reflection. So what about methods with return values? It is more convenient to construct a delegation object:
public int Call(object o1, object o2) { return 0; }
public Func<object, object[], object> GetDelegate()
{
Expression<Func<object, object[], object>> exp = (instance, parameters) =>
((Program)instance).Call(parameters[0], parameters[1]);
return ();
}
At this point, I think friends can easily figure out the delegate construction method for calling static methods. It can be seen that the key to this solution is to construct a suitable Expression<TDelegate>, so we will now write a DynamicExecuter class as a relatively complete solution:
public class DynamicMethodExecutor
{
private Func<object, object[], object> m_execute;
public DynamicMethodExecutor(MethodInfo methodInfo)
{
this.m_execute = (methodInfo);
}
public object Execute(object instance, object[] parameters)
{
return this.m_execute(instance, parameters);
}
private Func<object, object[], object> GetExecuteDelegate(MethodInfo methodInfo)
{
// parameters to execute
ParameterExpression instanceParameter =
(typeof(object), "instance");
ParameterExpression parametersParameter =
(typeof(object[]), "parameters");
// build parameter list
List<Expression> parameterExpressions = new List<Expression>();
ParameterInfo[] paramInfos = ();
for (int i = 0; i < ; i++)
{
// (Ti)parameters[i]
BinaryExpression valueObj = (
parametersParameter, (i));
UnaryExpression valueCast = (
valueObj, paramInfos[i].ParameterType);
(valueCast);
}
// non-instance for static method, or ((TInstance)instance)
Expression instanceCast = ? null :
(instanceParameter, );
// static invoke or ((TInstance)instance).Method
MethodCallExpression methodCall = (
instanceCast, methodInfo, parameterExpressions);
// ((TInstance)instance).Method((T0)parameters[0], (T1)parameters[1], ...)
if ( == typeof(void))
{
Expression<Action<object, object[]>> lambda =
<Action<object, object[]>>(
methodCall, instanceParameter, parametersParameter);
Action<object, object[]> execute = ();
return (instance, parameters) =>
{
execute(instance, parameters);
return null;
};
}
else
{
UnaryExpression castMethodCall = (
methodCall, typeof(object));
Expression<Func<object, object[], object>> lambda =
<Func<object, object[], object>>(
castMethodCall, instanceParameter, parametersParameter);
return ();
}
}
}
The key to DynamicMethodExecutor lies in the logic of constructing Expression Tree in the GetExecuteDelegate method. If you don't know much about the structure of an Expression Tree, you might as well try itExpression Tree VisualizerTo observe and analyze a ready-made Expression Tree. After we pass a MethodInfo object into the constructor of DynamicMethodExecutor, we can pass each group of different instance objects and parameter objects arrays into Execute for execution. This is like using reflection to make calls, but its performance has been significantly improved. For example, we add more test code:
DynamicMethodExecutor executor = new DynamicMethodExecutor(methodInfo);
Stopwatch watch3 = new Stopwatch();
();
for (int i = 0; i < times; i++)
{
(program, parameters);
}
();
( + " (Dynamic executor)");
The current execution result is:
00:00:00.0125539 (Directly invoke)
00:00:04.5349626 (Reflection invoke)
00:00:00.0322555 (Dynamic executor)
Press any key to continue...
In fact, the Compile method of type Expression<TDelegate> uses Emit to generate the delegate object. However, now we no longer need to focus on lower-end ILs. Just use high-end APIs to construct Expression Trees, which is undoubtedly an improvement. However, this method also has certain limitations. For example, we can only call public methods and include out/ref parameters, or other types of members except methods. We cannot write code as comfortable as the above example.
Replenish
Brother Muye quoted Code Project's article "A General Fast Method Invoker" in the comments, in which the FastInvokeHandler delegate object (whose signature is exactly the same as Func<object, object[], object>) is constructed through Emit, which seems to have a higher performance than "method direct" call (although this is not the case from the original example). In fact, the internal implementation of FastInvokeHandler is exactly the same as that of DynamicMethodExecutor, and it is amazing that it has such an incredible performance. I guess the performance advantages of FastInvokeHandler and DynamicMethodExecutor may be reflected in the following aspects:
1. The execution performance of the mode delegation type is slightly lower than that of the non-normal delegation type (prove it).
2. Once an Execute method call is added, some performance is lost.
3. The generated IL code is shorter and more compact.
4. Muye Fox Brother did not use Release mode to compile. :P
I don't know if anyone interested in this can do another test, but please note that such performance tests must be carried out under Release compilation (this point is easy to be ignored), otherwise the significance is not much.
In addition, what I want to emphasize is that this article is a purely technical comparison, not to guide everyone to pursue optimization of performance. Sometimes when I see some articles about comparing the performance of for or foreach, many friends are confused, and even make them blush. I always feel helpless. In fact, theoretically speaking, there are many ways to improve performance. I remember that when I was studying the course Introduction to Computer System in college, I had an assignment to optimize the performance of a C program. I used many methods at that time, such as inline method calls to reduce the number of CPU command calls, adjust the order of loop nesting to improve the CPU cache hit rate, replace some code with embedded ASM, etc., which can be said to be "using everything". Everyone is working hard for the performance improvement of several clock cycles...
That is theory, learning. However, in practical application, we must also treat the theoretical knowledge we have learned correctly. One of the things I often say is: "Any application will have its performance bottlenecks. Only by starting from the performance bottleneck can you achieve twice the result with half the effort." For example, the performance bottlenecks of ordinary web applications are often in external IO (especially database reading and writing). To truly improve performance, you must start from this (such as database tuning, better cache design). Because of this, the key to developing a high-performance web application will not be in the language or language running environment. .NET, RoR, PHP, Java, etc. all perform well in this field.