Сегодня на ревью прилетела очередная фабрика животных:
В который раз расстроился, что 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
Исходники