C++ 为什么asm中的这种差异会影响性能(在未优化的ptr和ptr循环中)?

C++ 为什么asm中的这种差异会影响性能(在未优化的ptr和ptr循环中)?,c++,performance,loops,assembly,x86,C++,Performance,Loops,Assembly,X86,TL;DR:第一个循环在Haswell CPU上运行快约18%。为什么?这些循环来自gcc-O0(未优化)循环,使用ptr++vs++ptr,但问题是为什么生成的asm表现不同,而不是如何编写更好的C 假设我们有两个循环: movl $0, -48(%ebp) //Loop counter set to 0 movl $_data, -12(%ebp) //Pointer to the data array movl %eax, -96(%eb

TL;DR:第一个循环在Haswell CPU上运行快约18%。为什么?这些循环来自
gcc-O0
(未优化)循环,使用
ptr++
vs
++ptr
,但问题是为什么生成的asm表现不同,而不是如何编写更好的C


假设我们有两个循环:

    movl    $0, -48(%ebp)     //Loop counter set to 0
    movl    $_data, -12(%ebp) //Pointer to the data array
    movl    %eax, -96(%ebp)
    movl    %edx, -92(%ebp)
    jmp L21
L22:
    // ptr++
    movl    -12(%ebp), %eax   //Get the current address
    leal    4(%eax), %edx     //Calculate the next address
    movl    %edx, -12(%ebp)   //Store the new (next) address
    // rest of the loop is the same as the other
    movl    -48(%ebp), %edx   //Get the loop counter to edx
    movl    %edx, (%eax)      //Move the loop counter value to the CURRENT address, note -12(%ebp) contains already the next one
    addl    $1, -48(%ebp)     //Increase the counter
L21:
    cmpl    $999999, -48(%ebp)
    jle     L22
第二个:

    movl    %eax, -104(%ebp)
    movl    %edx, -100(%ebp)
    movl    $_data-4, -12(%ebp) //Address of the data - 1 element (4 byte)
    movl    $0, -48(%ebp)       //Set the loop counter to 0
    jmp L23
L24:
    // ++ptr
    addl    $4, -12(%ebp)       //Calculate the CURRENT address by adding one sizeof(int)==4 bytes
    movl    -12(%ebp), %eax     //Store in eax the address
    // rest of the loop is the same as the other
    movl    -48(%ebp), %edx     //Store in edx the current loop counter
    movl    %edx, (%eax)        //Move the loop counter value to the current stored address location
    addl    $1, -48(%ebp)       //Increase the loop counter
L23:
    cmpl    $999999, -48(%ebp)
    jle L24
这些循环所做的事情完全相同,但方式略有不同,有关详细信息,请参阅注释

此ASM代码是由以下两个C++循环生成的:

    //FIRST LOOP:
    for(;index<size;index++){
        *(ptr++) = index;
    }
    //SECOND LOOP:
    ptr = data - 1;
    for(index = 0;index<size;index++){
        *(++ptr) = index;
    }
//第一个循环:

