我正在查看一些代码,其中包含一个巨大的switch语句和每个case上的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;
}
转换为使用条件运算符的第二个变量:
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次迭代进行预热然后定时的方法,令我惊讶的是我得到以下结果:
这怎么可能?条件运算符比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时分支。 shift
为真或假时执行的指令数量相等,两者相等。这些观察中的任何一个意味着条件运算符的执行速度会变慢吗?是否有其他副作用发挥作用?
答案 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 > 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转储看到的示例练习...
答案 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)
我会选择第三个选项,因为它更具可读性/可维护性。 我敢打赌,这段代码不是您应用程序性能的瓶颈。