C ++:将一个操作数保存在一个寄存器中,这个速度非常巨大

我一直想通过使用下面的代码来计算一个对数组元素进行扩展和求和的例程,来了解在L1caching中的数组对数组的影响。(我知道我应该通过' a“;关键是在循环中同时执行乘法和加法操作 – 到目前为止,编译器还没有计算出”a“):

double sum(double a,double* X,int size) { double total = 0.0; for(int i = 0; i < size; ++i) { total += a*X[i]; } return total; } #define KB 1024 int main() { //Approximately half the L1 cache size of my machine int operand_size = (32*KB)/(sizeof(double)*2); printf("Operand size: %d\n", operand_size); double* X = new double[operand_size]; fill(X,operand_size); double seconds = timer(); double result; int n_iterations = 100000; for(int i = 0; i < n_iterations; ++i) { result = sum(3.5,X,operand_size); //result += rand(); } seconds = timer() - seconds; double mflops = 2e-6*double(n_iterations*operand_size)/seconds; printf("Vector size %d: mflops=%.1f, result=%.1f\n",operand_size,mflops,result); return 0; } 

请注意,timer()和fill()例程并不包括在内; 如果你想运行代码,可以在这里find完整的代码:

http://codepad.org/agPWItZS

现在,这里是有趣的地方。 这是输出:

 Operand size: 2048 Vector size 2048: mflops=588.8, result=-67.8 

这是完全没有caching的性能,尽pipeX的所有元素都应该在循环迭代之间保存在caching中。 查看由以下产生的汇编代码:

 g++ -O3 -S -fno-asynchronous-unwind-tables register_opt_example.cpp 

我注意到sum函数循环中有一个奇怪的地方:

 L55: movsd (%r12,%rax,8), %xmm0 mulsd %xmm1, %xmm0 addsd -72(%rbp), %xmm0 movsd %xmm0, -72(%rbp) incq %rax cmpq $2048, %rax jne L55 

说明:

  addsd -72(%rbp), %xmm0 movsd %xmm0, -72(%rbp) 

表示将sum()中的“total”值存储在堆栈中,并在每次循环迭代中读写。 我修改了程序集,使这个操作数保存在一个寄存器中:

 ... addsd %xmm0, %xmm3 ... 

这个小小的改变带来了巨大的性能提升:

 Operand size: 2048 Vector size 2048: mflops=1958.9, result=-67.8 

tl; dr我的问题是:为什么更换一个寄存器的单个内存位置访问,加速代码这么多,因为单个位置应该存储在L1caching? 什么架构因素使这成为可能? 看起来很奇怪的是,反复写入堆栈位置会彻底破坏caching的有效性。

附录

我的gcc版本是:

 Target: i686-apple-darwin10 Configured with: /var/tmp/gcc/gcc-5646.1~2/src/configure --disable-checking --enable-werror --prefix=/usr --mandir=/share/man --enable-languages=c,objc,c++,obj-c++ --program-transform-name=/^[cg][^.-]*$/s/$/-4.2/ --with-slibdir=/usr/lib --build=i686-apple-darwin10 --with-gxx-include-dir=/include/c++/4.2.1 --program-prefix=i686-apple-darwin10- --host=x86_64-apple-darwin10 --target=i686-apple-darwin10 Thread model: posix gcc version 4.2.1 (Apple Inc. build 5646) (dot 1) 

我的CPU是:

英特尔至强X5650

这可能是一个更长的依赖链,加上错误预测*的组合。


更长的依赖链:

首先,我们确定关键的依赖path。 然后我们看看optimize/instruction_tables.pdf (第117页)提供的指令延迟

在未优化版本中,关键依赖path是:

  • addsd -72(%rbp), %xmm0
  • movsd %xmm0, -72(%rbp)

在内部,它可能分解成:

  • 负载(2个周期)
  • 添加(3个周期)
  • 存储(3个周期)

如果我们看一下优化版本,那只是:

  • 添加(3个周期)

所以你有8个周期与3个周期。 几乎是3倍。

我不确定Nehalem处理器系列对存储负载的依赖程度有多敏感,以及转发能力如何。 但有理由相信这不是零。


加载存储错误预测:

现代处理器以更多的方式使用预测。 其中最着名的可能是分支预测 。 其中一个不太为人所知的是Load Prediction。

当一个处理器看到一个负载,它会立即加载它,在所有等待写入完成之前。 它会假定这些写入不会与加载的值冲突。

如果之前的写入与负载发生冲突,则必须重新执行加载并将计算回滚到加载点。 (与分支错误预测回滚的方式大致相同)

这里是如何相关的:

不用说,现代处理器将能够同时执行该循环的多次迭代。 因此,处理器将在前一次迭代完成存储( movsd %xmm0, -72(%rbp) )之前尝试执行加载( addsd -72(%rbp), %xmm0)

结果? 之前的商店与负载冲突 – 因此是一个错误的预测和回滚。

*请注意,我不确定名称“负载预测”。 我只是在英特尔的文档中看到它,似乎没有给它命名。

我会推测问题不是在caching/内存访问,而是在处理器(执行你的代码)。 这里有几个可见的瓶颈。

这里的演出号码是根据我使用的盒子(sandybridge或westmere)

标量math的峰值性能是2.7Ghz x2 FLOPS / Clock x2,因为处理器可以同时进行加法和乘法运算。 代码的理论效率是0.6 /(2.7 * 2)= 11%

所需带宽:每个(+)和(x) – > 4个字节/ Flop 4个字节2个双倍* 5.4GFLOPS = 21.6GB / s

如果你知道它最近读取它可能在L1(89GB / s),L2(42GB / s)或L3(24GB / s),所以我们可以排除caching的B / W

内存的系统速度为18.9 GB / s,即使在主内存中,峰值性能也应该接近18.9 / 21.6GB / s = 87.5%

  • 可能希望尽早地批量请求(通过展开)

即使有推测执行,tot + = a * X [i]加法将被序列化,因为tot(n)需要在tot(n + 1)被启动之前被评估

首先展开循环
移动我8的做

 {//your func for( int i = 0; i < size; i += 8 ){ tot += a * X[i]; tot += a * X[i+1]; ... tot += a * X[i+7]; } return tot } 

使用多个累加器
这将打破依赖关系,并允许我们避免在添加pipe道上停滞

 {//your func// int tot,tot2,tot3,tot4; tot = tot2 = tot3 = tot4 = 0 for( int i = 0; i < size; i += 8 ) tot += a * X[i]; tot2 += a * X[i+1]; tot3 += a * X[i+2]; tot4 += a * X[i+3]; tot += a * X[i+4]; tot2 += a * X[i+5]; tot3 += a * X[i+6]; tot4 += a * X[i+7]; } return tot + tot2 + tot3 + tot4; } 

更新在SandyBridge框上运行这个后,我有权访问:(2.7GHZ SandyBridge与-O2 -march = native -mtune = native

原始码:

 Operand size: 2048 Vector size 2048: mflops=2206.2, result=61.8 2.206 / 5.4 = 40.8% 

改进的代码:

 Operand size: 2048 Vector size 2048: mflops=5313.7, result=61.8 5.3137 / 5.4 = 98.4% 

我实际上不能重现这一点,因为我的编译器(gcc 4.7.2)总是保存在一个寄存器。

我怀疑缓慢的主要原因不一定与一级caching有关,而是由于商店之间的数据依赖关系

 movsd %xmm0, -72(%rbp) 

以及后续迭代的负载:

 addsd -72(%rbp), %xmm0