对于(;index首先,对
-O0
编译器输出的性能分析通常不是很有趣或有用


计算正确地址的LEAL操作比ADDL(+4)方法快得多,这是正确的吗?这是性能差异的原因吗

不,
add
可以在任何x86 CPU上的每个ALU执行端口上运行。
lea
通常具有简单寻址模式的低延迟,但吞吐量不高。在Atom上,它运行在与普通ALU指令不同的管道阶段,因为它实际上符合其名称,并在该端口上使用AGU它的结构

查看TagWiki,了解不同微体系结构(尤其是)上的代码是如何变慢或变快的

add
只会更糟,因为它让gcc
-O0
通过将其与内存目标一起使用,然后从该目标加载,从而使代码变得更糟。


使用
-O0
编译甚至不会尝试使用最好的指令来完成这项工作。例如,您将获得
mov$0,%eax
,而不是您总是在优化代码中获得的
xor%eax,%eax
。您不应该从未优化的编译器输出中推断出什么是好的

-O0
代码总是充满瓶颈,通常是在加载/存储或存储转发时。不幸的是,IACA没有考虑存储转发延迟,因此它没有意识到这些循环实际上是在瓶颈上


据我所知,在内存被引用之前,一旦计算出一个新地址,一些时钟周期必须经过,因此addl$4,-12(%ebp)之后的第二个循环需要等待一段时间才能继续

是的,
-12(%ebp)
mov
加载在作为
add
读修改写的一部分的加载后大约6个周期内不会准备就绪

而在第一个循环中,我们可以立即引用内存

同时,LEAL将计算下一个地址

没有

您的分析很接近,但您忽略了一个事实,即下一次迭代仍然必须加载我们存储在
-12(%ebp)
中的值。因此循环携带的依赖链长度相同,下一次迭代的
lea
实际上不能比使用
add
在循环中更快开始


延迟问题可能不是环路吞吐量瓶颈: 需要考虑uop/执行端口吞吐量。在这种情况下,OP的测试表明它实际上是相关的(或资源冲突引起的延迟)

当gcc
-O0
实现
ptr++
时,它会将旧值保存在寄存器中,就像您所说的。因此,存储地址会提前知道得更多,并且需要AGU的加载uop也会减少一个

假设采用Intel SnB系列CPU:

## ptr++: 1st loop
movl    -12(%ebp), %eax   //1 uop (load)
leal    4(%eax), %edx     //1 uop (ALU only)
movl    %edx, -12(%ebp)   //1 store-address, 1 store-data
//   no load from -12(%ebp) into %eax
... rest the same.


 ## ++ptr:  2nd loop
addl    $4, -12(%ebp)       // read-modify-write: 2 fused-domain uops.  4 unfused: 1 load + 1 store-address + 1 store-data
movl    -12(%ebp), %eax     // load: 1 uop.   ~6 cycle latency for %eax to be ready
... rest the same
因此,第二个循环的指针增量部分还有一个加载uop。可能是AGU吞吐量(地址生成单元)上的代码瓶颈。IACA说arch=SNB就是这种情况,但HSW在存储数据吞吐量(不是AGU)上存在瓶颈

然而,在不考虑存储转发延迟的情况下,IACA说第一个循环可以每3.5个周期运行一次迭代,而第二个循环可以每4个周期运行一次迭代。这比
addl$1,-48(%ebp)的6个周期循环的依赖性要快
loop counter,表示循环因延迟而受到限制,小于最大AGU吞吐量(资源冲突可能意味着它实际运行速度低于每6c一次迭代,见下文)

我们可以检验这个理论: 在关键路径之外的
lea
版本中添加额外的加载uop会占用更多吞吐量,但不会成为循环延迟链的一部分

movl    -12(%ebp), %eax   //Get the current address
leal    4(%eax), %edx     //Calculate the next address
movl    %edx, -12(%ebp)   //Store the new (next) address

mov     -12(%ebp), %edx 
%edx
即将被
mov
覆盖,因此对该加载的结果没有依赖关系。(由于注册重命名,
mov
的目标仅为写,因此它会断开依赖关系链。)

因此,此额外负载将使
lea
循环达到与
add
循环相同数量和风格的UOP,但延迟不同。
。如果额外负载对速度没有影响,我们知道第一个循环在负载/存储吞吐量方面不会受到限制


更新:OP的测试确认,额外未使用的负载将
lea
循环的速度降低到与
add
循环的速度大致相同

当我们没有遇到执行端口吞吐量瓶颈时,为什么额外的UOP很重要 UOP是按最早的一阶进行调度的(操作数就绪的UOP中的一个),而不是按关键路径一阶进行调度的。本可以在以后的备用周期中完成的额外UOP实际上会延迟关键路径上的UOP(例如,循环携带依赖项的一部分)。这称为资源冲突,可能会增加关键路径的延迟

i、 e.与等待关键路径延迟导致加载端口无所事事的周期不同,未使用的加载将在其加载地址准备就绪的最旧加载时运行。这将延迟其他加载

类似地,在
add
循环中,额外负载为p