2011-07-19 51 views
8

所以在一位同事的建議下,我只是測試了三元運算符和等價的If-Else塊之間的速度差異......並且似乎三元運算符產生的代碼比If-Else快1倍到2倍。我的代碼是:C中的If-Else和Ternary運算符之間的速度差異?

gettimeofday(&tv3, 0); 
    for(i = 0; i < N; i++) 
    { 
    a = i & 1; 
    if(a) a = b; else a = c; 
    } 
    gettimeofday(&tv4, 0); 


    gettimeofday(&tv1, 0); 
    for(i = 0; i < N; i++) 
    { 
    a = i & 1; 
    a = a ? b : c; 
    } 
    gettimeofday(&tv2, 0); 

(對不起,使用gettimeofday的,而不是clock_gettime ...我會努力完善自己。)

我試圖改變我的定時到塊的順序,但結果似乎堅持下去。是什麼賦予了?另外,If-Else在執行速度方面表現出更多的可變性。我應該檢查gcc生成的程序集嗎?

順便說一句,這是所有在優化水平零(-O0)。

我想象一下,還是有我沒有考慮的東西,或者這是一個機器相關的東西,或什麼?任何幫助表示讚賞。

+0

更好,試試'a = i%2? b:c',然後與優化'-O2'或'-O3'進行比較。 –

+0

一個好的編譯器會注意到這相當於'a =(N&1)? c:b'。但是我在哪裏可以找到這樣的編譯器? (是的,是的,只要N> 0)。 –

+9

關閉優化的基準沒有意義...... –

回答

21

很有可能三元運算符編譯爲cmov而if/else結果爲cmp + jmp。只需看看裝配(使用-S)即可。在啓用優化的情況下,無論如何它都不會有任何問題,因爲任何優秀的編譯器都應該在這兩種情況下生成相同的代碼。

+8

在當今的CPU上,有條件的移動具有固定的延遲,而預測良好的條件分支基本上是空閒的。出於這個原因,你可能不會像你想象的那樣經常看到優化編譯器生成'CMOV'。 –

+0

提及彙編語言列表。 –

+0

GCC是否在'-O3'上優化它?懶惰測試:-) –

0

如果打開優化,任何體面的編譯器都應該爲這些代碼生成相同的代碼。

+1

海事組織這也應該是一個評論,考慮到前2個答案是實際答案 –

+0

你是對的,但是...其中一個雖然認真回答問題沒有提到,這只是一個問題' - O0'。編譯器在優化方面做得非常好,人們不應該像這樣做微觀優化。關閉。 –

8

這是一個很好的解釋:http://www.nynaeve.net/?p=178

基本上,有「條件組」處理器指令,其比支鏈和在單獨的指令設定得更快。

+0

非常不錯的鏈接! –

2

如果有,更改你的編譯器!

對於這類問題,我使用Try Out LLVM頁面。這是LLVM的舊版本(仍然使用gcc前端),但這些都是老花樣。

這裏是我的小樣本程序(簡化你的版本):

#include <stdio.h> 
#include <stdlib.h> 
#include <sys/time.h> 

int main (int argc, char* argv[]) { 
    int N = atoi(argv[0]); 

    int a = 0, d = 0, b = atoi(argv[1]), c = atoi(argv[2]); 

    int i; 
    for(i = 0; i < N; i++) 
    { 
    a = i & 1; 
    if(a) a = b+i; else a = c+i; 
    } 

    for(i = 0; i < N; i++) 
    { 
    d = i & 1; 
    d = d ? b+i : c+i; 
    } 

    printf("%d %d", a, d); 

    return 0; 
} 

而且有相應的LLVM IR生成:

