我目前正试图理解我的一个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)
行应该无关紧要,因为操作系统应该在程序结束时自动关闭文件。但是,我注意到,我的程序运行时间为2.5秒,当我包含fclose语句时,当我注释掉它时,运行时间为5s。两个因素的差异!这并不是因为程序开始或结束时的延迟:在带有fclose语句的版本中,打印.
的速度明显更快。
我怀疑这可能与内存对齐或缓存丢失问题有关。如果我用另一个函数(如ftell )替换fclose,那么运行它也需要5s,如果我将large_buffer
的大小缩小为<= 8000元素,那么它总是在2.5秒内运行,不管是否存在fclose语句。
但我真的很想百分之百地确定这种奇怪行为背后的罪魁祸首是什么。是否有可能在某种类型的分析器或其他工具下运行我的程序,从而给我提供这些信息?到目前为止,我尝试在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
看起来在这两种情况下几乎没有数据缓存错误,就像kcache差一点所报告的那样,但是该程序的较慢版本的分支预测更差,指令缓存丢失和iTLB加载更多。这些差异中哪一个最有可能对测试用例之间运行时的2x差异负责?
编辑2:有趣的事实,很显然,如果我用一个NOP指令代替"fclose“调用,我仍然可以保持这种奇怪的行为。
编辑3:我的处理器是Intel i5-2310 (桑迪桥)
编辑4:结果表明,如果我通过编辑程序集文件来调整数组的大小,它不会变得更快。更重要的是,当我在C代码中更改它们的大小时,它变得更快的原因是gcc决定更改二进制代码中的内容的顺序。
编辑5:更多的证据表明,真正重要的是JMP指令的确切地址:如果我在代码开始时添加一个NOP (而不是printf),它会变得更快。类似地,如果我从代码开始时删除无用的指令,它也会变得更快。当我在gcc的不同版本上编译我的代码时,它也变得更快了,尽管生成的汇编代码是相同的。唯一的区别是一开始就有调试信息,二进制文件的各个部分的顺序不同。
发布于 2017-04-12 22:58:23
关键度量
你的罪魁祸首是树枝失手:
440,171,927 branch-misses # 13.85% of all branches
与
71,034,122 branch-misses # 2.24% of all branches
我不确定您运行的是哪个处理器,但是如果您假设在Haswell上运行一个2.5 GHz处理器,您将看到每个分支预测忽略了大约15个周期(通常更多一些,因为其他东西也停止运行),而且每个周期都是0.4ns。因此,0.4ns/循环* 15圈/错过分支* (440,171,927 - 71,034,122)分支丢失= 2.2秒。它将取决于您的确切处理器和机器代码,但这解释了其中的大部分差异。
致
不同芯片的分支预测算法是专有的,但是如果您在这里研究( http://www.agner.org/optimize/microarchitecture.pdf),您可以了解更多关于不同处理器及其局限性的知识。从本质上讲,您会发现,某些处理器在避免它们存储的分支预测表中的冲突方面做得更好,以便对是否被占用的分支进行预测。
那这有什么关系呢?所发生的事情(99%的可能性)是,通过稍微地重新安排程序,您可以根据内存位置改变完全不同的分支位置。在处理器的分支预测表中,有太多映射到同一个桶的映射。通过稍微更改可执行文件,这个问题就消失了。在这两个分支点之间必须有一个非常特定的距离才能触发这个问题。你不幸做到了这一点。
简单解
正如您所发现的,许多更改实际上会导致程序没有达到这种下降的性能。本质上,任何改变两个关键分支点之间距离的东西都会解决这个问题。您可以通过插入机器代码nops的16个字节(或足够将分支点移动到不同的桶)来实现这一点。你也可以像你所做的那样改变一些东西,这会破坏到一个非病理性病例的距离。
深入挖掘
如果你想真正了解在这种情况下是什么原因,你需要弄脏你的手。有趣的!您将需要选择多个工具中的一个来查找被错误预测的特定分支。这里有一个方法:如何度量Linux上单个分支的错误预测?
在您识别出错误的分支之后,您可以知道是否有一种方法可以删除该分支(无论如何,这几乎总是性能的一个好主意)。如果没有,您可以为它设置一个提示,或者在最坏的情况下,只需移动一些东西,以确保相同的条目不会像以前建议的那样被共享。
更广泛的经验教训
程序员低估了分支的成本(当编译器无法在编译时删除分支时)。正如您已经发现的,每个分支都给处理器的分支预测缓冲区增加了更大的压力,增加了错误预测的可能性。因此,即使是对处理器100%可预测的分支,也会减少用于预测其他分支的资源,从而降低性能。此外,当一个分支被错误地预测时,它至少要花费12-15个周期,而且如果所需的指令不在L1缓存、L2缓存、L3缓存或天堂帮助您的主内存中,则可能要高得多。此外,编译器不能跨分支进行所有优化。
发布于 2017-04-09 10:40:55
首先,您希望通过拆卸所有功能并确保唯一的区别在于主功能来检查是否正常。您可以使用objdump -d进行反汇编,并将其与diff进行比较。
fclose的添加引入了一个新的符号(因此文件的一部分已经被修改了),之后主功能也被修改了。这反过来会改变地址和偏移量。
因此,人们怀疑,在程序的前一个版本中没有出现过多的缓存破坏。
在您的问题中,您指出cache差制已被执行,但它报告了0%。这是不符合的。即使上述的怀疑是不正确的,你一定会得到几次错过无论如何。请粘贴两个结果。
在linux上使用的规范工具是perf ( https://perf.wiki.kernel.org/index.php/Tutorial )。确保运行几次,因为对于如此短的运行时,您将得到很大的差异。
您可以通过用
__attribute__((aligned(64)))
玩这个号码。
发布于 2017-04-09 23:12:13
可见的变化是时间上的变化,所以您应该首先将时间分析器瞄准运行,这两者都会显示和不显示行为,以查看时间花费在哪里的变化。如果幸运的话,您可以编译用于跟踪,并查看gprof可以吐出什么而不影响行为。
如果你真的只有一个强大的函数在你的程序中占了绝大部分,那么任何做时间累积的函数都不会产生非常细粒度的结果。在这种情况下,您可以尝试将一个超级函数分解成一个由较小的函数组成的网络,以便获得更细粒度的时间成本统计数据。
如果您运气不佳,并且为分析编译使行为差异消失,那么跳过分析内核函数调用(这可以动态完成),并查找内核函数调用跟踪而不是用户函数调用跟踪的时间差。
一旦你知道时间在哪里,你应该有更精确的问题要问,并有可能排除一些因素。在这一点上,您可能想要突破perf-工具。
https://stackoverflow.com/questions/43241895
复制相似问题