Сравниваем c# операторы ?: vs if-else vs switch

—

от автора

Сегодня на ревью прилетела очередная фабрика животных:

public static class AnimalsFactory {     public static Animal CreateAnimalByTernaryOperator(bool isCat)     {         return isCat ? (Animal)new Cat() : new Dog();     } } 

В который раз расстроился, что C# заставляет делать каст объекта Cat к Animal. Но пусть лучше будет каст, ведь через if-else оператор код получается ещё длиннее:

public static class AnimalsFactory {     public static Animal CreateAnimalByIfElseOperator(bool isCat)     {         if (isCat)             return new Cat();          return new Dog();     } } 

Отвлечёмся на минуту от ревью и попробуем разобраться:
— будет ли отличаться IL-код в этих примерах?
— будет ли один из примеров в выигрыше по производительности?

Ответ на первый вопрос — да, IL-код отличается, ниже покажу чем.
Перейдём к вопросу о производительности. Качаем nuget-пакет для бенчмарков BenchmarkDotNet и пишем тест:

public class AnimalFactoryPerformanceTests {     [ParamsAllValues]     public bool IsCat { get; set; }      [Benchmark]     public void CreateAnimalByTernaryOperator() =>         AnimalsFactory.CreateAnimalByTernaryOperator(IsCat);      [Benchmark]     public void CreateAnimalByIfElseOperator() =>         AnimalsFactory.CreateAnimalByIfElseOperator(IsCat); } 

Результаты бенчмарков:

|                         Метод | IsCat |    Время | |------------------------------ |------ |---------:| | CreateAnimalByTernaryOperator | False | 1.357 ns | | CreateAnimalByTernaryOperator |  True | 1.655 ns | |------------------------------ |------ |---------:| |  CreateAnimalByIfElseOperator | False | 1.636 ns | |  CreateAnimalByIfElseOperator |  True | 1.360 ns | 

Удивительно, что «для собаки» быстрее работает тернарный, а «для кота» — if-else оператор.
Смотрим на IL-код метода с тернарным оператором:

