2009 年に初めて、GCC (少なくとも私のプロジェクトとマシンでは) では、速度 (または)ではなくサイズ( ) を最適化すると、著しく高速なコードが生成される傾向があることに気付きました。それ以来、その理由を疑問に思っていました。-Os
-O2
-O3
私は、この驚くべき動作を示し、ここに投稿できるほど小さい (かなりばかげた) コードを作成することができました。
const int LOOP_BOUND = 200000000;
__attribute__((noinline))
static int add(const int& x, const int& y) {
return x + y;
}
__attribute__((noinline))
static int work(int xval, int yval) {
int sum(0);
for (int i=0; i<LOOP_BOUND; ++i) {
int x(xval+sum);
int y(yval+sum);
int z = add(x, y);
sum += z;
}
return sum;
}
int main(int , char* argv[]) {
int result = work(*argv[1], *argv[2]);
return result;
}
でコンパイルすると、このプログラムの実行に 0.38 秒かかり、または-Os
でコンパイルすると 0.44 秒かかります。これらの時間は一貫して得られ、実質的にノイズはありません (gcc 4.7.2、x86_64 GNU/Linux、Intel Core i5-3320M)。-O2
-O3
(更新:アセンブリコードはすべてGitHubfno-align-*
: 投稿が肥大化し、フラグが同じ効果を持つため、質問にほとんど価値を追加していないようです。
残念ながら、アセンブリに関する私の理解は非常に限られているため、次にやったことが正しかったかどうかはわかりません。アセンブリを取得し、行を除いて-O2
すべての違いをアセンブリにマージしました。結果-Os
.p2align
こここのコードは依然として 0.38 秒で実行され、唯一の違いはその .p2align
内容です。
私の推測が正しければ、これらはスタックの整列のためのパディングです。GCC が NOP で関数を埋め込むのはなぜですか?これは、コードの実行速度が速くなることを期待して行われますが、どうやらこの最適化は私のケースでは裏目に出たようです。
この場合、パディングが原因なのでしょうか? なぜ、どのように?
それが出すノイズのせいで、タイミングのマイクロ最適化はほぼ不可能になります。
C または C++ ソース コードでマイクロ最適化 (スタック アライメントとは無関係) を実行するときに、このような偶発的な幸運な / 不運なアライメントが干渉しないようにするにはどうすればよいでしょうか。
アップデート:
続くパスカル・クオックの回答私はアラインメントを少しいじりました。gcc-O2 -fno-align-functions -fno-align-loops
に渡すことで、.p2align
アセンブリからすべてがなくなり、生成された実行ファイルは0.38秒で実行されます。gcc ドキュメント:
-Os はすべての -O2 最適化を有効にします [ただし] -Os は次の最適化フラグを無効にします:
-falign-functions -falign-jumps -falign-loops -falign-labels -freorder-blocks -freorder-blocks-and-partition -fprefetch-loop-arrays
つまり、これは(不正確な)位置合わせの問題のように思えます。
私はまだ懐疑的-march=native
ですマラト・ドゥカンの回答。これが(不正確な)位置合わせの問題に干渉しているだけではないということには納得がいきません。私のマシンにはまったく影響がありません。(それでも、私は彼の回答に賛成票を投じました。)
更新2:
-Os
これを図から取り出すことができます。以下の時間は、
-O2 -fno-omit-frame-pointer
0.37秒-O2 -fno-align-functions -fno-align-loops
0.37秒-S -O2
その後、 0.37秒add()
後に手動でアセンブリを移動しますwork()
-O2
0.44秒
add()
呼び出しサイトからのの距離が非常に重要であるように思われます。 を試してみましたが、とperf
の出力はほとんど意味をなさないようです。ただし、一貫した結果は 1 つしか得られませんでした。perf stat
perf report
-O2
:
602,312,864 stalled-cycles-frontend # 0.00% frontend cycles idle
3,318 cache-misses
0.432703993 seconds time elapsed
[...]
81.23% a.out a.out [.] work(int, int)
18.50% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
100.00 ¦ lea (%rdi,%rsi,1),%eax
¦ }
¦ ? retq
[...]
¦ int z = add(x, y);
1.93 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
79.79 ¦ add %eax,%ebx
のためにfno-align-*
:
604,072,552 stalled-cycles-frontend # 0.00% frontend cycles idle
9,508 cache-misses
0.375681928 seconds time elapsed
[...]
82.58% a.out a.out [.] work(int, int)
16.83% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
51.59 ¦ lea (%rdi,%rsi,1),%eax
¦ }
[...]
¦ __attribute__((noinline))
¦ static int work(int xval, int yval) {
¦ int sum(0);
¦ for (int i=0; i<LOOP_BOUND; ++i) {
¦ int x(xval+sum);
8.20 ¦ lea 0x0(%r13,%rbx,1),%edi
¦ int y(yval+sum);
¦ int z = add(x, y);
35.34 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
39.48 ¦ add %eax,%ebx
¦ }
のために-fno-omit-frame-pointer
:
404,625,639 stalled-cycles-frontend # 0.00% frontend cycles idle
10,514 cache-misses
0.375445137 seconds time elapsed
[...]
75.35% a.out a.out [.] add(int const&, int const&) [clone .isra.0] ¦
24.46% a.out a.out [.] work(int, int)
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
18.67 ¦ push %rbp
¦ return x + y;
18.49 ¦ lea (%rdi,%rsi,1),%eax
¦ const int LOOP_BOUND = 200000000;
¦
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ mov %rsp,%rbp
¦ return x + y;
¦ }
12.71 ¦ pop %rbp
¦ ? retq
[...]
¦ int z = add(x, y);
¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
29.83 ¦ add %eax,%ebx
add()
遅いケースでは、呼び出しが遅れているようです。
私は、上記に示した統計情報だけでなく、自分のマシンで出力できるものすべてを調べました。perf -e
同じ実行可能ファイルの場合、stalled-cycles-frontend
実行時間との線形相関が見られます。これほど明確に相関するものは他には見当たりませんでした。(stalled-cycles-frontend
異なる実行可能ファイルを比較しても意味がありません。)
最初のコメントとして出てきたキャッシュ ミスも含めました。私は、上記に示したものだけでなく、自分のマシンで測定できるすべてのキャッシュ ミスを調べましたperf
。キャッシュ ミスは非常にノイズが多く、実行時間との相関はほとんどまたはまったくありません。
ベストアンサー1
デフォルトでは、コンパイラは「平均的な」プロセッサ向けに最適化します。プロセッサごとに異なる命令シーケンスが優先されるため、 で有効にされたコンパイラ最適化は-O2
平均的なプロセッサにはメリットがありますが、特定のプロセッサではパフォーマンスが低下する可能性があります ( にも同じことが当てはまります)。同じ例を異なるプロセッサで試してみると、一部のプロセッサでは のメリットがあり、他のプロセッサでは の最適化の方が有利である-Os
ことがわかります。-O2
-Os
time ./test 0 0
複数のプロセッサでの結果は次のとおりです(ユーザー時間が報告されています)。
Processor (System-on-Chip) Compiler Time (-O2) Time (-Os) Fastest
AMD Opteron 8350 gcc-4.8.1 0.704s 0.896s -O2
AMD FX-6300 gcc-4.8.1 0.392s 0.340s -Os
AMD E2-1800 gcc-4.7.2 0.740s 0.832s -O2
Intel Xeon E5405 gcc-4.8.1 0.603s 0.804s -O2
Intel Xeon E5-2603 gcc-4.4.7 1.121s 1.122s -
Intel Core i3-3217U gcc-4.6.4 0.709s 0.709s -
Intel Core i3-3217U gcc-4.7.3 0.708s 0.822s -O2
Intel Core i3-3217U gcc-4.8.1 0.708s 0.944s -O2
Intel Core i7-4770K gcc-4.8.1 0.296s 0.288s -Os
Intel Atom 330 gcc-4.8.1 2.003s 2.007s -O2
ARM 1176JZF-S (Broadcom BCM2835) gcc-4.6.3 3.470s 3.480s -O2
ARM Cortex-A8 (TI OMAP DM3730) gcc-4.6.3 2.727s 2.727s -
ARM Cortex-A9 (TI OMAP 4460) gcc-4.6.3 1.648s 1.648s -
ARM Cortex-A9 (Samsung Exynos 4412) gcc-4.6.3 1.250s 1.250s -
ARM Cortex-A15 (Samsung Exynos 5250) gcc-4.7.2 0.700s 0.700s -
Qualcomm Snapdragon APQ8060A gcc-4.8 1.53s 1.52s -Os
場合によっては、gcc
特定のプロセッサ向けに最適化するように要求することで、不利な最適化の影響を軽減できます (オプション-mtune=native
またはを使用-march=native
)。
Processor Compiler Time (-O2 -mtune=native) Time (-Os -mtune=native)
AMD FX-6300 gcc-4.8.1 0.340s 0.340s
AMD E2-1800 gcc-4.7.2 0.740s 0.832s
Intel Xeon E5405 gcc-4.8.1 0.603s 0.803s
Intel Core i7-4770K gcc-4.8.1 0.296s 0.288s
更新: Ivy Bridge ベースの Core i3 では、gcc
( 4.6.4
、、 ) の 3 つのバージョン4.7.3
で4.8.1
パフォーマンスが大きく異なるバイナリが生成されますが、アセンブリ コードにはわずかな違いしかありません。これまでのところ、この事実について説明はありません。
アセンブリ元gcc-4.6.4 -Os
(実行時間 0.709 秒):
00000000004004d2 <_ZL3addRKiS0_.isra.0>:
4004d2: 8d 04 37 lea eax,[rdi+rsi*1]
4004d5: c3 ret
00000000004004d6 <_ZL4workii>:
4004d6: 41 55 push r13
4004d8: 41 89 fd mov r13d,edi
4004db: 41 54 push r12
4004dd: 41 89 f4 mov r12d,esi
4004e0: 55 push rbp
4004e1: bd 00 c2 eb 0b mov ebp,0xbebc200
4004e6: 53 push rbx
4004e7: 31 db xor ebx,ebx
4004e9: 41 8d 34 1c lea esi,[r12+rbx*1]
4004ed: 41 8d 7c 1d 00 lea edi,[r13+rbx*1+0x0]
4004f2: e8 db ff ff ff call 4004d2 <_ZL3addRKiS0_.isra.0>
4004f7: 01 c3 add ebx,eax
4004f9: ff cd dec ebp
4004fb: 75 ec jne 4004e9 <_ZL4workii+0x13>
4004fd: 89 d8 mov eax,ebx
4004ff: 5b pop rbx
400500: 5d pop rbp
400501: 41 5c pop r12
400503: 41 5d pop r13
400505: c3 ret
アセンブリ元gcc-4.7.3 -Os
(実行時間 0.822 秒):
00000000004004fa <_ZL3addRKiS0_.isra.0>:
4004fa: 8d 04 37 lea eax,[rdi+rsi*1]
4004fd: c3 ret
00000000004004fe <_ZL4workii>:
4004fe: 41 55 push r13
400500: 41 89 f5 mov r13d,esi
400503: 41 54 push r12
400505: 41 89 fc mov r12d,edi
400508: 55 push rbp
400509: bd 00 c2 eb 0b mov ebp,0xbebc200
40050e: 53 push rbx
40050f: 31 db xor ebx,ebx
400511: 41 8d 74 1d 00 lea esi,[r13+rbx*1+0x0]
400516: 41 8d 3c 1c lea edi,[r12+rbx*1]
40051a: e8 db ff ff ff call 4004fa <_ZL3addRKiS0_.isra.0>
40051f: 01 c3 add ebx,eax
400521: ff cd dec ebp
400523: 75 ec jne 400511 <_ZL4workii+0x13>
400525: 89 d8 mov eax,ebx
400527: 5b pop rbx
400528: 5d pop rbp
400529: 41 5c pop r12
40052b: 41 5d pop r13
40052d: c3 ret
アセンブリ元gcc-4.8.1 -Os
(実行時間 0.994 秒):
00000000004004fd <_ZL3addRKiS0_.isra.0>:
4004fd: 8d 04 37 lea eax,[rdi+rsi*1]
400500: c3 ret
0000000000400501 <_ZL4workii>:
400501: 41 55 push r13
400503: 41 89 f5 mov r13d,esi
400506: 41 54 push r12
400508: 41 89 fc mov r12d,edi
40050b: 55 push rbp
40050c: bd 00 c2 eb 0b mov ebp,0xbebc200
400511: 53 push rbx
400512: 31 db xor ebx,ebx
400514: 41 8d 74 1d 00 lea esi,[r13+rbx*1+0x0]
400519: 41 8d 3c 1c lea edi,[r12+rbx*1]
40051d: e8 db ff ff ff call 4004fd <_ZL3addRKiS0_.isra.0>
400522: 01 c3 add ebx,eax
400524: ff cd dec ebp
400526: 75 ec jne 400514 <_ZL4workii+0x13>
400528: 89 d8 mov eax,ebx
40052a: 5b pop rbx
40052b: 5d pop rbp
40052c: 41 5c pop r12
40052e: 41 5d pop r13
400530: c3 ret