条件运算符是否缓慢?

我正在查看一些带有巨大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); } 

注意:两个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)如果/ 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时条件运算符分支,而当shift为false时if / else分支。
  • 虽然1)实际上编译的指令多于2),但是当shift为true或false时执行的指令数对于两者是相等的。
  • 1)的指令排序是任何时候只占用一个堆栈槽,而2)总是加载两个。

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

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

作者反汇编了三个版本的三元表达式,发现它们与if语句完全相同,只有一个小的区别。 三元语句有时会产生代码来测试您期望的相反条件,因为它测试子表达式是否为false而不是测试是否为真。 这重新排序了一些指令,偶尔可以提高性能。

http://dotnetperls.com/ternary

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

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

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

我很想知道你是否使用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%(无论如何,对于给定的函数)。

有意思的是,我在这里开发了一个小类IfElseTernaryTest ,好吧,代码并没有真正“优化”或很好的例子,但是为了讨论起见:

 public class IfElseTernaryTest { private bool bigX; public void RunIfElse() { int x = 4; int y = 5; if (x > y) bigX = false; else if (x < y) bigX = true; } public void RunTernary() { int x = 4; int y = 5; bigX = (x > y) ? false : ((x < 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 > 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 < 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 > y) ? false : ((x < 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转储看到的示例练习…

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

在编写实时系统时,我们总是使用一个查找表 – 一个简单的数组 – 来翻译你的例子中给出的。 当输入范围相当小时,它是最快的。

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

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

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

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

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

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