私はこの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()
subl
slow()
簡単なマイクロ ベンチマークでは、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 回抽出する」パターンを使用します。subl
1 つのメモリと 1 つのレジスタではなく 2 つのレジスタで発行するという最初の問題は、GCC のどちらのプラットフォームでも同じであるため、これは興味深いことですが、あまり関連性がありません。