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