📜 ⬆️ ⬇️

Investigating the speed of a method call

Result and conclusions for those who do not like long text



100,000 calls, 20 test iterations, x86100,000 calls, 20 test iterations, x641,000,000 calls, 10 test iterations, x861,000,000 calls, 10 test iterations, x64
Direct call
 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

Call through reflection
 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

Call via delegate
 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

Call via delegate with optimizations
 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

Call via 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

Call via 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



When using the .NET Framework 3.5, it is best to use a method call through a delegate with call optimization. For .NET Framework 4.0+, using dynamic is a great choice.
UPD: new output from mayorovp : best used Expression

UPD: and as suggested by CdEmON , such a study was published in Habré earlier
')


A little offtopic, about the reasons for the study
We write the following code:
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]; } } 


Such code is used quite often, and there is one inconvenience in it - in C # you cannot store a collection of generic types explicitly. All the tips that I found come down to highlighting the basic non-generic class, interface, or abstract class that will be specified for storage. Those. we get something like this:
 public interface ISampleGeneric { } class SampleGeneric<T> : ISampleGeneric // private static Dictionary<Type, ISampleGeneric> _instances = new Dictionary<Type, ISampleGeneric>(); 


In my opinion, it would be convenient to add to the language the ability to write as follows:
 //  Type expected Dictionary<Type, SampleGeneric<>> 

Especially considering that when doing a generic type through reflection, we use a similar construction:
 typeof(SampleGeneric<>).MakeGenericType(typeof(string)) 

But back to the problem. Now imagine that we need to get a specific instance, but we need to get it in a non-generic method. For example, a method that accepts an object and, based on its type, must select a handler.
 void NonGenericMethod(object obj) { var handler = Container.Get(obj.GetType()); } 


We’ll get the handler, but the environment will not allow writing now handler.Process (obj), and if we write, the compiler will swear at the absence of such a method.
Here, too, could be from C # developers like:
 Container.GetInstance<fromtype(obj.GetType())>().Process(obj); 

, but it doesn’t exist, but the method to call is required (although given Roslyn, can there already be something similar in the new IDE?). Ways to do this mass, from which there are several major. they are listed in the table at the beginning of the article.


About code


Below is a call to the code given in the spoiler. Time measurements were removed from the code, measurements were made via Stopwatch. For analysis, I was interested in the relative execution time, not absolute, so the iron and other parameters are not important. Tested on different PCs, the results are similar.
It is also worth noting that when you call, the time for preprocessing is not taken into account, in which we get to the desired method, since in real conditions, in highly loaded tasks, such actions are performed only once, and the result is cached, respectively, this time does not matter in the analysis.

Direct call


Just pull the method directly. In the table, the results of a direct call in the first row, the Abs value is always one, respectively; in the remaining rows, you can see the slowdown of calls by other methods of calling the method (in the Rel value).
 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 } 


Call via Reflection


The easiest and most affordable way that you want to use first. Took a method from the table of methods and we pull it through Invoke. At the same time, one of the slowest ways.
 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 } 


Call via delegate and via delegate with additional optimization


Code to create a delegate
 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(); } 


Accordingly, the test code becomes the following:
 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 } 

Got a slowdown compared with the Reflection method in another two times, you could throw out this method, but there is a great way to speed up the process. Honestly, I spied it in the project Impromptu , namely in this place .

Delegate Call Optimization Code
 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 } } 



Slightly changing the test code

 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 } 


And we get a tenfold acceleration compared to the usual delegate call. At the moment it is the best option from the considered.

Call via dynamic


And go to the main character (unless of course you support legacy projects created before .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 } 


All that we have done in comparison with the call through the delegate, we added the dynamic keyword, which allowed the runtime to build the delegate call through the DLR itself during the work. In essence, they threw checks on the type match. And they accelerated two more times compared to the optimized call of delegates.


UPD: Added a more efficient way to call on the mayorovp prompt. Shows the best results in speed, and eats less memory compared to 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(); } 


Test
 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 } 


Project code

Back to results

Source: https://habr.com/ru/post/279367/


All Articles