Результат и выводы для тех кто не любит длинный текст



100.000 вызовов, 20 итераций теста, x86 100.000 вызовов, 20 итераций теста, x64 1.000.000 вызовов, 10 итераций теста, x86 1.000.000 вызовов, 10 итераций теста, x64
Прямой вызов
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

Вызов через отражение
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

Вызов через делегат
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

Вызов через делегат с оптимизациями
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

Вызов через 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

Вызов через 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



При использованиии .NET Framework 3.5 лучше всего использовать вызов методов через делегат с оптимизацией вызова. Для .NET Framework 4.0+ отличным выбором будет использование dynamic.
UPD: новый вывод от mayorovp: лучше всего использовать Expression

UPD: и как подсказал CdEmON, такое исследование было опубликовано на хабре ранее



Немного оффтопа, про причины исследования
Напишем следующий код:
class SampleGeneric<T>
{
    public long Process(T obj)
    {
        return String.Format("{0} [{1}]", obj.ToString(), obj.GetType().FullName).Length;
    }
}

class Container
{
    private static Dictionary<Type, object> _instances = new Dictionary<Type, object>();

    public static void Register<T>(SampleGeneric<T> instance)
    {
        if (false == _instances.ContainsKey(typeof(T)))
        {
            _instances.Add(typeof(T), instance);
        }
        else
        {
            _instances[typeof(T)] = instance;
        }
    }

    public static SampleGeneric<T> Get<T>()
    {
        if (false == _instances.ContainsKey(typeof(T))) throw new KeyNotFoundException();
        return (SampleGeneric<T>)_instances[typeof(T)];
    }

    public static object Get(Type type)
    {
        if (false == _instances.ContainsKey(type)) throw new KeyNotFoundException();
        return _instances[type];
    }
}


Подобный код используется довольно часто, и в нем есть одно неудобство, — в C# нельзя хранить коллекцию generic типов явным образом. Все советы которые я находил сводятся к выделению базового non-generic класса, интерфейса или абстрактного класса, который и будет указан для хранения. Т.е. получим что-то вроде такого:
public interface ISampleGeneric { }
class SampleGeneric<T> : ISampleGeneric
//
private static Dictionary<Type, ISampleGeneric> _instances = new Dictionary<Type, ISampleGeneric>();


На мой взгляд, было бы удобно добавить в язык возможность писать таким образом:
// Ошибка Type expected
Dictionary<Type, SampleGeneric<>>

Особенно учитывая, что делая generic тип через рефлексию, мы пользуемся схожей конструкцией:
typeof(SampleGeneric<>).MakeGenericType(typeof(string))

Но вернемся к проблеме. Теперь представим что нам нужно получить конкретный инстанс, но получить его нам нужно в non-generic методе. Например, метод который принимает объект и исходя из его типа должен подобрать обработчик.
void NonGenericMethod(object obj)
{
   var handler = Container.Get(obj.GetType());
}


Обработчик мы получим, но среда не позволит написать теперь handler.Process(obj), а если и напишем, компилятор ругнется на отсутствие такого метода.
Вот тут тоже могла бы быть от разработчиков C# конструкция наподобие:
Container.GetInstance<fromtype(obj.GetType())>().Process(obj);

, но ее нет, а метод вызвать требуется (хотя учитывая Roslyn может уже есть подобное в новых IDE?). Способов сделать это масса, из которых можно выделить несколько основных. они и перечислены в таблице в начале статьи.


Про код


Ниже используется вызов кода приведенного в спойлере. Из кода убраны замеры времение, замеры делались через Stopwatch. Для анализа интересовало относительное время выполнения, а не абсолютное, поэтому железо и другие параметры не важны. Тестировал на разных пк, результаты схожие.
Также стоит заметить, что при вызовах не учитывается время на предобработку, в которой добираемся до нужного метода, т.к. в реальных условиях, в высоконагруженных задачах такие действия выполняются только раз, и результат кэшируется, соответственно это время не имеет значения при анализе.

Прямой вызов


Просто дергаем метод напрямую. В таблице, результаты прямого вызова в первой строке, значение Abs соответственно всегда единица, относительно него в остальных строках можно видеть замедление вызовов другими способами вызова метода (в значении Rel).
public static TestResult TestDirectCall(DateTime arg)
{
    var instance = Container.Get<DateTime>();
    long summ = 0;
    for (long i = 0; i < ITERATION_COUNT; i++)
    {
        summ += instance.Process(arg);
    }
// return
}


Вызов через Reflection


Самый простой и доступный способ, который хочется использовать в первую очередь. Забрали метод из таблицы методов и дергаем его через Invoke. В то же время, один из самых медленных способов.
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
}


Вызов через делегат и через делегат с дополнительной оптимизацией


Код для создания делегата
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();
}


Соответственно код теста становится следующим:
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
}

Получили замедление по сравнению с Reflection способом еще в два раза, можно было бы выкинуть этот метод, но есть отличный способ ускорить процесс. Честно скажу что подсмотрел его в проекте Impromptu, а именно в этом месте.

Код оптимизации вызова делегата
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
    }
}



Незначительно меняем код теста

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
}


И получаем десятикратное ускорение по сравнению с обычным вызовом делегата. На текущий момент это лучший вариант из рассмотренных.

Вызов через dynamic


И переходим к главному герою (если конечно вы не поддерживаете legacy проекты созданные до .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
}


