24

私はこのCコードをコンパイルしています:

int mode; // use aa if true, else bb
int aa[2];
int bb[2];

inline int auto0() { return mode ? aa[0] : bb[0]; }
inline int auto1() { return mode ? aa[1] : bb[1]; }

int slow() { return auto1() - auto0(); }
int fast() { return mode ? aa[1] - aa[0] : bb[1] - bb[0]; }

slow()関数と関数はどちらもfast()同じことを行うことを意図していますがfast()、2 つではなく 1 つの分岐ステートメントで実行します。GCC が 2 つのブランチを 1 つに折りたたむかどうかを確認したかったのです。-O2、-O3、-Os、-Ofast などのさまざまなレベルの最適化を使用して、GCC 4.4 および 4.7 でこれを試しました。常に同じ奇妙な結果が得られます。

スロー():

        movl    mode(%rip), %ecx
        testl   %ecx, %ecx
        je      .L10

        movl    aa+4(%rip), %eax
        movl    aa(%rip), %edx
        subl    %edx, %eax
        ret
.L10:
        movl    bb+4(%rip), %eax
        movl    bb(%rip), %edx
        subl    %edx, %eax
        ret

速い():

        movl    mode(%rip), %esi
        testl   %esi, %esi
        jne     .L18

        movl    bb+4(%rip), %eax
        subl    bb(%rip), %eax
        ret
.L18:
        movl    aa+4(%rip), %eax
        subl    aa(%rip), %eax
        ret

実際、各関数で生成される分岐は 1 つだけです。ただし、驚くべき方法で劣っているようです。各ブランチでとslow()に対して 1 つの余分な負荷を使用します。コードは、最初にレジスタにロードせずに、s のメモリから直接使用します。そのため、呼び出しごとに 1 つの追加レジスタと 1 つの追加命令を使用します。aa[0]bb[0]fast()sublslow()

簡単なマイクロ ベンチマークでは、fast()10 億回の呼び出しに 0.7 秒かかるのに対し、slow(). 2.9 GHz で Xeon E5-2690 を使用しています。

なぜこれが必要なのですか?GCC がより適切に機能するように、ソース コードを微調整できますか?

編集: Mac OS での clang 4.2 の結果は次のとおりです。

スロー():

        movq    _aa@GOTPCREL(%rip), %rax   ; rax = aa (both ints at once)
        movq    _bb@GOTPCREL(%rip), %rcx   ; rcx = bb
        movq    _mode@GOTPCREL(%rip), %rdx ; rdx = mode
        cmpl    $0, (%rdx)                 ; mode == 0 ?
        leaq    4(%rcx), %rdx              ; rdx = bb[1]
        cmovneq %rax, %rcx                 ; if (mode != 0) rcx = aa
        leaq    4(%rax), %rax              ; rax = aa[1]
        cmoveq  %rdx, %rax                 ; if (mode == 0) rax = bb
        movl    (%rax), %eax               ; eax = xx[1]
        subl    (%rcx), %eax               ; eax -= xx[0]

速い():

        movq    _mode@GOTPCREL(%rip), %rax ; rax = mode
        cmpl    $0, (%rax)                 ; mode == 0 ?
        je      LBB1_2                     ; if (mode != 0) {
        movq    _aa@GOTPCREL(%rip), %rcx   ;   rcx = aa
        jmp     LBB1_3                     ; } else {
LBB1_2:                                    ; // (mode == 0)
        movq    _bb@GOTPCREL(%rip), %rcx   ;   rcx = bb
LBB1_3:                                    ; }
        movl    4(%rcx), %eax              ; eax = xx[1]
        subl    (%rcx), %eax               ; eax -= xx[0]

興味深い: clangslow()fast()! 一方、slow()は 3 つのロード (うち 2 つは投機的、1 つは不要) に対して 2 つを実行しfast()ます。fast()実装はより「明白」であり、GCC と同様に短く、使用するレジスタが 1 つ少なくなります。

Mac OS 上の GCC 4.7 は、通常、Linux と同じ問題を抱えています。それでも、Mac OS の Clang と同じ「8 バイトをロードしてから 4 バイトを 2 回抽出する」パターンを使用します。subl1 つのメモリと 1 つのレジスタではなく 2 つのレジスタで発行するという最初の問題は、GCC のどちらのプラットフォームでも同じであるため、これは興味深いことですが、あまり関連性がありません。

4

3 に答える 3

9

GCC がコードを思い通りに最適化できない理由についての答えはありませんが、同様のパフォーマンスを達成するためにコードを再編成する方法があります。slow()またはで行った方法でコードを編成する代わりに、ブランチを必要とせずにまたはに基づいfast()て返すインライン関数を定義することをお勧めします。aabbmode

inline int * xx () { static int *xx[] = { bb, aa }; return xx[!!mode]; }
inline int kwiky(int *xx) { return xx[1] - xx[0]; }
int kwik() { return kwiky(xx()); }

GCC 4.7 でコンパイルした場合-O3:

    movl    mode, %edx
    xorl    %eax, %eax
    testl   %edx, %edx
    setne   %al
    movl    xx.1369(,%eax,4), %edx
    movl    4(%edx), %eax
    subl    (%edx), %eax
    ret

の定義により、次のようにxx()再定義できます。auto0()auto1()

inline int auto0() { return xx()[0]; }
inline int auto1() { return xx()[1]; }

slow()そして、これから、 が と類似または同一のコードにコンパイルされることがわかるはずですkwik()

于 2013-09-23T06:21:28.527 に答える