Результат и выводы для тех кто не любит длинный текст
| 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 | 
UPD: новый вывод от mayorovp: лучше всего использовать Expression
UPD: и как подсказал CdEmON, такое исследование было опубликовано на хабре ранее
Немного оффтопа, про причины исследования
Напишем следующий код:
Подобный код используется довольно часто, и в нем есть одно неудобство, — в C# нельзя хранить коллекцию generic типов явным образом. Все советы которые я находил сводятся к выделению базового non-generic класса, интерфейса или абстрактного класса, который и будет указан для хранения. Т.е. получим что-то вроде такого:
На мой взгляд, было бы удобно добавить в язык возможность писать таким образом:
Особенно учитывая, что делая generic тип через рефлексию, мы пользуемся схожей конструкцией:
Но вернемся к проблеме. Теперь представим что нам нужно получить конкретный инстанс, но получить его нам нужно в non-generic методе. Например, метод который принимает объект и исходя из его типа должен подобрать обработчик.
Обработчик мы получим, но среда не позволит написать теперь handler.Process(obj), а если и напишем, компилятор ругнется на отсутствие такого метода.
Вот тут тоже могла бы быть от разработчиков C# конструкция наподобие:
, но ее нет, а метод вызвать требуется (хотя учитывая Roslyn может уже есть подобное в новых IDE?). Способов сделать это масса, из которых можно выделить несколько основных. они и перечислены в таблице в начале статьи.
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
}
Код проекта
Вернуться к результатам

