我一直试图通过使用以下代码对一个 routine 进行计时,从而了解将数组放在一级缓存和内存中会产生什么影响(我知道我应该在最后用‘a’对结果进行zoom ;重点是在循环内既做乘法又做加法-到目前为止,编译器还没有计算出‘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() routine ;如果您想运行代码,可以在此处找到它们的完整源代码:

http://codepad.org/agPWItZS

现在,这里是它变得有趣的地方.这是输出:

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

这是完全未缓存的性能,尽管在循环迭代之间X的所有元素都应该被保存在缓存中.查看以下程序生成的汇编代码:

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"值,并在每次循环迭代时读取和写入该值.我修改了程序集,以便将此操作数保存在a寄存器中:

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

这一小小的变化带来了huge%的性能提升:

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

tl;dr个 我的问题是:既然单个内存位置应该存储在L1缓存中,为什么用寄存器替换单个内存位置访问会使代码速度如此之快?是什么架构因素使这成为可能?重复写入一个堆栈位置会完全 destruct 缓存的有效性,这似乎非常奇怪.

Appendix

我的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

推荐答案

这可能是一个较长的依赖链,以及负载预测失误*的组合.


Longer Dependency Chain:

首先,我们确定关键的依赖路径.然后我们看一下:http://www.agner.org/optimize/instruction_tables.pdf提供的指令延迟(第117页)

在非优化版本中,关键依赖项路径为:

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

在内部,它可能分为以下几个部分:

  • 负载(2个循环)
  • addsd(3个周期)
  • 存储(3个周期)

如果我们看一下优化后的版本,它只是:

  • addsd(3个周期)

所以你有8个周期而不是3个周期.几乎是3倍.

我不确定Nehalem处理器线对存储负载依赖性有多敏感,以及它的性能如何.但我们有理由相信它不是零.


Load-store Misprediction:

现代处理器以你能想象的更多方式使用预测.其中最著名的大概是Branch Prediction个.其中一个鲜为人知的是负荷预测.

当处理器看到加载时,它会在所有挂起的写入完成之前立即加载它.它将假定这些写入不会与加载的值冲突.

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

它在这里的相关性:

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

结果是什么呢?前一次存储与加载冲突,因此是预测错误和回滚.

*请注意,我不确定"负载预测"这个名称.我只在英特尔文档中读到过,他们似乎没有给出它的名字

C++相关问答推荐

-O and -wrap的行为

错误:C中需要参数声明符

为什么写入系统调用打印的字符数不正确?

如何判断宏参数是否为C语言中的整型文字

C中是否有语法可以直接初始化一个常量文本常量数组的 struct 成员?

无法用C++编译我的单元测试

当输入负数时,排序算法存在问题

防止C++中递归函数使用堆栈内存

为什么Fread()函数会读取内容,然后光标会跳到随机位置?

链表删除 node 错误

不确定如何处理此编译错误

C-try 将整数和 struct 数组存储到二进制文件中

合并对 struct 数组进行排序

Valgrind用net_pton()抱怨

向左移位3如何得到以字节为单位的位数?

被调用方函数内部的C-Struct变量,它是指针还是无关紧要

&stdbool.h&q;在嵌入式系统中的使用

变量值不正确的问题

为什么这个代码的最后一次迭代不能正常工作?

为什么 Linux 共享库 .so 在内存中可能比在磁盘上大?