我目前正在努力理解我一个C程序中的一些非常奇怪的行为。显然,在程序的末尾添加或删除一个看似不重要的行会极大地影响程序其他部分的性能。
我的程序大致像这样:
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)
语句应该没有作用,因为操作系统应该会在程序结束时自动关闭文件。但是我发现,当我包含了fclose语句时,程序运行需要2.5秒,而当我注释掉它时则需要5秒,相差了两倍!而且这不是由于程序开始或结束时的延迟所导致的:在有fclose语句的版本中,打印.
的速度明显更快。我怀疑这可能与某些内存对齐或缓存未命中问题有关。如果我将fclose替换为另一个函数(例如ftell),那么运行时间也是5秒;如果我将
large_buffer
的大小减小到<=8000个元素,则无论是否有fclose语句,程序总是以2.5秒的速度运行。但我真的很想确定这种奇怪行为的罪魁祸首是什么。有没有可能在某种性能分析器或其他工具下运行我的程序,以便给我提供这些信息?到目前为止,我已经尝试使用
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:有趣的事实是,如果我用单个NOP指令替换“fclose”调用,我仍然可以保留奇怪的行为。编辑3:我的处理器是Intel i5-2310(Sandy Bridge)。编辑4:结果证明,如果我通过编辑汇编文件来调整数组大小,它不会变快。显然,当我在C代码中更改它们的大小时,它变快的原因是gcc决定重新排列二进制文件中的内容的顺序。编辑5:更多的证据表明重要的是JMP指令的精确地址:如果我在代码开头添加一个单独的NOP(而不是printf),它就会变快。同样,如果我从代码开头删除一个无用的指令它也会变得更快。当我在不同版本的gcc上编译我的代码时,它也变得更快,尽管生成的汇编代码相同。唯一的区别是开始处的调试信息和二进制文件的部分顺序不同。
fclose
之后运行速度更快,而不是更慢。 - Daniel Jourdo_lots_of_stuff()
。 - dbush