Результат и выводы для тех кто не любит длинный текст
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
}
Код проекта
Вернуться к результатам