Friday, May 15, 2009

Strong Typed, High Performance Reflection with C# Delegate (Part II)

Update: Open source project SharpCut delivers a less than 50K library which does what described in this series plus much more. Check it out.

Content

  1. Inspiration: C#.Net Calling Grandparent's Virtual Method (base.base in C#)
  2. Prototype: Strong Typed, High Performance Reflection with C# Delegate (Part I)
  3. Performance: Strong Typed, High Performance Reflection with C# Delegate (Part II) <= you are here
  4. Library Usage: Strong Typed, High Performance Reflection with C# Delegate (Part III)

In the Part I, I completed a prototype of extension method that creates a Delegate to make non-virtual invoke of otherwise virtual method. This prototype had since evolved into a extension method library that gets you a Delegate to any method on a given Type object or any object instance. In this post, We'll compare the performance of direct virtual method call, delegate call and reflection invocation using the Invoke method.

Test Setup

The source used for the performance test is Program.cs which forms a simple console application. The performance is measured by calling a virtual method that doesn't nothing but return a literal integer value. The method take two parameters, one reference type and another is value type, and returns a value type. The virtual method then got overridden in the sub class.

        private class Base
        {
            public virtual int PerfTest(int i, object o) { return 0; }
        }

        private class Sub : Base
        {
            public override int PerfTest(int i, object o) { return 1; }
        }

To illustrate how different types of call are made in the test, let's use pseudo code for clarity. Please see the actual source code for the detail.

Direct Virtual Call

Makes the call to a instance of Sub class with a reference type of Base class.

            Base sub = new Sub();
            DateTime start = DateTime.Now;
            for (int i = loop; i > 0; i--) sub.PerfTest(0, o);
Regular Delegate

Create a Delegate from the method Base.PerfTest on an instance of Sub.

            Base sub = new Sub();
            Func<int, object, int> callDelegate = sub.PerfTest;
            DateTime start = DateTime.Now;
            for (int i = loop; i > 0; i--) callDelegate(1, o);
MethodInfo.Invoke

Obtains a MethodInfo object from the Sub type and calls Invoke method on an instance of Sub.

            Base sub = new Sub();
            MethodInfo methodInfo = typeof(Base).GetMethod(methodName);
            DateTime start = DateTime.Now;
            for (int i = loop/1000; i > 0; i--)
                methodInfo.Invoke(sub, new object[] {1, o});
MethodInfo Delegate

Create a Delegate out of a reflected method from Sub type using the extension method in CommonReflection library. Then call the Delegate.

            var callDelegate = new Sub().GetInstanceInvokerOrFail<Func<int, object, int>>(methodName);
            DateTime start = DateTime.Now;
            for (int i = loop; i > 0; i--) callDelegate(1, o);
DynamicMethod.Invoke

Create a DynamicMethod that performs non-virtual invocation to a virtual method on Base type. Then calls the Invoke method on an instance of Sub type.

            Base sub = new Sub();
            DynamicMethod dynamicMethod = Reflections.CreateDynamicMethod(typeof(Base).GetMethod(methodName));
            DateTime start = DateTime.Now;
            for (int i = loop/1000; i > 0; i--)
                dynamicMethod.Invoke(null, new object[] {sub, 1, o});
DynamicMethod Delegate

Create a Delegate our of a DynamicMethod that performs non-virtual invocation to a virtual method on Base type using the extension method in the CommonReflection library. Then call the Delegate on an instance of Sub type.

            var callDelegate = new Sub().GetNonVirtualInvoker<Func<int, object, int>>(typeof(Base), methodName);
            DateTime start = DateTime.Now;
            for (int i = loop; i > 0; i--) callDelegate(1, o);

Performance Test Result

Same test is repeated twice to avoid any warm up effect. And I tested both Debug build and Release build. The result shows nanoseconds per call, which is calculated by calling the method millions of time in a loop to get the total time, then divide the total time by the number of calls to get the per call time.

The Debug build ran on my laptop with Intel Core 2 Due T7300 2GHz and DDR2 5300 RAM yielded this result:

===== First  Round =====
Direct Virtual Call   :      8.281ns
Regular Delegate      :      8.125ns
MethodInfo.Invoke     :  5,468.750ns
MethodInfo Delegate   :      7.969ns
DynamicMethod.Invoke  :  5,468.750ns
DynamicMethod Delegate:     14.844ns
===== Second Round =====
Direct Virtual Call   :      7.969ns
Regular Delegate      :      7.656ns
MethodInfo.Invoke     :  5,468.750ns
MethodInfo Delegate   :      7.813ns
DynamicMethod.Invoke  :  5,468.750ns
DynamicMethod Delegate:     14.844ns

And the Release build test result is

===== First  Round =====
Direct Virtual Call   :      3.594ns
Regular Delegate      :      2.813ns
MethodInfo.Invoke     :  5,468.750ns
MethodInfo Delegate   :      2.813ns
DynamicMethod.Invoke  :  5,625.000ns
DynamicMethod Delegate:      3.750ns
===== Second Round =====
Direct Virtual Call   :      2.813ns
Regular Delegate      :      2.969ns
MethodInfo.Invoke     :  5,312.500ns
MethodInfo Delegate   :      2.656ns
DynamicMethod.Invoke  :  5,625.000ns
DynamicMethod Delegate:      3.594ns

Conclusion

  • The performance of Delegate call is as fast as regular method call. This is quite different from what I learned before in an MSDN article. (Update 5/18: it is confirmed. See on wikipedia and Jon Skeet's blog post
  • The Delegate created from the reflection is as fast as regular method call.
  • The reflection call of MethodInfo.Invoke and DynamicMethod.Invoke is 1500-2000 times slower.

In next post, I'll explain the use of each method in the extension library named "CommonReflection" that you can download its binary here.

7 comments:

DotNetWise said...

You rock!
Nice and clean, Fast and quick! :)

Felix said...

Yeah, but how long does it take to create the delegate from a method name using reflection? If you add that time in I bet the performance difference between calling from a reflected delegate and using invoke disappears.

Kenneth Xu said...

@Felix, you are correct literally but that was not the point. If you get the same MethodInfo again and again every time you invoke, then you need to go through the first step of performance tuning. You should get the MethodInfo once and cache it. Once you have completed the first step, the bottle neck is now just the invocation. That's the what we are talking about here.

Baloney said...

Hi and thanks for this excellent information.

I came across it while researching the question of whether having my base class invoke a Func supplied by the user of the class would be faster than invoking an equivalent overridden virtual method provided by a derived type.

If you have any insight into that, it would be greately appreciated.

Kenneth Xu said...

@Baloney, that will be same as the comparison between "Direct Virtual Call" vs. "Regular Delegate" that I have listed. I would never consider such subtle performance different in my design decision. There are many other good reasons to choose one over the other.

Anonymous said...

Oh wuuut, delegate is not as fast, it is faster than direct call except dynamic delegate @o@. This is amazing.

This is such a late post, but, do you think there would be any pitfall if I use typedDelegate[] indexed by int enum, instead of switches? Or use List as execution pipeline?

I like the flexibility of such design pattern, but, I am unsure if there is a performance tradeoff to it. Or is there any better pattern than this?

Kenneth Xu said...

@Anonymous
I'm not sure if I understood your question. Direct virtual call is different from calling a non-virtual method. I didn't measure but I would expect direct non-virtual method call will be same or faster (due to possible inline) the delegate. In most of use cases, you should not think about performance when you choose between delegate, virtual or non-virtual methods. The other design concerns, for example encapsulation, extensibility, single responsibility and testability, are more important then performance.

Post a Comment