私は現在、私の C プログラムの 1 つで非常に奇妙な動作を理解しようとしています。どうやら、一見取るに足らない行を最後に追加または削除すると、プログラムの残りの部分のパフォーマンスに大きな影響を与えるようです。
私のプログラムは次のようになります。
int large_buffer[10000];
void compute(FILE * input) {
for(int i=0; i<100; i++) {
do_lots_of_stuff();
printf(".");
fflush(stdout);
}
}
int main() {
FILE *input = fopen("input.txt", "r");
compute(input);
fclose(input); // <--- everything gets 2x slower if I comment this out (!)
return 0;
}
理論的にfclose(input)
は、OS はプログラムの最後でファイルを自動的に閉じる必要があるため、main 関数の最後の行は重要ではありません。ただし、fclose ステートメントを含めた場合はプログラムの実行に 2.5 秒かかり、コメントアウトした場合は 5 秒かかりました。2倍の差!これは、プログラムの開始時または終了時の遅延によるものではありません。出力される速度は.
、fclose ステートメントを使用したバージョンの方が目に見えて高速です。
これは、メモリ アライメントまたはキャッシュ ミスの問題に関係している可能性があると思われます。fclose を ftell などの別の関数に置き換えると、実行に 5 秒かかり、要素のサイズlarge_buffer
を <= 8000 要素に減らすと、fclose ステートメントがあるかどうかに関係なく、常に 2.5 秒で実行されます。
しかし、この奇妙な動作の背後にある犯人が何であるかを 100% 確信できるようにしたいと考えています。私のプログラムをある種のプロファイラーまたはその情報を提供する他のツールで実行することは可能でしょうか? これまでのところ、両方のバージョンを実行してみましvalgrind --tool=cachegrind
たが、プログラムの両方のバージョンで同じ量のキャッシュ ミス (0%) が報告されました。
編集1:私のプログラムの両方のバージョンを実行した後perf stat -d -d -d
、次の結果が得られました:
Performance counter stats for './no-fclose examples/bench.o':
5625.535086 task-clock (msec) # 1.000 CPUs utilized
38 context-switches # 0.007 K/sec
0 cpu-migrations # 0.000 K/sec
54 page-faults # 0.010 K/sec
17,851,853,580 cycles # 3.173 GHz (53.23%)
6,421,955,412 stalled-cycles-frontend # 35.97% frontend cycles idle (53.23%)
4,919,383,925 stalled-cycles-backend # 27.56% backend cycles idle (53.23%)
13,294,878,129 instructions # 0.74 insn per cycle
# 0.48 stalled cycles per insn (59.91%)
3,178,485,061 branches # 565.010 M/sec (59.91%)
440,171,927 branch-misses # 13.85% of all branches (59.92%)
4,778,577,556 L1-dcache-loads # 849.444 M/sec (60.19%)
125,313 L1-dcache-load-misses # 0.00% of all L1-dcache hits (60.22%)
12,110 LLC-loads # 0.002 M/sec (60.25%)
<not supported> LLC-load-misses
<not supported> L1-icache-loads
20,196,491 L1-icache-load-misses (60.22%)
4,793,012,927 dTLB-loads # 852.010 M/sec (60.18%)
683 dTLB-load-misses # 0.00% of all dTLB cache hits (60.13%)
3,443 iTLB-loads # 0.612 K/sec (53.38%)
90 iTLB-load-misses # 2.61% of all iTLB cache hits (53.31%)
<not supported> L1-dcache-prefetches
51,382 L1-dcache-prefetch-misses # 0.009 M/sec (53.24%)
5.627225926 seconds time elapsed
Performance counter stats for './yes-fclose examples/bench.o':
2652.609254 task-clock (msec) # 1.000 CPUs utilized
15 context-switches # 0.006 K/sec
0 cpu-migrations # 0.000 K/sec
57 page-faults # 0.021 K/sec
8,277,447,108 cycles # 3.120 GHz (53.39%)
2,453,171,903 stalled-cycles-frontend # 29.64% frontend cycles idle (53.46%)
1,235,728,409 stalled-cycles-backend # 14.93% backend cycles idle (53.53%)
13,296,127,857 instructions # 1.61 insn per cycle
# 0.18 stalled cycles per insn (60.20%)
3,177,698,785 branches # 1197.952 M/sec (60.20%)
71,034,122 branch-misses # 2.24% of all branches (60.20%)
4,790,733,157 L1-dcache-loads # 1806.046 M/sec (60.20%)
74,908 L1-dcache-load-misses # 0.00% of all L1-dcache hits (60.20%)
15,289 LLC-loads # 0.006 M/sec (60.19%)
<not supported> LLC-load-misses
<not supported> L1-icache-loads
140,750 L1-icache-load-misses (60.08%)
4,792,716,217 dTLB-loads # 1806.793 M/sec (59.93%)
1,010 dTLB-load-misses # 0.00% of all dTLB cache hits (59.78%)
113 iTLB-loads # 0.043 K/sec (53.12%)
167 iTLB-load-misses # 147.79% of all iTLB cache hits (53.44%)
<not supported> L1-dcache-prefetches
29,744 L1-dcache-prefetch-misses # 0.011 M/sec (53.36%)
2.653584624 seconds time elapsed
kcachegrind が報告したように、どちらの場合もデータ キャッシュ ミスはほとんどないように見えますが、遅いバージョンのプログラムでは分岐予測が悪化し、命令キャッシュ ミスと iTLB ロードが多くなりました。これらの違いのうち、テスト ケース間の実行時間の 2 倍の違いの原因となる可能性が最も高いのはどれですか?
編集 2: おもしろい事実ですが、「fclose」呼び出しを単一の NOP 命令に置き換えても、奇妙な動作を維持できるようです。
編集 3: 私のプロセッサは Intel i5-2310 (Sandy Bridge) です
編集 4: アセンブリ ファイルを編集して配列のサイズを変更すると、高速にならないことがわかりました。C コードでサイズを変更したときに高速になった理由は、gcc がバイナリ内のものの順序を再配置することを決定したためだと思われます。
編集 5: 重要なのは JMP 命令の正確なアドレスであるというさらなる証拠: コードの先頭に単一の NOP (printf の代わりに) を追加すると、高速になります。同様に、コードの先頭から不要な命令を削除すると、速度も向上します。また、別のバージョンの gcc でコードをコンパイルすると、生成されたアセンブリ コードは同じであるにもかかわらず、高速になりました。唯一の違いは、最初のデバッグ情報と、バイナリ ファイルのセクションの順序が異なっていたことです。