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

в 2:44, , рубрики: C#, метки: , ,

При заходе в метод мы часто выполняемым проверку на 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;

        public void ThrowIfNullGenericArg<T>(T arg)
            where T : class
        {
            if (arg == null)
            {
                throw new ArgumentNullException();
            }
        }

        public void ThrowIfNullGenericArg<T>(Nullable<T> arg) // Nullable argument with struct constraint
            where T : struct
        {
            if(arg == null)
            {
                throw new ArgumentNullException();
            }
        }

        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 0.0001 ns 0.0003 ns 0.0003 ns
CallMethodWithObjArgNullableInt 121.1810 ns 0.1218 ns 0.1017 ns
CallMethodWithGenericArgString 0.0000 ns 0.0000 ns 0.0000 ns
CallMethodWithGenericArgNullableInt 0.0667 ns 0.0112 ns 0.0105 ns

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

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

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

Автор: крепыш

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js