CreateAnimalByTernaryOperator(bool isCat) {     IL_0000: ldarg.0      // isCat     IL_0001: brtrue.s     IL_0009     IL_0003: newobj       instance void AnimalPerformance.Dog::.ctor()     IL_0008: ret     IL_0009: newobj       instance void AnimalPerformance.Cat::.ctor()     IL_000e: ret } 

При создании объекта Dog последовательно выполнятся команды IL_0000 — IL_0008, в то время как при создании объекта Cat происходит условный переход (IL_0001: brtrue.s IL_0009)

Как можно догадаться, для if-else оператора генерируется IL-код, который не требует условных переходов для создания объекта Cat. В то время как объект Dog создаётся через условный переход:

CreateAnimalByIfElseOperator(bool isCat) {     IL_0000: ldarg.0      // isCat     IL_0001: brfalse.s    IL_0009     IL_0003: newobj       instance void AnimalPerformance.Cat::.ctor()     IL_0008: ret     IL_0009: newobj       instance void AnimalPerformance.Dog::.ctor()     IL_000e: ret } 

Добавим в фабрику создание «Попугая» и новый метод с оператором switch:

public static class AnimalFactory {     public static Animal CreateAnimalByTernaryOperator(AnimalType animalType)     {         return animalType == AnimalType.Cat             ? new Cat()             : animalType == AnimalType.Dog                 ? (Animal)new Dog()                 : new Parrot();     }      public static Animal CreateAnimalByIfElseOperator(AnimalType animalType)     {         if (animalType == AnimalType.Cat)             return new Cat();          if (animalType == AnimalType.Dog)             return new Dog();          return new Parrot();     }      public static Animal CreateAnimalBySwitchOperator(AnimalType animalType)     {         switch (animalType)         {             case AnimalType.Cat:                 return new Cat();             case AnimalType.Dog:                 return new Dog();             case AnimalType.Parrot:                 return new Parrot();             default:                 throw new InvalidOperationException();         }     } } 

Какой из методов окажется быстрее?

Результаты бенчмарков

|                         Метод | AnimalType |    Время | |------------------------------ |----------- |---------:| | CreateAnimalByTernaryOperator |        Cat | 2.490 ns | | CreateAnimalByTernaryOperator |        Dog | 2.515 ns | | CreateAnimalByTernaryOperator |     Parrot | 2.333 ns | |------------------------------ |----------- |---------:| |  CreateAnimalByIfElseOperator |        Cat | 2.368 ns | |  CreateAnimalByIfElseOperator |        Dog | 2.545 ns | |  CreateAnimalByIfElseOperator |     Parrot | 2.735 ns | |------------------------------ |----------- |---------:| |  CreateAnimalBySwitchOperator |        Cat | 2.747 ns | |  CreateAnimalBySwitchOperator |        Dog | 2.730 ns | |  CreateAnimalBySwitchOperator |     Parrot | 2.722 ns | 

IL-код

CreateAnimalByTernaryOperator(AnimalsFactory.AnimalType animalType) {     IL_0000: ldarg.0      // animalType     IL_0001: brfalse.s    IL_0013     IL_0003: ldarg.0      // animalType     IL_0004: ldc.i4.1     IL_0005: beq.s        IL_000d     IL_0007: newobj       instance void AnimalsFactory.Parrot::.ctor()     IL_000c: ret     IL_000d: newobj       instance void AnimalsFactory.Dog::.ctor()     IL_0012: ret     IL_0013: newobj       instance void AnimalsFactory.Cat::.ctor()     IL_0018: ret }  CreateAnimalByIfElseOperator(AnimalsFactory.AnimalType animalType) {     IL_0000: ldarg.0      // animalType     IL_0001: brtrue.s     IL_0009     IL_0003: newobj       instance void AnimalsFactory.Cat::.ctor()     IL_0008: ret     IL_0009: ldarg.0      // animalType     IL_000a: ldc.i4.1     IL_000b: bne.un.s     IL_0013     IL_000d: newobj       instance void AnimalsFactory.Dog::.ctor()     IL_0012: ret     IL_0013: newobj       instance void AnimalsFactory.Parrot::.ctor()     IL_0018: ret  }  CreateOtherAnimalBySwitchOperator(AnimalsFactory.AnimalType animalType) {     IL_0000: ldarg.0      // animalType     IL_0001: switch       (IL_0014, IL_001a, IL_0020)     IL_0012: br.s         IL_0026     IL_0014: newobj       instance void AnimalsFactory.Cat::.ctor()     IL_0019: ret     IL_001a: newobj       instance void AnimalsFactory.Dog::.ctor()     IL_001f: ret     IL_0020: newobj       instance void AnimalsFactory.Parrot::.ctor()     IL_0025: ret     IL_0026: newobj       instance void System.InvalidOperationException::.ctor()     IL_002b: throw } 

Вывод 1: Для тернарного и if-else операторов время работы напрямую зависит от количества условных переходов, которые прозошли в потоке выполнения.
Вывод 2: C# оператор switch преобразуется в IL-коде в инструкцию switch и в среднем работает немного дольше, чем обычные операторы ветвления.
Вывод 3: Всё, что выше, актуально только для .NET Framework v.4.8. Прогнав те же самые тесты на .NetCore получились совсем другие результаты, которые ещё предстоит как-то интерпретировать.

Результаты бенчмарков .NetCore 3.0

|                         Метод | AnimalType |    Время | |------------------------------ |----------- |---------:| | CreateAnimalByTernaryOperator |        Cat | 3.046 ns | | CreateAnimalByTernaryOperator |        Dog | 2.984 ns | | CreateAnimalByTernaryOperator |     Parrot | 3.019 ns | |------------------------------ |----------- |---------:| |  CreateAnimalByIfElseOperator |        Cat | 2.980 ns | |  CreateAnimalByIfElseOperator |        Dog | 2.977 ns | |  CreateAnimalByIfElseOperator |     Parrot | 3.103 ns | |------------------------------ |----------- |---------:| |  CreateAnimalBySwitchOperator |        Cat | 3.519 ns | |  CreateAnimalBySwitchOperator |        Dog | 3.533 ns | |  CreateAnimalBySwitchOperator |     Parrot | 3.312 ns | 

Процессор: Intel® Core(TM) i7-7700K
Исходники

ссылка на оригинал статьи https://habr.com/ru/post/501676/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *