При заходе в метод мы часто выполняемым проверку на 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 потому что:
- Спасает от упаковки
- Позволяет улучшить сигнатуру метода, при использовании ограничений
Upd. Спасибо хабраюзеру zelyony за замечание. Методы инлайнились, для более точных замеров добавил атрибут MethodImpl(MethodImplOptions.NoInlining).
