Back to Home

Investigation of the method call speed in various ways

.NET · DLR · CLR · dynamic · C #

Investigation of the method call speed in various ways

    Result and conclusions for those who do not like long text



    100,000 calls, 20 iterations of the test, x86100,000 calls, 20 iterations of the test, x641,000,000 calls, 10 test iterations, x861,000,000 calls, 10 test iterations, x64
    Direct call
    Min: 1 ms
    Max: 1 ms
    Mean: 1 ms
    Median: 1 ms
    Abs: 1
    

    Min: 1 ms
    Max: 1 ms
    Mean: 1 ms
    Median: 1 ms
    Abs: 1
    

    Min: 7 ms
    Max: 8 ms
    Mean: 7.5 ms
    Median: 7.5 ms
    Abs: 1
    

    Min: 5 ms
    Max: 6 ms
    Mean: 5.2 ms
    Median: 5 ms
    Abs: 1
    

    Call through reflection
    Min: 32 ms
    Max: 36 ms
    Mean: 32.75 ms
    Median: 32.5 ms
    Rel: 32
    

    Min: 35 ms
    Max: 44 ms
    Mean: 36.5 ms
    Median: 36 ms
    Rel: 36
    

    Min: 333 ms
    Max: 399 ms
    Mean: 345.5 ms
    Median: 338 ms
    Rel: 45
    

    Min: 362 ms
    Max: 385 ms
    Mean: 373.6 ms
    Median: 376 ms
    Rel: 75
    

    Delegate Call
    Min: 64 ms
    Max: 71 ms
    Mean: 65.05 ms
    Median: 64.5 ms
    Rel: 64
    

    Min: 72 ms
    Max: 86 ms
    Mean: 75.95 ms
    Median: 75 ms
    Rel: 75
    

    Min: 659 ms
    Max: 730 ms
    Mean: 688.8 ms
    Median: 689.5 ms
    Rel: 92
    

    Min: 746 ms
    Max: 869 ms
    Mean: 773.4 ms
    Median: 765 ms
    Rel: 153
    

    Call through delegate with optimizations
    Min: 16 ms
    Max: 18 ms
    Mean: 16.2 ms
    Median: 16 ms
    Rel: 16
    

    Min: 21 ms
    Max: 25 ms
    Mean: 22.15 ms
    Median: 22 ms
    Rel: 22
    

    Min: 168 ms
    Max: 187 ms
    Mean: 172.8 ms
    Median: 170.5 ms
    Rel: 22.7
    

    Min: 218 ms
    Max: 245 ms
    Mean: 228.8 ms
    Median: 227 ms
    Rel: 45.4
    

    Call through dynamic
    Min: 11 ms
    Max: 14 ms
    Mean: 11.5 ms
    Median: 11 ms
    Rel: 11
    

    Min: 12 ms
    Max: 14 ms
    Mean: 12.5 ms
    Median: 12 ms
    Rel: 12
    

    Min: 124 ms
    Max: 147 ms
    Mean: 132.1 ms
    Median: 130 ms
    Rel: 17
    

    Min: 127 ms
    Max: 144 ms
    Mean: 131.5 ms
    Median: 129.5 ms
    Rel: 26
    

    Call via Expression
    Min: 4 ms
    Max: 4 ms
    Mean: 4 ms
    Median: 4 ms
    Rel: 4
    

    Min: 4 ms
    Max: 5 ms
    Mean: 4.15 ms
    Median: 4 ms
    Rel: 4
    

    Min: 46 ms
    Max: 55 ms
    Mean: 50 ms
    Median: 50.5 ms
    Rel: 6.7
    

    Min: 47 ms
    Max: 51 ms
    Mean: 47.7 ms
    Median: 47 ms
    Rel: 9.4
    



    When using the .NET Framework 3.5, it is best to use method calls through a delegate with call optimization. For the .NET Framework 4.0+, using dynamic is a great choice.
    UPD: a new conclusion from mayorovp : it is best to use Expression

    UPD: and as CdEmON suggested , such a study was published on the hub



    A bit of offtopic, about the reasons for the study
    We will write the following code:
    class SampleGeneric
    {
        public long Process(T obj)
        {
            return String.Format("{0} [{1}]", obj.ToString(), obj.GetType().FullName).Length;
        }
    }
    class Container
    {
        private static Dictionary _instances = new Dictionary();
        public static void Register(SampleGeneric instance)
        {
            if (false == _instances.ContainsKey(typeof(T)))
            {
                _instances.Add(typeof(T), instance);
            }
            else
            {
                _instances[typeof(T)] = instance;
            }
        }
        public static SampleGeneric Get()
        {
            if (false == _instances.ContainsKey(typeof(T))) throw new KeyNotFoundException();
            return (SampleGeneric)_instances[typeof(T)];
        }
        public static object Get(Type type)
        {
            if (false == _instances.ContainsKey(type)) throw new KeyNotFoundException();
            return _instances[type];
        }
    }
    


    Similar code is used quite often, and there is one disadvantage in it - in C # you cannot store a collection of generic types explicitly. All the advice that I found comes down to highlighting the base non-generic class, interface or abstract class, which will be indicated for storage. Those. we get something like this:
    public interface ISampleGeneric { }
    class SampleGeneric : ISampleGeneric
    //
    private static Dictionary _instances = new Dictionary();
    


    In my opinion, it would be convenient to add the ability to write in this way:
    // Ошибка Type expected
    Dictionary>
    

    Especially considering that when making a generic type through reflection, we use a similar construction:
    typeof(SampleGeneric<>).MakeGenericType(typeof(string))
    

    But back to the problem. Now imagine that we need to get a specific instance, but we need to get it in a non-generic method. For example, a method that accepts an object and based on its type must select a handler.
    void NonGenericMethod(object obj)
    {
       var handler = Container.Get(obj.GetType());
    }
    


    We will get the handler, but now the environment will not allow handler.Process (obj) to be written, and if we write, the compiler will swear at the absence of such a method.
    Here, too, there could be a design like from C # developers:
    Container.GetInstance().Process(obj);
    

    , but it doesn’t exist, and a method must be called (although considering Roslyn, can it already be similar in the new IDEs?). There are many ways to do this, of which several main ones can be distinguished. they are listed in the table at the beginning of the article.


    About the code


    Below is the call to the code given in the spoiler. Measurements were removed from the code, measurements were taken through Stopwatch. For analysis, I was interested in relative runtime, and not absolute, so iron and other parameters are not important. Tested on different PCs, the results are similar.
    It is also worth noting that calls do not take into account the preprocessing time, in which we get to the desired method, because in real conditions, in highly loaded tasks, such actions are performed only once, and the result is cached, so this time does not matter in the analysis.

    Direct call


    Just pull the method directly. In the table, the results of a direct call in the first row, the Abs value, respectively, is always one, relative to it in the remaining rows you can see a slowdown in calls by other methods of the method call (in the Rel value).
    public static TestResult TestDirectCall(DateTime arg)
    {
        var instance = Container.Get();
        long summ = 0;
        for (long i = 0; i < ITERATION_COUNT; i++)
        {
            summ += instance.Process(arg);
        }
    // return
    }
    


    Call through Reflection


    The easiest and most affordable way that you want to use in the first place. They took the method from the method table and pull it through Invoke. At the same time, one of the slowest ways.
    public static TestResult TestReflectionCall(object arg)
    {
        var instance = Container.Get(arg.GetType());
        var method = instance.GetType().GetMethod("Process");
        long summ = 0;
        for (long i = 0; i < ITERATION_COUNT; i++)
        {
            summ += (long)method.Invoke(instance, new object[] { arg });
        }
    // return
    }
    


    Call through delegate and through delegate with additional optimization


    Code for creating a delegate
    private static Delegate CreateDelegate(object target, MethodInfo method)
    {
        var methodParameters = method.GetParameters();
        var arguments = methodParameters.Select(d => Expression.Parameter(d.ParameterType, d.Name)).ToArray();
        var instance = target == null ? null : Expression.Constant(target);
        var methodCall = Expression.Call(instance, method, arguments);
        return Expression.Lambda(methodCall, arguments).Compile();
    }
    


    Accordingly, the test code becomes as follows:
    public static TestResult TestDelegateCall(object arg)
    {
        var instance = Container.Get(arg.GetType());
        var hook = CreateDelegate(instance, instance.GetType().GetMethod("Process"));
        long summ = 0;
        for (long i = 0; i < ITERATION_COUNT; i++)
        {
            summ += (long)hook.DynamicInvoke(arg);
        }
    // return
    }
    

    We got a slowdown compared to the Reflection method twice as much, you could throw this method, but there is a great way to speed up the process. Honestly, I spied on him in the Impromptu project , namely in this place .

    Delegate Call Optimization Code
    internal static object FastDynamicInvokeDelegate(Delegate del, params dynamic[] args)
    {
        dynamic tDel = del;
        switch (args.Length)
        {
            default:
                try
                {
                    return del.DynamicInvoke(args);
                }
                catch (TargetInvocationException ex)
                {
                    throw ex.InnerException;
                }
        #region Optimization
        case 1:
            return tDel(args[0]);
        case 2:
            return tDel(args[0], args[1]);
        case 3:
            return tDel(args[0], args[1], args[2]);
        case 4:
            return tDel(args[0], args[1], args[2], args[3]);
        case 5:
            return tDel(args[0], args[1], args[2], args[3], args[4]);
        case 6:
            return tDel(args[0], args[1], args[2], args[3], args[4], args[5]);
        case 7:
            return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
        case 8:
            return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]);
        case 9:
            return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]);
        case 10:
            return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]);
        case 11:
            return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10]);
        case 12:
            return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11]);
        case 13:
            return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12]);
        case 14:
            return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13]);
        case 15:
            return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14]);
        case 16:
            return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15]);
            #endregion
        }
    }
    



    Slightly change the test code

    public static TestResult TestDelegateOptimizeCall(object arg)
    {
        var instance = Container.Get(arg.GetType());
        var hook = CreateDelegate(instance, instance.GetType().GetMethod("Process"));
        long summ = 0;
        for (long i = 0; i < ITERATION_COUNT; i++)
        {
            summ += (long)FastDynamicInvokeDelegate(hook, arg);
        }
    // return
    }
    


    And we get tenfold acceleration compared to the usual delegate call. At the moment, this is the best option among those considered.

    Call through dynamic


    And move on to the main character (unless of course you support legacy projects created before .NET 4.0)
    public static TestResult TestDynamicCall(dynamic arg)
    {
        var instance = Container.Get(arg.GetType());
        dynamic hook = CreateDelegate(instance, instance.GetType().GetMethod("Process"));
        long summ = 0;
        for (long i = 0; i < ITERATION_COUNT; i++)
        {
            summ += hook(arg);
        }
    // return
    }
    


    All that we did in comparison with the call through the delegate, we added the dynamic keyword, which allowed the runtime to build the delegate call through the DLR itself during operation . In fact, they threw type checking checks. And they accelerated twice as compared to the optimized call of delegates.


    UPD: Added a more efficient way to call at the prompt mayorovp . It shows the best results in speed, and eats less memory in comparison with dynamic.

    delegate object Invoker(object target, params object[] args);
    static Invoker CreateExpression(MethodInfo method)
    {
        var targetArg = Expression.Parameter(typeof(object));
        var argsArg = Expression.Parameter(typeof(object[]));
        Expression body = Expression.Call(
            method.IsStatic ? null : Expression.Convert(targetArg, method.DeclaringType),
            method,
            method.GetParameters().Select((p, i) => Expression.Convert(Expression.ArrayIndex(argsArg, Expression.Constant(i)), p.ParameterType)));
        if (body.Type == typeof(void))
            body = Expression.Block(body, Expression.Constant(null));
        else if (body.Type.IsValueType)
            body = Expression.Convert(body, typeof(object));
        return Expression.Lambda(body, targetArg, argsArg).Compile();
    }
    


    Test
    public static TestResult TestExpressionCall(object arg)
    {
        var instance = Container.Get(arg.GetType());
        var hook = CreateExpression(instance.GetType().GetMethod("Process"));
        long summ = 0;
        for (long i = 0; i < ITERATION_COUNT; i++)
        {
            summ += (long)hook(instance, arg);
        }
    //return
    }
    


    Project Code

    Return to Results

    Read Next