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