我觉得你的分析基本是对的。一些笔记。
1.
流水线在这里是错误的词;您在这里看到的是数据依赖性。CPU 流水线将单个指令拆分为多个步骤,然后可以同时执行连续指令的不同步骤。另一方面,数据依赖性是指在前一条指令的结果可用之前,一条指令无法开始执行的情况。该指令仍将被流水线化,并且各个流水线阶段将一直执行,直到需要不可用的先前结果的阶段:这是由于数据依赖性而导致的流水线停顿。
你这里对数据依赖的分析是对的;树版本应该相同或更快。但是,您关于编译器将做什么的问题很复杂。
2.如果数字是浮点数,则两个版本不相同,因此不允许编译器将顺序版本更改为树版本(如果这样做将是一个错误)。通常,您必须指定一些选项,例如-ffast-math
指示非等效代数浮点表达式转换是可以接受的。
处理器的内部细节也很重要。在第一个版本中,即使连续指令之间存在依赖关系,如果只有一个算术单元,也不会出现停顿。
3.我的笔记本电脑 (OSX) 上的 clang (5.1) 版本对此代码执行以下操作:
#include <iostream>
using namespace std;
int main(int argc, char** argv) {
typedef double T; // Also try with long
T M[8] = {1,2,3,4,5,6,7,8};
// Don't let the compiler compute the sum of M's at compile time.
for (int i = 1; i <= 8; ++i)
if (argc > i) M[i-1] = atof(argv[i]);
// T S1 = M[0] + M[1] + M[2] + M[3] + M[4] + M[5] + M[6] + M[7];
// cout << S1 << endl;
T S2 = (((M[0] + M[1]) + (M[2] + M[3])) + ((M[4] + M[5]) + (M[6] + M[7])));
cout << S2 << endl;
return 0;
}
树版本变成这样:
-> 0x100000de0: addsd %xmm6, %xmm7
0x100000de4: addsd %xmm5, %xmm1
0x100000de8: addsd %xmm7, %xmm1
0x100000dec: addsd %xmm4, %xmm3
0x100000df0: addsd %xmm2, %xmm0
0x100000df4: addsd %xmm3, %xmm0
0x100000df8: addsd %xmm1, %xmm0
(前两个级别的添加实际上已经顺序化了)并且顺序版本变成了这样:
-> 0x100000de8: addsd %xmm7, %xmm2
0x100000dec: addsd %xmm6, %xmm2
0x100000df0: addsd %xmm4, %xmm2
0x100000df4: addsd %xmm5, %xmm2
0x100000df8: addsd %xmm1, %xmm2
0x100000dfc: addsd %xmm3, %xmm2
0x100000e00: addsd %xmm0, %xmm2
这是带有标志的-O3 -ffast-math -std=c++11 -g
。为了获得汇编输出,我在 下运行程序lldb
,在感兴趣的行设置断点break
并使用命令dis
。
很明显,我的 clang 版本不会像那样重写表达式。
4.至于性能,显然第二个版本至少应该一样快。但是,您必须在实际拥有的功能上实际尝试此操作。如果你发现性能提升微不足道,那就不值得考虑了。
5.举个例子,如果你将要添加的值的向量存储在内存中,那么从内存中获取所有值所需的时间远远大于实际添加它们所需的时间。在分层内存中,对主内存(而不是寄存器或 L1 缓存)的访问速度要慢得多,以至于这些微优化带来的性能提升只是舍入错误。大多数时候 CPU 不会执行任何操作,只会坐在那里等待数据到达。