مقارنة عوامل c # ؟: Vs if-else vs switch

اليوم ، دخل مصنع حيوانات آخر في المراجعة:

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

مرة أخرى شعرت بالضيق لأن C # يجبرك على إلقاء كائن القط إلى 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 |

من المثير للدهشة أن ثلاثية "كلب" تعمل بشكل أسرع ، و "لقط" - بيان إذا كان آخر.
ننظر إلى كود 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
}

عند إنشاء كائن الكلب ، يتم تنفيذ أوامر IL_0000 - IL_0008 بالتسلسل ، بينما عند إنشاء كائن Cat ، تحدث قفزة مشروطة ( IL_0001: brtrue.s IL_0009 ).

كما قد تخمن ، بالنسبة لبيان if-else ، يتم إنشاء رمز IL لا يتطلب قفزات شرطية لإنشاء كائن Cat . أثناء إنشاء كائن الكلب عبر فرع شرطي:

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
}

أضف إنشاء الببغاء وطريقة جديدة مع بيان التبديل إلى المصنع :

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: بالنسبة لعبارات ternary و if-else ، يعتمد وقت التشغيل بشكل مباشر على عدد القفزات الشرطية التي مرت في سلسلة التنفيذ.

الاستنتاج 2: يتم تحويل عبارة المحول C # في رمز IL إلى عبارة تبديل ، وفي المتوسط ​​، تعمل لفترة أطول قليلاً من عبارات التفريع العادية.

الخلاصة 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

Sources

All Articles