25

我正在寻找一些代码,其中包含一个巨大的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. 使用A捻字典预填充有键/字符对:

    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); 
    } 
    

注意:两个开关语句具有完全相同的情况下和字典具有字符的等量。

我在期待1)和2)在性能上有些类似,并且3)会稍微慢一些。

对于运行两次10.000.000迭代热身每个方法,然后定时,让我惊讶,我得到以下结果:每次通话

  1. 0.0000166毫秒每次通话
  2. 0.0000779毫秒
  3. 0.0000413毫秒每呼叫

这是怎么回事?条件运算符比if-else语句慢四倍,​​几乎比字典查找慢两倍。我在这里错过了一些必要的东西,还是条件运算符本身很慢?

更新1:有关我的测试设备的几句话。我在Visual Studio 2010中编译.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 

一些观察:

  • 而如果/ else分支时shift为假时shift等于true条件运算符分支。
  • 虽然1)实际上编译到大于2的几个更多的指令),当shift是真或假执行的指令数,等于两个。
  • 指令排序为1)是这样的,只有一个堆栈槽在所有时间被占用,而2)总是加载两项。

是否有任何这些观察结果暗示,条件运算符将执行慢?是否还有其他副作用?

+7

你的意思是“有条件的”运营商,是吗? – 2010-02-14 00:53:49

+7

正式的,它是“条件运算符”,但我经常听到它被称为“三元运算符”。据我所知,它是C#中唯一具有三个参数的运算符。那么谁来狡辩命名呢? :) – Nathan 2010-02-14 00:58:37

+1

我不知道“总是应该做”。我的第一个反应是首先看目标代码,以确保1 /和2 /的编译方式不同。接下来,你需要关心吗?即使它们现在没有用相同的高效代码编译,它们也可能在您的编译器的下一个版本中。你试图获得的知识至多暂时的价值。 – 2010-02-14 00:59:18

回答

12

很奇怪,也许是.NET优化backfireing你的情况:

笔者拆开几个 版本三元表情和 的发现,它们是相同的 if语句,用一个小 差异。三元语句 有时产生的代码测试 的相反条件,您会预期 ,因为它测试 子表达式为false,而不是 测试它是否为真。这种重新排序 一些指令,可以 偶尔提高性能。

http://dotnetperls.com/ternary

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

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

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

+2

我做了一些测试,当你扔进.ToString(),表现坦克。字符串操作是一些最慢的,并且像原始示例一样手动测试显式值和转换,而更详细的操作显着更快。 – jrista 2010-02-14 02:34:06

+0

是的ToString()不是最快的解决方案,但它会节省很多代码。如果性能是一个问题,请去ifs /运营商。 – Zyphrax 2010-02-14 13:42:18

+0

检查IL是我现在正在处理的事情。是的,我也希望看到与if/else和条件运算符相等的性能。但实际上,如果保存代码行是我的最终目标,那么我会去找我的字典示例,它干净而快速,适用于常规和特殊情况,无需额外的逻辑。 – 2010-02-14 20:35:27

3

我期望#1和#2是一样的。优化器应该产生相同的代码。在#3本字典将有望得到减缓,除非它以某种方式优化,而不是实际使用的哈希值。

当编码实时系统,我们总是用一个查表 - 一个简单的数组 - 翻译为您的示例中给出。当输入范围相当小时,这是最快的。

+0

我不明白你为什么被低估。操作系统误解了字典查询的速度应该是多快,而且你正在明确这一点。 – 2010-02-14 02:07:12

+0

@silky,谢谢!我不知道为什么我也是低调的。他们没有留下评论。 – 2010-02-14 13:01:39

+0

认真。我知道从词典样本中可以期待什么。我清楚地表明,我预计它会变慢。我的整个问题源于这样一个事实,即我的条件运算符样本比if/else和字典都慢得多。查找表是一个好的建议,我可能会在此基础上添加一个样本到我的测试平台。 – 2010-02-14 20:42:25

-1

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

+8

这并没有解决这个问题。问题不是应该选择哪个版本,问题是试图找出意想不到的结果的根本原因。 – Nathan 2010-02-14 01:03:00

4

有趣的是,我去了,并制定了小班IfElseTernaryTest这里,OK,代码是不是真的“优化”或很好的例子,但仍然...为讨论的缘故:

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 

如此看来短,即三元运营商显然是短,我愿意猜测,更快,因为更少的指令s被使用...但在此基础上,它似乎与您的情况#2相矛盾,这是令人惊讶的...

编辑:在Sky的评论中,暗示'code bloat for#2',这将反驳什么天空说! OK,代码就不同,背景不同,它是一个示范性锻炼检查IL转储看到...

+0

我'怀疑',你可能是对的,但你的榜样并不能证明这一点。它只能证明你编译的语句的代码优化得更小。这是一个简单的事情,编译给出的代码,并实际显示证明...... – 2010-02-14 02:15:07

2

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

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

+0

...对于一些n> N :-) – 2010-02-14 02:16:40

+0

我没有料到if/else NOR条件比字典慢,我表示,我希望字典查找速度较慢。将字典样本放在首位的原因仅仅是为了比较。 – 2010-02-14 20:44:37

11

我很想知道你是否正在用Debug或Release构建来测试它。如果它是一个调试版本,那么由于编译器在使用发布模式时添加的低级优化缺失(或手动禁用调试模式并启用编译器优化),所以差异很可能是不同的。

然而,我希望优化的是,三元运算符要么是相同的速度,要么比if/else语句快一点,而字典查找速度最慢。这里是我的结果10万元的热身迭代,接着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和ternary结果要么相同,要么在10,000,000次迭代中关闭几分之一毫秒。有时候,如果/ else稍微快一点,有时候是三元的,但实际上它们的表现也一样。

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

+0

我很好奇你如何构建你的字典样本。这里你的电话号码根本不会和我的电话号码相比。你用什么类型查找? – 2010-02-15 07:23:03

+0

我复制并粘贴了您的代码...与其相同。我的时间是所有1000万次迭代,而不是一个单独的请求(当你谈论几纳秒时,秒表的分辨率开始变得不准确,所以我发现测量整个过程是一个更有效的测试。) – jrista 2010-02-15 19:04:37

+0

你正在计数初始化你的字典里的字典时间?我不。 是的,我也用优化运行发布模式。 – 2010-02-22 00:22:14

1

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

+1

我很担心该方法的性能,为此,可以将其优化为可接受的度量。但这不是问题,问题在于为什么条件操作符执行比等效的if-else语句更糟的原因。 – 2010-02-22 03:05:02

+0

如果你问题的50%与你想回答的问题没有密切关系,那么忽略它并不会造成什么伤害。 – 2010-02-22 09:15:04

0

我没有VS,但肯定有一个简单的内置方式来获得作为一个字符的关键?就像一个toString方法,所以你可以替换滔天switch本:

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

......这会使代码变得简单和美观,但会完全破坏性能。这些都不是我的问题的焦点,但我正在寻找条件运算符执行比if/else语句慢的原因。 – 2010-02-22 00:24:53

+0

真的,它会破坏性能?你有没有测试过我发布的代码?当然,它可能会稍微慢一点,但在事情的宏伟计划中只有几个毫秒。 – DisgruntledGoat 2010-02-22 16:36:58