C# 条件运算符速度慢吗?

C# 条件运算符速度慢吗?,c#,performance,if-statement,conditional-operator,micro-optimization,C#,Performance,If Statement,Conditional Operator,Micro Optimization,我在看一些代码,每种情况下都有一个巨大的switch语句和if-else语句,我立刻感到了优化的冲动。作为一名优秀的开发人员,我应该始终做到这一点。我开始了解一些艰难的时间事实,并从三个变体开始: 原始代码如下所示: public static bool SwitchIfElse(Key inKey, out char key, bool shift) { switch (inKey) { case Key.A: if (shift) { key = 'A'; }

我在看一些代码,每种情况下都有一个巨大的switch语句和if-else语句,我立刻感到了优化的冲动。作为一名优秀的开发人员,我应该始终做到这一点。我开始了解一些艰难的时间事实,并从三个变体开始:

  • 原始代码如下所示:

    public static bool SwitchIfElse(Key inKey, out char key, bool shift)
    {
        switch (inKey)
        {
           case Key.A: if (shift) { key = 'A'; } else { key = 'a'; } return true;
           case Key.B: if (shift) { key = 'B'; } else { key = 'b'; } return true;
           case Key.C: if (shift) { key = 'C'; } else { key = 'c'; } return true;
           ...
           case Key.Y: if (shift) { key = 'Y'; } else { key = 'y'; } return true;
           case Key.Z: if (shift) { key = 'Z'; } else { key = 'z'; } return true;
           ...
           //some more cases with special keys...
        }
        key = (char)0;
        return false;
    }
    
    for (int i = 0; i < iterations / 4; i++)
    {
        method(Key.Space, key, true);
        method(Key.A, key, true);
        method(Key.Space, key, false);
        method(Key.A, key, false);
    }
    
  • 转换为使用条件运算符的第二个变量:

    public static bool SwitchConditionalOperator(Key inKey, out char key, bool shift)
    {
        switch (inKey)
        {
           case Key.A: key = shift ? 'A' : 'a'; return true;
           case Key.B: key = shift ? 'B' : 'b'; return true;
           case Key.C: key = shift ? 'C' : 'c'; return true;
           ...
           case Key.Y: key = shift ? 'Y' : 'y'; return true;
           case Key.Z: key = shift ? 'Z' : 'z'; return true;
           ...
           //some more cases with special keys...
        }
        key = (char)0;
        return false;
    }
    
  • 使用预先填充键/字符对的字典进行扭曲:

    public static bool DictionaryLookup(Key inKey, out char key, bool shift)
    {
        key = '\0';
        if (shift)
            return _upperKeys.TryGetValue(inKey, out key);
        else
            return _lowerKeys.TryGetValue(inKey, out key);
    }
    
  • 注意:两个switch语句的大小写完全相同,字典中的字符数相等

    我原以为1)和2)在性能上有些相似,3)会稍微慢一点

    对于每种方法,运行两次10.000.000迭代进行预热,然后计时,令我惊讶的是,我得到了以下结果:

  • 每次通话0.0000166毫秒
  • 每次通话0.0000779毫秒
  • 每次通话0.0000413毫秒
  • 这怎么可能?条件运算符比if-else语句慢四倍,几乎比字典查找慢两倍。我是缺少了一些基本的东西,还是条件运算符天生就很慢

    更新1:关于我的测试线束的几句话。我在VisualStudio2010中编译的.NET3.5项目的版本下为上述每个变体运行以下(伪)代码。代码优化已打开,调试/跟踪常量已关闭。在进行定时运行之前,我运行了一次测量下的方法进行预热。run方法执行了大量迭代,将
    shift
    设置为true和false,并选择一组输入键:

    Run(method);
    var stopwatch = Stopwatch.StartNew();
    Run(method);
    stopwatch.Stop();
    var measure = stopwatch.ElapsedMilliseconds / iterations;
    
    Run方法如下所示:

    public static bool SwitchIfElse(Key inKey, out char key, bool shift)
    {
        switch (inKey)
        {
           case Key.A: if (shift) { key = 'A'; } else { key = 'a'; } return true;
           case Key.B: if (shift) { key = 'B'; } else { key = 'b'; } return true;
           case Key.C: if (shift) { key = 'C'; } else { key = 'c'; } return true;
           ...
           case Key.Y: if (shift) { key = 'Y'; } else { key = 'y'; } return true;
           case Key.Z: if (shift) { key = 'Z'; } else { key = 'z'; } return true;
           ...
           //some more cases with special keys...
        }
        key = (char)0;
        return false;
    }
    
    for (int i = 0; i < iterations / 4; i++)
    {
        method(Key.Space, key, true);
        method(Key.A, key, true);
        method(Key.Space, key, false);
        method(Key.A, key, false);
    }
    
    2) 条件运算符:

    L_0165: ldarg.1 
    L_0166: ldarg.2 
    L_0167: brtrue.s L_016d
    
    L_0169: ldc.i4.s 0x62
    L_016b: br.s L_016f
    
    L_016d: ldc.i4.s 0x42
    L_016f: stind.i2 
    
    L_0170: ldc.i4.1 
    L_0171: ret 
    
    一些意见:

    • shift
      等于true时,条件运算符分支,而当
      shift
      为false时,if/else分支
    • 虽然1)实际编译为比2)多几个指令,但当
      shift
      为true或false时执行的指令数对于这两个指令是相等的
    • 1)的指令顺序是,始终只占用一个堆栈插槽,而2)始终加载两个堆栈插槽
    这些观察结果是否暗示条件运算符的执行速度较慢?还有其他副作用吗

    我希望1和2是一样的。优化器应该产生相同的代码。#3中的字典可能会很慢,除非它以某种方式进行了优化,以避免实际使用散列


    在对实时系统进行编码时,我们总是使用一个查找表(一个简单的数组)进行转换,如您的示例所示。当输入范围相当小时,它的速度最快。

    我会选择第三个选项,只是因为它更易于阅读/维护。
    我打赌这段代码不是您应用程序性能的瓶颈。

    非常奇怪,也许.NET优化会适得其反:

    作者拆开了几个 三元表达式和 发现它们与 if语句,用一个小的 差别。三元语句 有时生成测试 相反的条件,你会 期望,因为在它的测试中 子表达式为false,而不是 测试它是否为真。这将重新排序 一些说明和可以 偶尔提高性能

    您可能会考虑枚举值上的ToStin(对于非特殊情况):

    编辑:

    我将if-else方法与三值运算符进行了比较,对于1000000个循环,三值运算符始终至少与if-else方法一样快(有时快几毫秒,支持上面的文本)。我认为您在测量所用时间时犯了某种错误。

    有趣的是,我在这里开发了一个小类
    IfElseTernaryTest
    ,好的,代码不是真正的“优化”或很好的示例,但是……为了讨论:

    public class IfElseTernaryTest
    {
        private bool bigX;
        public void RunIfElse()
        {
            int x = 4; int y = 5;
            if (x &gt; y) bigX = false;
            else if (x &lt; y) bigX = true; 
        }
        public void RunTernary()
        {
            int x = 4; int y = 5;
            bigX = (x &gt; y) ? false : ((x &lt; y) ? true : false);
        }
    }
    
    这是代码的IL转储…有趣的是,IL中的三元指令实际上比
    if

    .class /*02000003*/ public auto ansi beforefieldinit ConTern.IfElseTernaryTest
           extends [mscorlib/*23000001*/]System.Object/*01000001*/
    {
      .field /*04000001*/ private bool bigX
      .method /*06000003*/ public hidebysig instance void 
              RunIfElse() cil managed
      // SIG: 20 00 01
      {
        // Method begins at RVA 0x205c
        // Code size       44 (0x2c)
        .maxstack  2
        .locals /*11000001*/ init ([0] int32 x,
                 [1] int32 y,
                 [2] bool CS$4$0000)
        .line 19,19 : 9,10 ''
    //000013:     }
    //000014: 
    //000015:     public class IfElseTernaryTest
    //000016:     {
    //000017:         private bool bigX;
    //000018:         public void RunIfElse()
    //000019:         {
        IL_0000:  /* 00   |                  */ nop
        .line 20,20 : 13,23 ''
    //000020:             int x = 4; int y = 5;
        IL_0001:  /* 1A   |                  */ ldc.i4.4
        IL_0002:  /* 0A   |                  */ stloc.0
        .line 20,20 : 24,34 ''
        IL_0003:  /* 1B   |                  */ ldc.i4.5
        IL_0004:  /* 0B   |                  */ stloc.1
        .line 21,21 : 13,23 ''
    //000021:             if (x &gt; y) bigX = false;
        IL_0005:  /* 06   |                  */ ldloc.0
        IL_0006:  /* 07   |                  */ ldloc.1
        IL_0007:  /* FE02 |                  */ cgt
        IL_0009:  /* 16   |                  */ ldc.i4.0
        IL_000a:  /* FE01 |                  */ ceq
        IL_000c:  /* 0C   |                  */ stloc.2
        IL_000d:  /* 08   |                  */ ldloc.2
        IL_000e:  /* 2D   | 09               */ brtrue.s   IL_0019
    
        .line 21,21 : 24,37 ''
        IL_0010:  /* 02   |                  */ ldarg.0
        IL_0011:  /* 16   |                  */ ldc.i4.0
        IL_0012:  /* 7D   | (04)000001       */ stfld      bool ConTern.IfElseTernaryTest/*02000003*/::bigX /* 04000001 */
        IL_0017:  /* 2B   | 12               */ br.s       IL_002b
    
        .line 22,22 : 18,28 ''
    //000022:             else if (x &lt; y) bigX = true; 
        IL_0019:  /* 06   |                  */ ldloc.0
        IL_001a:  /* 07   |                  */ ldloc.1
        IL_001b:  /* FE04 |                  */ clt
        IL_001d:  /* 16   |                  */ ldc.i4.0
        IL_001e:  /* FE01 |                  */ ceq
        IL_0020:  /* 0C   |                  */ stloc.2
        IL_0021:  /* 08   |                  */ ldloc.2
        IL_0022:  /* 2D   | 07               */ brtrue.s   IL_002b
    
        .line 22,22 : 29,41 ''
        IL_0024:  /* 02   |                  */ ldarg.0
        IL_0025:  /* 17   |                  */ ldc.i4.1
        IL_0026:  /* 7D   | (04)000001       */ stfld      bool ConTern.IfElseTernaryTest/*02000003*/::bigX /* 04000001 */
        .line 23,23 : 9,10 ''
    //000023:         }
        IL_002b:  /* 2A   |                  */ ret
      } // end of method IfElseTernaryTest::RunIfElse
    
      .method /*06000004*/ public hidebysig instance void 
              RunTernary() cil managed
      // SIG: 20 00 01
      {
        // Method begins at RVA 0x2094
        // Code size       27 (0x1b)
        .maxstack  3
        .locals /*11000002*/ init ([0] int32 x,
                 [1] int32 y)
        .line 25,25 : 9,10 ''
    //000024:         public void RunTernary()
    //000025:         {
        IL_0000:  /* 00   |                  */ nop
        .line 26,26 : 13,23 ''
    //000026:             int x = 4; int y = 5;
        IL_0001:  /* 1A   |                  */ ldc.i4.4
        IL_0002:  /* 0A   |                  */ stloc.0
        .line 26,26 : 24,34 ''
        IL_0003:  /* 1B   |                  */ ldc.i4.5
        IL_0004:  /* 0B   |                  */ stloc.1
        .line 27,27 : 13,63 ''
    //000027:             bigX = (x &gt; y) ? false : ((x &lt; y) ? true : false);
        IL_0005:  /* 02   |                  */ ldarg.0
        IL_0006:  /* 06   |                  */ ldloc.0
        IL_0007:  /* 07   |                  */ ldloc.1
        IL_0008:  /* 30   | 0A               */ bgt.s      IL_0014
    
        IL_000a:  /* 06   |                  */ ldloc.0
        IL_000b:  /* 07   |                  */ ldloc.1
        IL_000c:  /* 32   | 03               */ blt.s      IL_0011
    
        IL_000e:  /* 16   |                  */ ldc.i4.0
        IL_000f:  /* 2B   | 01               */ br.s       IL_0012
    
        IL_0011:  /* 17   |                  */ ldc.i4.1
        IL_0012:  /* 2B   | 01               */ br.s       IL_0015
    
        IL_0014:  /* 16   |                  */ ldc.i4.0
        IL_0015:  /* 7D   | (04)000001       */ stfld      bool ConTern.IfElseTernaryTest/*02000003*/::bigX /* 04000001 */
        .line 28,28 : 9,10 ''
    //000028:         }
        IL_001a:  /* 2A   |                  */ ret
      } // end of method IfElseTernaryTest::RunTernary
    
    看来,三元运算符显然更短,而且我猜,指令越少,运算速度越快……但在此基础上,它似乎与您的案例#2相矛盾,这是令人惊讶的


    编辑:在Sky的评论之后,建议“代码膨胀为#2”,这将反驳Sky的说法!!!好的,代码不同,上下文不同,这是一个示例练习,检查IL转储以查看…

    我不太明白为什么您会期望if语句比字典查找慢。至少需要计算一个hashcode,然后在列表中查找它。我不明白为什么你会认为这比cmp/jmp快


    具体来说,我甚至不认为你正在优化的方法是那么好;似乎在调用阶段可以做得更好(尽管我不能确定,因为您没有提供上下文)。

    我很想知道您是否正在使用调试或发布版本来测试它。如果是调试生成,那么差异很可能是由于使用发布模式(或手动禁用调试模式并启用编译器优化)时编译器添加的低级优化不足造成的

    但是,我希望通过优化,三元运算符的速度与if/else语句相同或稍快,而字典查找速度最慢。以下是我的结果,1000万次预热迭代,然后1000万次定时,每一次:

    调试模式

       If/Else: 00:00:00.7211259
       Ternary: 00:00:00.7923924
    Dictionary: 00:00:02.3319567
    
       If/Else: 00:00:00.5217478
       Ternary: 00:00:00.5050474
    Dictionary: 00:00:02.7389423
    
    释放模式

       If/Else: 00:00:00.7211259
       Ternary: 00:00:00.7923924
    Dictionary: 00:00:02.3319567
    
       If/Else: 00:00:00.5217478
       Ternary: 00:00:00.5050474
    Dictionary: 00:00:02.7389423
    
    我认为这里值得注意的是,在启用优化之前,三元计算比if/else慢,而在启用优化之后,三元计算慢