7 July 2017

Как generic-и нас спасают от упаковки

C#
Sandbox

При заходе в метод мы часто выполняемым проверку на null. Кто-то выносит проверку в отдельный метод, что бы код выглядел чище, и получается что то-такое:


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

И что интересно при такой проверке, я массово вижу использование именно object атрибута, можно ведь воспользоватся generic-ом. Давайте попробуем заменить наш метод на generic и сравнить производительность.


Перед тестированием нужно учесть ещё один недостаток object аргумента. Значимые типы(value types) никогда не могут быть равны null(Nullable тип не в счёт). Вызов метода, вроде ThrowIfNull(5), бессмыслен, однако, поскольку тип аргумента у нас object, компилятор позволит вызвать метод. Как по мне, это снижает качество кода, что в некоторых ситуациях гораздо важнее производительности. Для того что бы избавится от такого поведения, и улучшить сигнатуру метода, generic метод придётся разделить на два, с указанием ограничений(constraints). Беда в том что нельзя указать Nullable ограничение, однако, можно указать nullable аргумент, с ограничением struct.


Приступаем к тестированию производительности, и воспользуемся библиотекой BenchmarkDotNet. Навешиваем атрибуты, запускаем, и смотрим на результаты.


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>();
        }
    }

Method Mean Error StdDev
CallMethodWithObjArgString 1.784 ns 0.0166 ns 0.0138 ns
CallMethodWithObjArgNullableInt 124.335 ns 0.2480 ns 0.2320 ns
CallMethodWithGenericArgString 1.746 ns 0.0290 ns 0.0271 ns
CallMethodWithGenericArgNullableInt 2.158 ns 0.0089 ns 0.0083 ns

Наш generic на nullable типе отработал в 2000 раз быстрее! А всё из-за пресловутой упаковки(boxing). Когда мы вызываем CallMethodWithObjArgNullableInt, то наш nullable-int "упаковывается" и размещается в куче. Упаковка очень дорогая операция, от того метод и проседает по производительности. Таким образом использую generic мы можем избежать упаковки.


Итак, generic аргумент лучше object потому что:


  1. Спасает от упаковки
  2. Позволяет улучшить сигнатуру метода, при использовании ограничений

Upd. Спасибо хабраюзеру zelyony за замечание. Методы инлайнились, для более точных замеров добавил атрибут MethodImpl(MethodImplOptions.NoInlining).

Tags:c#genericsboxing
Hubs: C#
+10
9.1k 41
Comments 30