条件运算符是否缓慢?

时间:2010-02-14 00:50:34

标签: c# performance if-statement conditional-operator micro-optimization

我正在查看一些代码,其中包含一个巨大的switch语句和每个case上的if-else语句,并立即感受到优化的冲动。作为一个优秀的开发人员,我总是应该着手获得一些难以解决的时间问题,并从三个变体开始:

  1. 原始代码如下所示:

    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;
    }
    
  2. 转换为使用条件运算符的第二个变量:

    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;
    }
    
  3. 使用预先填充了键/字符对的字典进行扭曲:

    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);
    }
    
  4. 注意:两个switch语句具有完全相同的情况,字典具有相同数量的字符。

    我期待1)和2)在性能上有些相似,而且3)会稍慢。

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

    1. 每次通话0.0000166毫秒
    2. 每次通话0.0000779毫秒
    3. 每次通话0.0000413毫秒
    4. 这怎么可能?条件运算符比if-else语句慢四倍,​​比字典查找慢几乎两倍。我在这里遗漏了一些必要的东西,或者条件算子本身是否很慢?

      更新1:关于我的测试工具的几句话。我在Visual Studio 2010中的 Release 编译的.Net 3.5项目下为上述每个变体运行以下(伪)代码。启用代码优化并关闭DEBUG / TRACE常量。在执行定时运行之前,我将测量方法运行一次以进行预热。 run方法执行大量迭代的方法,shift设置为true和false,并使用一组输入键:

      Run(method);
      var stopwatch = Stopwatch.StartNew();
      Run(method);
      stopwatch.Stop();
      var measure = stopwatch.ElapsedMilliseconds / iterations;
      

      Run方法如下所示:

      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:进一步深入研究,我已经研究了为1)和2)生成的IL,并发现主开关结构与我预期的相同,但是案例体略有差异。这是我正在看的IL:

      1)if / else statement:

      L_0167: ldarg.2 
      L_0168: brfalse.s L_0170
      
      L_016a: ldarg.1 
      L_016b: ldc.i4.s 0x42
      L_016d: stind.i2 
      L_016e: br.s L_0174
      
      L_0170: ldarg.1 
      L_0171: ldc.i4.s 0x62
      L_0173: stind.i2 
      
      L_0174: ldc.i4.1 
      L_0175: ret 
      

      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时分支,而if {else在shift为false时分支。
      • 虽然1)实际编译的指令多于2),但当shift为真或假时执行的指令数量相等,两者相等。
      • 1)的指令排序是任何时候只占用一个堆栈槽,而2)总是加载两个。

      这些观察中的任何一个意味着条件运算符的执行速度会变慢吗?是否有其他副作用发挥作用?

8 个答案:

答案 0 :(得分:12)

非常奇怪,也许.NET优化会在你的情况下适得其反:

  

作者拆解了几个   三元表达式的版本和   发现它们是相同的   if-statements,只有一个小的   区别。三元声明   有时产生测试的代码   与你相反的条件   期待,就像它测试那样   subexpression是false而不是   测试是否属实。这重新排序   一些指示和可以   偶尔会提升表现。

http://dotnetperls.com/ternary

你想要考虑枚举值上的ToString(对于非特殊情况):

string keyValue = inKey.ToString();
return shift ? keyValue : keyValue.ToLower();

修改
我将if-else方法与三元运算符进行了比较,并且在1000000个周期内,三元运算符总是至少与if-else方法一样快(有时快几毫秒,支持上面的文本)。我认为你在测量时间方面犯了一些错误。

答案 1 :(得分:11)

我很想知道您是否使用Debug或Release版本进行测试。如果它是一个调试版本,那么由于编译器在使用Release模式时添加的低级优化的LACK(或手动禁用调试模式并启用编译器优化),差异很可能是差异。

然而,我希望通过优化,三元运算符的速度与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慢,而之后,它更快。

编辑:

经过一些测试,在实际意义上,if / else和ternary之间几乎没有区别。虽然三元代码导致较小的IL,但它们的表现几乎相同。在具有释放模式二进制的十二种不同测试中,if / else和三元结果要么相同,要么在10,000,000次迭代中关闭几分之一毫秒。有时候if / else会稍快一些,有时甚至是三元组,但实际上,它们表现相同。

另一方面,

词典的表现要差得多。当谈到这些优化时,如果代码已经存在,我不会浪费时间在if / else和ternary之间进行选择。但是,如果您目前有字典实现,我肯定会重构它以使用更有效的方法,并将性能提高约400%(无论如何,对于给定的函数。)

答案 2 :(得分:4)

有趣的是,我开始在这里开发了一个小班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转储看到的示例练习...

答案 3 :(得分:3)

我希望#1和#2相同。优化器应该生成相同的代码。 #3中的字典预计会很慢,除非它以某种方式进行优化而不是实际使用散列。

在对实时系统进行编码时,我们总是使用查找表 - 一个简单的数组 - 进行翻译,如示例中所示。当输入范围相当小时,它是最快的。

答案 4 :(得分:2)

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

具体来说,我甚至认为你所优化的方法很好;它似乎可以在调用阶段变得更好(虽然我不能确定,因为你没有提供上下文)。

答案 5 :(得分:1)

假设您关注该方法的性能(如果不是,为什么还要发布它?),您应该考虑将char值存储在数组中并转换{{1}值到数组的索引。

答案 6 :(得分:0)

我手边没有VS,但肯定有一个简单的内置方法可以将密钥作为角色来获取?像toString方法之类的东西,所以你可以用这个替换那个怪异的switch

if (shift)
  return inKey.toString().toUppercase();
else
  return inKey.toString().toLowercase();

答案 7 :(得分:-1)

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