11

私は現在、私の 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 でコードをコンパイルすると、生成されたアセンブリ コードは同じであるにもかかわらず、高速になりました。唯一の違いは、最初のデバッグ情報と、バイナリ ファイルのセクションの順序が異なっていたことです。

4

3 に答える 3