27

我正在查看一些代码,其中每个案例都有一个巨大的 switch 语句和一个 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);
    }
    

注意:这两个 switch 语句具有完全相同的大小写,并且字典具有相同数量的字符。

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

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

  1. 每次调用 0.0000166 毫秒
  2. 每次调用 0.0000779 毫秒
  3. 每次调用 0.0000413 毫秒

怎么会这样?条件运算符比 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 语句:

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) 总是加载两个。

这些观察是否暗示条件运算符的执行速度会变慢?还有其他副作用起作用吗?

4

8 回答 8

13

很奇怪,在您的情况下,.NET 优化可能适得其反:

作者拆解了几个版本的三元表达式,发现它们和if语句是一样的,只有一点点不同。三元语句有时会生成测试您期望的相反条件的代码,因为它测试子表达式是否为假而不是测试它是否为真。这会重新排序一些指令,有时可以提高性能。

http://dotnetperls.com/ternary

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

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

编辑:
我已经将 if-else 方法与三元运算符和 1000000 个周期进行了比较,三元运算符始终至少与 if-else 方法一样快(有时快几毫秒,这支持上面的文本)。我认为您在测量花费的时间时犯了某种错误。

于 2010-02-14T01:06:20.683 回答
11

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

但是,我希望通过优化,三元运算符与 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 和三元之间几乎没有区别。虽然三进制代码会产生更小的 IL,但它们的性能几乎相同。在使用发布模式二进制文件的十几个不同测试中,if/else 和三进制结果要么相同,要么在 10,000,000 次迭代中相差几分之一毫秒。有时 if/else 会稍微快一些,有时是三元的,但实际上,它们的性能是一样的。

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

于 2010-02-14T02:08:15.863 回答
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 转储以查看...

于 2010-02-14T01:40:57.360 回答
3

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

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

于 2010-02-14T00:59:20.443 回答
2

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

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

于 2010-02-14T02:06:08.473 回答
1

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

于 2010-02-14T18:44:24.317 回答
0

我手头没有VS,但肯定有一种简单的内置方法可以将钥匙作为角色吗?类似toString方法的东西,所以你可以用这个来代替那个可怕的东西switch

if (shift)
  return inKey.toString().toUppercase();
else
  return inKey.toString().toLowercase();
于 2010-02-17T13:28:03.150 回答
-1

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

于 2010-02-14T00:59:24.437 回答