私が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;
}
でコンパイルした場合-Os
、このプログラムを実行するには0.38秒かかります。-O2
または-O3
。これらの時間は一貫して取得され、実質的にノイズはありません(gcc 4.7.2、x86_64 GNU / Linux、Intel Core i5-3320M)。
(更新:すべてのアセンブリコードをGitHubに移動しました:fno-align-*
フラグが同じ効果を持つため、投稿が肥大化し、質問にはほとんど価値がありません。)
残念ながら、アセンブリについての私の理解は非常に限られているため、次に何をしたのかがわかりません。アセンブリを取得し、-O2
すべての違いを行-Os
以外のアセンブリにマージしました。このコードはまだ0.38秒で実行され、唯一の違いはものです。.p2align
.p2align
私が正しく推測すれば、これらはスタックアライメントのパディングです。によるなぜのNOPとGCCパッド機能していますか?コードがより高速に実行されることを期待して行われますが、私の場合は明らかにこの最適化が裏目に出ました。
この場合の原因はパディングですか?なぜ、どうやって?
それによって生じるノイズは、タイミングのマイクロ最適化を不可能にします。
CまたはC ++ソースコードでマイクロ最適化(スタックアライメントとは無関係)を実行するときに、そのような偶然の幸運/不幸なアライメントが干渉しないようにするにはどうすればよいですか?
更新:
Pascal Cuoqの答えに続いて、私は調整を少しいじりました。-O2 -fno-align-functions -fno-align-loops
gccに渡すことで、すべて.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
次にadd()
、work()
0.37秒後のアセンブリを手動で移動します-O2
0.44秒
add()
コールサイトからの距離がとても重要だと私には思えます。私は試しましたperf
が、の出力perf stat
とperf report
私にはほとんど意味がありません。しかし、私はそれから1つの一貫した結果しか得ることができませんでした:
-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
上で与えられたものだけでなく、私のマシンでによって測定できるすべてのキャッシュミスを調べました。キャッシュミスは非常にノイズが多く、実行時間との相関はほとんどありません。