define i32 @main(i32 %argc, i8** nocapture %argv) nounwind { 
entry: 
    %0 = load i8** %argv, align 8     ; <i8*> [#uses=1] 
    %N = tail call i32 @atoi(i8* %0) nounwind readonly ; <i32> [#uses=5] 

    %2 = getelementptr inbounds i8** %argv, i64 1 ; <i8**> [#uses=1] 
    %3 = load i8** %2, align 8      ; <i8*> [#uses=1] 
    %b = tail call i32 @atoi(i8* %3) nounwind readonly ; <i32> [#uses=2] 

    %5 = getelementptr inbounds i8** %argv, i64 2 ; <i8**> [#uses=1] 
    %6 = load i8** %5, align 8      ; <i8*> [#uses=1] 
    %c = tail call i32 @atoi(i8* %6) nounwind readonly ; <i32> [#uses=2] 

    %8 = icmp sgt i32 %N, 0       ; <i1> [#uses=2] 
    br i1 %8, label %bb, label %bb11 

bb:            ; preds = %bb, %entry 
    %9 = phi i32 [ %10, %bb ], [ 0, %entry ]  ; <i32> [#uses=2] 
    %10 = add nsw i32 %9, 1       ; <i32> [#uses=2] 
    %exitcond22 = icmp eq i32 %10, %N    ; <i1> [#uses=1] 
    br i1 %exitcond22, label %bb10.preheader, label %bb 

bb10.preheader:         ; preds = %bb 
    %11 = and i32 %9, 1        ; <i32> [#uses=1] 
    %12 = icmp eq i32 %11, 0      ; <i1> [#uses=1] 
    %.pn13 = select i1 %12, i32 %c, i32 %b   ; <i32> [#uses=1] 
    %tmp21 = add i32 %N, -1       ; <i32> [#uses=1] 
    %a.1 = add i32 %.pn13, %tmp21     ; <i32> [#uses=2] 
    br i1 %8, label %bb6, label %bb11 

bb6:            ; preds = %bb6, %bb10.preheader 
    %13 = phi i32 [ %14, %bb6 ], [ 0, %bb10.preheader ] ; <i32> [#uses=2] 
    %14 = add nsw i32 %13, 1      ; <i32> [#uses=2] 
    %exitcond = icmp eq i32 %14, %N     ; <i1> [#uses=1] 
    br i1 %exitcond, label %bb10.bb11_crit_edge, label %bb6 

bb10.bb11_crit_edge:        ; preds = %bb6 
    %15 = and i32 %13, 1       ; <i32> [#uses=1] 
    %16 = icmp eq i32 %15, 0      ; <i1> [#uses=1] 
    %.pn = select i1 %16, i32 %c, i32 %b   ; <i32> [#uses=1] 
    %tmp = add i32 %N, -1       ; <i32> [#uses=1] 
    %d.1 = add i32 %.pn, %tmp      ; <i32> [#uses=1] 
    br label %bb11 

bb11:            ; preds = %bb10.bb11_crit_edge, %bb10.preheader, %entry 
    %a.0 = phi i32 [ %a.1, %bb10.bb11_crit_edge ], [ %a.1, %bb10.preheader ], [ 0, %entry ] ; <i32> [#uses=1] 
    %d.0 = phi i32 [ %d.1, %bb10.bb11_crit_edge ], [ 0, %bb10.preheader ], [ 0, %entry ] ; <i32> [#uses=1] 
    %17 = tail call i32 (i8*, ...)* @printf(i8* noalias getelementptr inbounds ([6 x i8]* @.str, i64 0, i64 0), i32 %a.0, i32 %d.0) nounwind ; <i32> [#uses=0] 
    ret i32 0 
} 

好了,所以它可能是中國人,甚至儘管我繼續並重新命名了一些變量以使其更易於閱讀。

重要的位是這兩大塊:

%.pn13 = select i1 %12, i32 %c, i32 %b   ; <i32> [#uses=1] 
    %tmp21 = add i32 %N, -1       ; <i32> [#uses=1] 
    %a.1 = add i32 %.pn13, %tmp21     ; <i32> [#uses=2] 

    %.pn = select i1 %16, i32 %c, i32 %b   ; <i32> [#uses=1] 
    %tmp = add i32 %N, -1       ; <i32> [#uses=1] 
    %d.1 = add i32 %.pn, %tmp      ; <i32> [#uses=1] 

分別設置ad

而得出的結論是:沒有區別

注:在一個簡單的例子兩個變量實際上得到了合併,似乎這裏的優化沒有檢測到的相似...

7

你也可以去完全網點和衡量,如果這有什麼差別:

int m = -(i & 1); 
a = (b & m) | (c & ~m); 

在今天的架構,這種編程風格已經變得有些過時。

+1

當你在一個運行大量數字(比如說數十億次)的循環中有一個條件時,它仍然很有用。 – sumodds

0

瞭解它完全取決於編譯器如何解釋三元表達式(除非實際上強制它不與(內聯)asm)。它可以像內部表示語言一樣容易地理解三元表達式'if..else',並且根據目標後端,它可以選擇生成條件移動指令(在x86上,CMOVcc就是這樣一個,還應該有一個最小值/最大值,絕對值等)。使用條件移動的主要動機是將分支錯誤預測的風險轉移到內存/寄存器移動操作。對這條指令的警告是幾乎所有的時候,條件加載的操作數寄存器將不得不被下載到寄存器形式以利用cmov指令。

這意味着無條件評估過程現在必須是無條件的,這似乎會增加程序無條件路徑的長度。但是要明白,分支預測失誤最常被解析爲「刷新」管道,這意味着將會完成執行的指令被忽略(轉向無操作指令)。這意味着由於停頓或NOP,執行的指令的實際數量會更高,並且效果會隨着處理器管道的深度和錯誤預測率而變化。

這在確定正確的啓發式時帶來了一個有趣的困境。首先,我們確實知道,如果管道太淺或分支預測完全能夠從分支歷史中學習模式,那麼cmov就不值得這樣做。如果評價條件性論證的成本高於平均預測誤差的成本,那麼這也不值得。

這些可能是編譯器難以利用cmov指令的核心原因,因爲啓發式確定在很大程度上取決於運行時分析信息。在JIT編譯器上使用它會更有意義,因爲它可以提供運行時檢測反饋併爲使用此反饋建立更強的啓發式(「分支是否真的不可預測?」)。在沒有訓練數據或分析器的靜態編譯器一側,很難假定它何時有用。但是,如前所述,如果編譯器知道數據集完全是隨機的或者強制cond,那麼簡單的否定啓發式就是如此。第二。評估是昂貴的(可能是由於不可分割的,高成本的操作,如fp分割),這樣做會產生很好的啓發式效果。

任何值得它的鹽的編譯器都可以做到。問題是,在所有可靠的啓發式方法已經用完之後,它會做什麼...

相關問題