Все что мы сделали по сравнению с вызовом через делегат, добавили ключевое слово dynamic, чем позволили среде исполнения во время работы самой построить через DLR вызов делегата. По сути выкинули проверки на совпадение типов. И ускорились еще в два раза по сравнению с оптимизированным вызовом делегатов.


UPD: Добавил более эффективный способ вызова по подсказке mayorovp. Показывает наилучшие результаты по скорости, и ест меньше памяти в сравнении с 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<Invoker>(body, targetArg, argsArg).Compile();
}


Тест
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
}


Код проекта

Вернуться к результатам

Комментарии (12)


  1. Ogoun
    14.04.2016 16:43

    Я не совсем понял комментарий, вы описали ситуацию когда известен тип аргумента, а значит и известен нужный инстанс. Пост же совсем о другом, я описываю случай когда тип будет неизвестен. И код который вы указали не работает, точнее если его дописать в

    Func<DateTime, long> hook = Container.Get<DateTime>().Process;
    

    то это прямой вызов, бесполезно обернутый анонимной функцией, а не вызов посредством создания делегата метода заранее неизвестного инстанса.


  1. DrReiz
    14.04.2016 17:03
    +4

    Имхо, терминология в статье отличается от классической.
    Называемый в статье «вызов через делегат» — в классике «вызов через expression».
    Вызов через делегат в классике — это:

    Func<DateTime, int> f = instance.Process; 
    for(..){f(arg);}
    


    1. Ogoun
      14.04.2016 17:12

      Expression в коде служит для создания делегата, вызов метода идет именно через вызов делегата.


      1. 77vlad
        14.04.2016 18:01

        Вызов делегата это delegate(aaa);
        А не всякие там GetType() и GetMethod() это тот же рефлекшен только в профиль
        А еще есть вызов интерфейсов, вобщем прежде чем писать мегастатьи лучше погуглите codeplex с 2005 годов, много узнаете нового


        1. Ogoun
          14.04.2016 18:05

          Задача описанная в статье, вызов метода у заранее неизвестного инстанса. При этом метод generic. По результатам я пришел к выводу что вызывать лучше всего через dynamic или через оптимизированный вызов делегата.
          Если вы знаете как вызвать более оптимально, покажите как. Я пока не нашел решения лучше.


  1. 77vlad
    14.04.2016 17:12
    +8

    Нда… так криво вызывать делегат — это надо постараться… нафига писать говнокод. а потом обсуждать, что он плохо работает…

    Пишем:

    public static TestResult TestDelegateCall(DateTime arg)
    {
    var instance = Container.Get(arg.GetType());
    //var hook = CreateDelegate(instance, instance.GetType().GetMethod(«Process»));
    Func hook = Container.Get().Process;
    Stopwatch sw = new Stopwatch();
    sw.Start();
    long summ = 0;
    for (long i = 0; i < ITERATION_COUNT; i++)
    {
    //summ += (long)hook.DynamicInvoke(arg);
    summ += hook(arg);
    }
    sw.Stop();
    return new TestResult { Result = summ, ElapsedMilliseconds = sw.ElapsedMilliseconds };
    }
    И вуаля:
    Прямой вызов
    Min: 921 ms
    Max: 943 ms
    Mean: 928,8 ms
    Median: 928,5 ms

    Вызов через отражение
    Min: 1302 ms
    Max: 1319 ms
    Mean: 1311,3 ms
    Median: 1310,5 ms

    Вызов через делегат
    Min: 921 ms
    Max: 940 ms
    Mean: 929,2 ms
    Median: 929 ms

    Вызов через делегат с оптимизациями
    Min: 1093 ms
    Max: 1117 ms
    Mean: 1103,7 ms
    Median: 1101,5 ms

    Вызов через dynamic
    Min: 1058 ms
    Max: 1076 ms
    Mean: 1066,6 ms
    Median: 1065,5 ms


  1. shai_hulud
    14.04.2016 17:49
    +5

    >(long)hook.DynamicInvoke(arg);
    Это бесполезный бенчмарк. Реализация Dynamic Invoke идёт через MethodInfo.Invoke() и не отличается от reflection. Вызов делегата это вызов Invoke на нем.


  1. mayorovp
    15.04.2016 08:48
    +5

    Даже если типы неизвестны заранее — это еще не повод использовать DynamicInvoke! Раз уж вы используете Expression.Compile() — надо с помощью приведения типов привести делегат к заранее известной сигнатуре.

    	delegate object Invoker(object target, params object[] args);
    	static Invoker CreateDelegate(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<Invoker>(body, targetArg, argsArg).Compile();
    	}
    


    Вот такой делегат в тесты добавьте, пожалуйста! На моих тестах он работает столь же быстро, как и прямой вызов.


    1. Ogoun
      15.04.2016 12:30

      Да, действительно, работает почти также быстро как прямой вызов, и быстрее чем dynamic, спасибо! Добавил в пост.


  1. CdEmON
    15.04.2016 12:36

    Статье не хватает вывода: для чего были все потуги?
    Вот тут (по теме) есть выводы: habrahabr.ru/post/103558
    Вот тут (по теме) есть выводы: msdn.microsoft.com/library/ms973852.aspx


    1. Ogoun
      15.04.2016 12:36
      -2

      Для чего были потуги описано под спойлером.


  1. m08pvv
    15.04.2016 22:14
    +2

    Почему бы не использовать для бенчмарков BenchmarkDotNet?
    Бенчмарк не учитывает много аспектов, которые учитываются в BenchmarkDotNet.
    Почему и как правильно бенчмаркать можно узнать, например,

    из вот этого видео,