Comparer les opérateurs c #?: Vs if-else vs switch

Aujourd'hui, une autre animalerie s'est lancée dans la revue:

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

Encore une fois, j'étais contrarié que C # m'oblige à lancer l'objet Chat sur Animal. Mais que la caste soit meilleure, car grâce à l'instruction if-else, le code est encore plus long:
public static class AnimalsFactory
{
    public static Animal CreateAnimalByIfElseOperator(bool isCat)
    {
        if (isCat)
            return new Cat();

        return new Dog();
    }
}

Écartons-nous une minute de l'examen et essayons de comprendre:

  • Le code IL sera-t-il différent dans ces exemples?
  • l'un des exemples bénéficiera-t-il des performances?


La réponse à la première question est oui, le code IL est différent, je vais montrer ci-dessous quoi.

Passons à la question de la performance. Téléchargez le package nuget pour les benchmarks BenchmarkDotNet et écrivez un test:

public class AnimalFactoryPerformanceTests
{
    [ParamsAllValues]
    public bool IsCat { get; set; }

    [Benchmark]
    public void CreateAnimalByTernaryOperator() =>
        AnimalsFactory.CreateAnimalByTernaryOperator(IsCat);

    [Benchmark]
    public void CreateAnimalByIfElseOperator() =>
        AnimalsFactory.CreateAnimalByIfElseOperator(IsCat);
}

Résultats de référence:

|                          | IsCat |     |
|------------------------------ |------ |---------:|
| CreateAnimalByTernaryOperator | False | 1.357 ns |
| CreateAnimalByTernaryOperator |  True | 1.655 ns |
|------------------------------ |------ |---------:|
|  CreateAnimalByIfElseOperator | False | 1.636 ns |
|  CreateAnimalByIfElseOperator |  True | 1.360 ns |

Il est surprenant que "pour un chien" ternaire fonctionne plus rapidement, et "pour un chat" - une déclaration si-sinon.
Nous regardons le code IL de la méthode avec l'opérateur ternaire:

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
}

Lors de la création de l'objet Dog , les commandes IL_0000 - IL_0008 sont exécutées séquentiellement , tandis que lors de la création de l'objet Cat , un saut conditionnel se produit ( IL_0001: brtrue.s IL_0009 ).

Comme vous pouvez le deviner, pour l'instruction if-else, un code IL est généré qui ne nécessite pas de sauts conditionnels pour créer un objet Cat . Alors que l'objet Dog est créé via une branche conditionnelle:

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
}

Ajoutez la création Parrot et une nouvelle méthode avec l' instruction switch à l'usine :

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();
        }
    }
}

Quelle méthode sera plus rapide?

Résultats de référence
|                          | 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 |


Code 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
}


Conclusion 1: pour les instructions ternaires et if-else, l'exécution dépend directement du nombre de sauts conditionnels qui se sont passés dans le thread d'exécution.

Conclusion 2: l' instruction switch C # est convertie en code IL en instruction switch et, en moyenne, fonctionne un peu plus longtemps que les instructions de branchement standard.

Conclusion 3: Tout ce qui précède n'est pertinent que pour le .NET Framework v.4.8. Après avoir exécuté les mêmes tests sur .NetCore, nous avons obtenu des résultats complètement différents qui doivent encore être interprétés d'une manière ou d'une autre.

Benchmarks .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 |


Processeur: Intel® Core (TM) i7-7700K

Sources

All Articles