Pull to refresh

Исследование скорости вызова метода различными способами

Reading time7 min
Views12K

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



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
}


Код проекта

Вернуться к результатам
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+9
Comments12

Articles