【发布时间】:2016-09-12 00:41:22
【问题描述】:
TL;DR:第一个循环在 Haswell CPU 上运行速度提高了约 18%。为什么?这些循环来自使用ptr++ 和++ptr 的gcc -O0(未优化)循环,但问题是为什么生成的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;
}
现在,第一个循环比第二个循环快约 18%,无论循环执行的顺序如何,ptr++ 的循环都比 ++ptr 的循环快。
为了运行我的基准测试,我只是收集了这些循环在不同 size 下的运行时间,并将它们都嵌套在其他循环中以频繁重复操作。
ASM 分析
查看 ASM 代码,第二个循环包含较少的指令,我们有 3 个 movl 和 2 个 addl,而在第一个循环中,我们有 4 个 movl 一个 addl 和一个 leal,所以我们有一个 movl 和 一个 leal而不是添加
LEA 计算正确地址的操作比ADD (+4) 方法快得多,这对吗?这是造成性能差异的原因吗?
据我所知,一旦在内存可以引用之前计算出新地址,就必须经过一些时钟周期,因此 addl $4,-12(%ebp) 之后的第二个循环需要稍等片刻才能继续,而在第一个循环中,我们可以立即引用内存,同时 LEAL 将计算下一个地址(这里是某种更好的流水线性能)。
这里有一些重新排序吗?我不确定我对这些循环的性能差异的解释,我能听听你的意见吗?
【问题讨论】:
-
请注意,由于
data - 1下溢,第二个循环可能是未定义的行为。 -
@EOF:你能详细说明一下吗?对我来说,它看起来像普通的指针数学......所以 ptr-1+1 = ptr+0,对我来说一切都很好。
-
@Ped7g:C++ 标准草案 N3337:5.7 加法运算符 [...] 如果指针操作数和结果都指向同一个数组对象的元素,或者最后一个元素之后对数组对象,求值不会产生溢出;否则,行为是未定义的。 虽然指向数组的最后一个元素过去存在特殊情况,但指向之前第一个元素。
-
@EOF:使用强制转换创建未对齐的指针也是未定义的行为,即使您不取消引用。但是,我们一直使用
__m128i类型来执行此操作。在大多数 C 实现(包括针对 x86 的 gcc)中,语义已经很好地定义了。有趣的是,OP 的习语在技术上是未定义的行为。如果有同样好的替代品,值得避免。 -
@Yakk:当然没有人关心一般未优化代码的速度。我应该试一试。将问题标题编辑回关于 asm,而不是 C,因为问题实际上是关于
gcc -O0恰好生成的 asm。 你不会学到任何关于如何编写更好/更高效的 C 的知识,但你可以学到一些关于现代 x86 微架构的管道。 事实证明,答案可能是资源冲突。
标签: c++ performance loops assembly x86