C # -Operatoren vergleichen?: Vs if-else vs switch

Heute ist eine weitere Tierfabrik in die Bewertung eingeflogen:

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

Wieder einmal war ich verärgert, dass C # Sie zwingt, das Cat-Objekt auf Animal zu werfen. Aber lassen Sie die Kaste besser sein, denn durch die if-else-Anweisung ist der Code noch länger:
public static class AnimalsFactory
{
    public static Animal CreateAnimalByIfElseOperator(bool isCat)
    {
        if (isCat)
            return new Cat();

        return new Dog();
    }
}

Lassen Sie uns eine Minute von der Überprüfung abschweifen und versuchen, es herauszufinden:

  • Wird der IL-Code in diesen Beispielen unterschiedlich sein?
  • Wird eines der Beispiele von der Leistung profitieren?


Die Antwort auf die erste Frage lautet "Ja". Der IL-Code ist anders. Ich werde im Folgenden zeigen, was.

Kommen wir zum Thema Leistung. Laden Sie das Nuget-Paket für BenchmarkDotNet-Benchmarks herunter und schreiben Sie einen Test:

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

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

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

Benchmark-Ergebnisse:

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

Es ist überraschend, dass "für einen Hund" ternär schneller funktioniert und "für eine Katze" - eine Wenn-Sonst-Aussage.
Wir betrachten den IL-Code der Methode mit dem ternären Operator:

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
}

Beim Erstellen des Dog- Objekts werden die Befehle IL_0000 - IL_0008 nacheinander ausgeführt , während beim Erstellen des Cat- Objekts ein bedingter Sprung erfolgt ( IL_0001: brtrue.s IL_0009 ).

Wie Sie vielleicht erraten haben, wird für die if-else-Anweisung IL-Code generiert, der keine bedingten Sprünge zum Erstellen eines Cat- Objekts erfordert . Während das Dog- Objekt über einen bedingten Zweig erstellt wird:

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
}

Fügen Sie die Parrot-Erstellung und eine neue Methode mit der switch-Anweisung zur Factory hinzu :

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

Welche Methode wird schneller sein?

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


Schlussfolgerung 1: Bei ternären und if-else-Anweisungen hängt die Laufzeit direkt von der Anzahl der bedingten Sprünge ab, die im Ausführungsthread übergeben wurden.

Schlussfolgerung 2: Die C # -Switch-Anweisung wird im IL-Code in eine Switch-Anweisung konvertiert und funktioniert im Durchschnitt etwas länger als normale Verzweigungsanweisungen.

Schlussfolgerung 3: Alles oben Genannte ist nur für .NET Framework v.4.8 relevant. Nachdem wir dieselben Tests auf .NetCore durchgeführt haben, haben wir völlig unterschiedliche Ergebnisse erhalten, die noch irgendwie interpretiert werden müssen.

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 |


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

Quellen

All Articles