📜 ⬆️ ⬇️

How generic-they save us from packing

When entering a method, we often perform a null check. Someone makes a check in a separate method, so that the code looks cleaner, and it turns out that it is:


public void ThrowIfNull(object obj) { if(obj == null) { throw new ArgumentNullException(); } } 

And what is interesting with such a check, I see the use of the object attribute in large quantities, because you can use a generic. Let's try to replace our method with generic and compare performance.


Before testing, you need to take into account another drawback of the object argument. Significant types (value types) can never be equal to null (Nullable type does not count). Calling a method like ThrowIfNull (5) is meaningless, however, since the type of the argument is object, the compiler will allow the method to be called. As for me, this reduces the quality of the code, which in some situations is much more important than performance. In order to get rid of this behavior, and to improve the method signature, the generic method will have to be divided into two, with an indication of constraints (constraints). The trouble is that you cannot specify a nullable constraint, however, you can specify a nullable argument with a struct constraint.


We start performance testing, and use the BenchmarkDotNet library. We hang attributes, run, and look at the results.


 public class ObjectArgVsGenericArg { public string str = "some string"; public Nullable<int> num = 5; [MethodImpl(MethodImplOptions.NoInlining)] public void ThrowIfNullGenericArg<T>(T arg) where T : class { if (arg == null) { throw new ArgumentNullException(); } } [MethodImpl(MethodImplOptions.NoInlining)] public void ThrowIfNullGenericArg<T>(Nullable<T> arg) where T : struct { if(arg == null) { throw new ArgumentNullException(); } } [MethodImpl(MethodImplOptions.NoInlining)] public void ThrowIfNullObjectArg(object arg) { if(arg == null) { throw new ArgumentNullException(); } } [Benchmark] public void CallMethodWithObjArgString() { ThrowIfNullObjectArg(str); } [Benchmark] public void CallMethodWithObjArgNullableInt() { ThrowIfNullObjectArg(num); } [Benchmark] public void CallMethodWithGenericArgString() { ThrowIfNullGenericArg(str); } [Benchmark] public void CallMethodWithGenericArgNullableInt() { ThrowIfNullGenericArg(num); } } class Program { static void Main(string[] args) { var summary = BenchmarkRunner.Run<ObjectArgVsGenericArg>(); } } 

MethodMeanErrorStddev
CallMethodWithObjArgString1.784 ns0.0166 ns0.0138 ns
CallMethodWithObjArgNullableInt124.335 ns0.2480 ns0.2320 ns
CallMethodWithGenericArgString1.746 ns0.0290 ns0.0271 ns
CallMethodWithGenericArgNullableInt2.158 ns0.0089 ns0.0083 ns

Our generic on nullable type worked 2000 times faster! And all because of the notorious packaging (boxing) . When we call CallMethodWithObjArgNullableInt, our nullable-int is "packaged" and placed on the heap. Packaging is a very expensive operation, and the method sags in performance. Thus using generic we can avoid packaging.


So, the generic argument is better than object because:


  1. Saves from packing
  2. Allows you to improve the method signature when using constraints

Upd. Thanks to the zelyony habraiser for comment. Methods inline for more accurate measurements added attribute MethodImpl (MethodImplOptions.NoInlining) .


')

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


All Articles