It is内联,但没有优化掉,因为你编译了-O0
(默认)。这会生成用于一致调试的 asm,允许您modify任何 C++ 变量在任何行的断点处停止。
这意味着编译器在每个语句之后都会溢出寄存器中的所有内容,并重新加载下一个语句所需的内容。因此,表达相同逻辑的语句越多 = 代码速度越慢,无论它们是否在同一个函数中。为什么 clang 对于这个简单的浮点和(使用 -O0)会产生低效的 asm? https://stackoverflow.com/questions/53366394/why-does-clang-produce-inefficient-asm-for-this-simple-floating-point-sum-with更详细地解释。
通常情况下-O0
不会内联函数,但它确实尊重__attribute__((always_inline))
.
最终分配的 C 循环优化帮助 https://stackoverflow.com/questions/32000917/c-loop-optimization-help-for-final-assignment/32001196#32001196解释了为什么进行基准测试或调整-O0
完全没有意义。这两个版本对于性能来说都是可笑的垃圾。
如果没有内联,就会有一个call
在循环内调用它的指令。
asm 实际上是在寄存器中创建指针const WrappedDouble& left
and right
。 (效率非常低,使用多条指令而不是一条指令lea
. The addq %rdx, %rax
是其中一个的最后一步。)
然后它将这些指针参数溢出到堆栈内存,因为它们是真正的变量并且必须位于调试器可以修改它们的内存中。就是这样movq %rax, -16(%rbp)
and %rdx
... 是在做。
重新加载并取消引用这些指针后,addsd
(添加标量双精度)结果本身溢出回堆栈内存中的本地movsd %xmm0, -8(%rbp)
。这不是一个命名变量,而是函数的返回值。
然后重新加载并再次复制到另一个堆栈位置,最后arr
and i
从堆栈加载,以及double
的结果operator+
,并且被存储到arr[i]
with movq %rsi, (%rax,%rdx,8)
。 (是的,LLVM 使用了 64 位整数mov
复制一个double
那时。早期使用SSE2movsd
.)
返回值的所有这些副本都位于循环携带依赖链的关键路径上,因为下一次迭代读取arr[i-1]
.与 3 或 4 周期 FP 相比,大约 5 或 6 周期存储转发延迟确实会增加add
潜伏。
显然这是大规模地效率低下。启用优化后,gcc 和 clang 可以毫无困难地内联和优化您的包装器。
他们还通过保留arr[i]
结果在一个寄存器中用作arr[i-1]
导致下一次迭代。这避免了大约 6 个周期的存储转发延迟,如果它使 asm 与源一样,则该延迟将出现在循环内。
即优化后的 asm 看起来有点像 C++:
double tmp = arr[0]; // kept in XMM0
for(...) {
tmp += arr[i]; // no re-read of mmeory
arr[i] = tmp;
}
有趣的是, clang 不费心去初始化它的tmp
(xmm0
) 在循环之前,因为您不必费心初始化数组。奇怪的是它没有警告 UB。实际操作中大malloc
使用 glibc 的实现将为您提供来自操作系统的新页面,并且它们都将保留零,即0.0
。但是 clang 会给你 XMM0 中剩下的一切!如果您添加一个((double*)arr)[0] = 1;
, clang 将加载循环之前的第一个元素。
不幸的是,编译器不知道如何比前缀和计算做得更好。看与 SSE 的并行前缀(累积)和 https://stackoverflow.com/questions/19494114/parallel-prefix-cumulative-sum-with-sse and Intel cpu 上的 SIMD 前缀和 https://stackoverflow.com/questions/10587598/simd-prefix-sum-on-intel-cpu了解如何将其速度加快 2 倍,和/或使其并行化。
我更喜欢 Intel 语法,但是Godbolt 编译器浏览器 https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(j:1,lang:c%2B%2B,source:'%23include+%3Ccstdio%3E%0A%23include+%3Ccstdlib%3E%0A%0A%23define+INLINE+__attribute__((always_inline))+inline%0A%0Astruct+alignas(8)+WrappedDouble+%7B%0A++++double+value%3B%0A%0A++++INLINE+friend+const+WrappedDouble+operator%2B(const+WrappedDouble%26+left,+const+WrappedDouble%26+right)+%7B%0A++++++++return+%7Bleft.value+%2B+right.value%7D%3B%0A++++%7D%3B%0A%7D%3B%0A%0A%23define+doubleType+WrappedDouble+//+either+%22double%22+or+%22WrappedDouble%22%0A%0Aint+main()+%7B%0A++++int+N+%3D+100000000%3B%0A++++doubleType*+arr+%3D+(doubleType*)malloc(sizeof(doubleType)*N)%3B%0A++++//+((double*)arr)%5B0%5D+%3D+1%3B%0A++++//+with+no+array+init,+clang+doesn!'t+bother+to+init+its+XMM0%0A++++for+(int+i+%3D+1%3B+i+%3C+N%3B+i%2B%2B)+%7B%0A++++++++arr%5Bi%5D+%3D+arr%5Bi+-+1%5D+%2B+arr%5Bi%5D%3B%0A++++%7D%0A%0A++++free(arr)%3B%0A++++printf(%22done%5Cn%22)%3B%0A%0A++++return+0%3B%0A%7D'),l:'5',n:'0',o:'C%2B%2B+source+%231',t:'0')),k:30.50309915596882,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:clang700,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'1',trim:'1'),lang:c%2B%2B,libs:!(),options:'-O3',source:1),l:'5',n:'0',o:'x86-64+clang+7.0.0+(Editor+%231,+Compiler+%231)+C%2B%2B',t:'0')),k:36.16356751069786,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:g82,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'1',trim:'1'),lang:c%2B%2B,libs:!(),options:'-O3+-march%3Dhaswell+-Wall+-Wextra',source:1),l:'5',n:'0',o:'x86-64+gcc+8.2+(Editor+%231,+Compiler+%232)+C%2B%2B',t:'0')),k:33.33333333333333,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4如果您愿意,可以为您提供像问题中那样的 AT&T 语法。
# gcc8.2 -O3 -march=haswell -Wall
.LC1:
.string "done"
main:
sub rsp, 8
mov edi, 800000000
call malloc # return value in RAX
vmovsd xmm0, QWORD PTR [rax] # load first elmeent
lea rdx, [rax+8] # p = &arr[1]
lea rcx, [rax+800000000] # endp = arr + len
.L2: # do {
vaddsd xmm0, xmm0, QWORD PTR [rdx] # tmp += *p
add rdx, 8 # p++
vmovsd QWORD PTR [rdx-8], xmm0 # p[-1] = tmp
cmp rdx, rcx
jne .L2 # }while(p != endp);
mov rdi, rax
call free
mov edi, OFFSET FLAT:.LC0
call puts
xor eax, eax
add rsp, 8
ret
Clang 展开了一点,就像我说的,不需要初始化它tmp
.
# just the inner loop from clang -O3
# with -march=haswell it unrolls a lot more, so I left that out.
# hence the 2-operand SSE2 addsd instead of 3-operand AVX vaddsd
.LBB0_1: # do {
addsd xmm0, qword ptr [rax + 8*rcx - 16]
movsd qword ptr [rax + 8*rcx - 16], xmm0
addsd xmm0, qword ptr [rax + 8*rcx - 8]
movsd qword ptr [rax + 8*rcx - 8], xmm0
addsd xmm0, qword ptr [rax + 8*rcx]
movsd qword ptr [rax + 8*rcx], xmm0
add rcx, 3 # i += 3
cmp rcx, 100000002
jne .LBB0_1 } while(i!=100000002)
苹果XCode的gcc
在现代 OS X 系统上,它实际上是变相的 clang/LLVM。