Сегодня на ревью прилетела очередная фабрика животных:
В который раз расстроился, что C# заставляет делать каст объекта Cat к Animal. Но пусть лучше будет каст, ведь через if-else оператор код получается ещё длиннее:
Отвлечёмся на минуту от ревью и попробуем разобраться:
Ответ на первый вопрос — да, IL-код отличается, ниже покажу чем.
Перейдём к вопросу о производительности. Качаем nuget-пакет для бенчмарков BenchmarkDotNet и пишем тест:
Результаты бенчмарков:
Удивительно, что «для собаки» быстрее работает тернарный, а «для кота» — if-else оператор.
Смотрим на IL-код метода с тернарным оператором:
При создании объекта Dog последовательно выполнятся команды IL_0000 — IL_0008, в то время как при создании объекта Cat происходит условный переход (IL_0001: brtrue.s IL_0009).
Как можно догадаться, для if-else оператора генерируется IL-код, который не требует условных переходов для создания объекта Cat. В то время как объект Dog создаётся через условный переход:
Добавим в фабрику создание «Попугая» и новый метод с оператором switch:
Какой из методов окажется быстрее?
Вывод 1: Для тернарного и if-else операторов время работы напрямую зависит от количества условных переходов, которые прозошли в потоке выполнения.
Вывод 2: C# оператор switch преобразуется в IL-коде в инструкцию switch и в среднем работает немного дольше, чем обычные операторы ветвления.
Вывод 3: Всё, что выше, актуально только для .NET Framework v.4.8. Прогнав те же самые тесты на .NetCore получились совсем другие результаты, которые ещё предстоит как-то интерпретировать.
Процессор: Intel® Core(TM) i7-7700K
Исходники
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
Исходники
