当前位置: 首页 > 知识库问答 >
问题:

C++:在寄存器中保留一个操作数的惊人加速

奚翰海
2023-03-14

我一直试图通过使用以下代码(我知道我应该在最后按'a'缩放结果;重点是在循环中同时执行乘法和加法--到目前为止,编译器还没有想出将'a'除以系数)对一个数组放在L1缓存中和内存中的影响进行计时(我知道我应该在最后按'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()例程;如果要运行代码,可以在这里找到它们的完整源代码:

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
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)

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

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

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

Operand size: 2048
Vector size 2048: mflops=1958.9, result=-67.8
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

共有1个答案

令狐功
2023-03-14

这可能是一个更长的依赖链和负载错误预测的组合*。

更长的依赖链:

首先,我们识别关键依赖路径。然后我们查看以下文件提供的指令延迟:http://www.agner.org/optimize/instruction_tables.pdf(第117页)

  • AddSD-72(%RBP)、%XMM0
  • movsd%xmm0,-72(%rbp)

在内部,它可能分为:

  • 加载(2个周期)
  • ADDSD(3个周期)
  • 存储(3个周期)
    null

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

我不确定Nehalem处理器线对存储-加载依赖性有多敏感,以及它的转发性能有多好。但有理由相信它不是零。

加载存储错误预测:

当处理器看到一个加载时,它会在所有挂起的写操作完成之前立即加载它。它将假设那些写入不会与加载的值冲突。

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

它在这里有何相关性:

*注意,我不确定“Load Prediction”这个名称。我只在Intel的文档中读到过它,他们似乎没有给它起名字。

 类似资料:
  • Vim 可以将不同字段剪切或复制到不同寄存器中,您可以从不同寄存器中取出内容后粘贴 "寄存器名称 按下"键和另一个字符键,便可以定义一个寄存器。例如:"a "1 定义寄存器后直接进行操作 "ayy 将当前行复制到寄存器 a 中 "ap 将寄存器 a 中的内容粘贴到光标之后 :registers 查看所有寄存器的内容 通常情况下,寄存器 + (先按",再按 Shift+= )对应

  • 我相信我了解linux x86-64 ABI如何使用寄存器和堆栈将参数传递给函数(参见前面的ABI讨论)。我感到困惑的是,在函数调用中是否/哪些寄存器应该保留。也就是说,哪些寄存器被保证不被破坏?

  • 问题:执行相应行后,用保存在相应寄存器中的值填充间隙。以十六进制和32位输入所有值。 我的想法是:我是大会新手。我知道EAX、EBX、ECX、EDX、ESI、EDI、ESP或EBP等值适用于任何32位寄存器。或AX、BX、CX或DX等值适用于任何16位寄存器。我读过xor eax,eax-将eax的内容设置为零。这意味着第一个间隙是0x00000000,对吗?第二个gap将0x12345678复制

  • 问题内容: 我有一对多关系的两个表。假设对于表格中的每一行,表格中可以有0或更多行引用。 客户想要知道引用某行中有多少行,对于中的所有行。 我可以使用以下查询完成此操作: 但是,如果表和很大,该怎么办?假设有100万行,并且有1000万行。我们还要说,其中99%的行的引用计数少于1000行。假设客户通常一次请求大约100行。 我应该将朴素的count()查询与外键上的索引一起使用,还是最好保留一个

  • 在wikipedia x86调用约定中,它说对于Microsoft x64调用约定: 寄存器RBX、RBP、RDI、RSI、RSP、R12、R13、R14和R15被视为非易失性(被叫方保存)。 但对于System V AMD64 ABI: 如果被调用方希望使用寄存器RBX、RBP和R12-R15,则必须在将控制权返回给调用方之前恢复它们的原始值。 我的问题是,在不同的平台上调用约定是不是不同的?(

  • Windows上是否有任何方法可以解决XMM寄存器保留在函数调用中的要求?(除了将其全部写入汇编中) 不幸的是,我有许多AVX2内在函数因此而臃肿。 例如,这将被编译器(MSVC)放置在函数的顶部: 00007FF9D0EBC602 vmovaps xmmword ptr[rsp 1490h]、xmm6 00007FF9 D0EBC60B vmovaps XMMWORDPTR[rsp 1480h]