c++ - 为什么这个 C++ 包装类没有被内联?
问题描述
编辑- 我的构建系统出了点问题。我仍在弄清楚到底是什么,但gcc
产生了奇怪的结果(即使它是一个.cpp
文件),但是一旦我使用g++
它,它就会按预期工作。
对于我一直遇到的问题,这是一个非常简化的测试用例,其中使用数字包装类(我认为它会被内联)使我的程序慢了 10 倍。
这与优化级别无关(尝试使用-O0
and -O3
)。
我在包装类中遗漏了一些细节吗?
C++
我有以下程序,我在其中定义了一个包装 adouble
并提供+
运算符的类:
#include <cstdio>
#include <cstdlib>
#define INLINE __attribute__((always_inline)) inline
struct alignas(8) WrappedDouble {
double value;
INLINE friend const WrappedDouble operator+(const WrappedDouble& left, const WrappedDouble& right) {
return {left.value + right.value};
};
};
#define doubleType WrappedDouble // either "double" or "WrappedDouble"
int main() {
int N = 100000000;
doubleType* arr = (doubleType*)malloc(sizeof(doubleType)*N);
for (int i = 1; i < N; i++) {
arr[i] = arr[i - 1] + arr[i];
}
free(arr);
printf("done\n");
return 0;
}
我认为这会编译成同样的事情——它做同样的计算,而且一切都是内联的。
然而,事实并非如此——无论优化级别如何,它都会产生更大且更慢的结果。
(这个特定的结果并没有明显变慢,但我的实际用例包括更多的算术。)
编辑- 我知道这不是构造我的数组元素。我认为这可能会产生更少的 ASM,因此我可以更好地理解它,但如果有问题我可以更改它。
编辑- 我也知道我应该使用new[]
/ delete[]
。不幸的是gcc
拒绝编译它,即使它在一个.cpp
文件中。这是我的构建系统被搞砸的症状,这可能是我的实际问题。
编辑- 如果我使用g++
而不是gcc
,它会产生相同的输出。
编辑- 我发布了错误版本的 ASM(-O0
而不是-O3
),因此本节没有帮助。
集会
我在我的 Mac 上使用 XCode 的 gcc,在 64 位系统上。结果是一样的,除了 for 循环的主体。
这是它为循环体生成的 if doubleType
is double
:
movq -16(%rbp), %rax
movl -20(%rbp), %ecx
subl $1, %ecx
movslq %ecx, %rdx
movsd (%rax,%rdx,8), %xmm0 ## xmm0 = mem[0],zero
movq -16(%rbp), %rax
movslq -20(%rbp), %rdx
addsd (%rax,%rdx,8), %xmm0
movq -16(%rbp), %rax
movslq -20(%rbp), %rdx
movsd %xmm0, (%rax,%rdx,8)
WrappedDouble
版本更长:
movq -40(%rbp), %rax
movl -44(%rbp), %ecx
subl $1, %ecx
movslq %ecx, %rdx
shlq $3, %rdx
addq %rdx, %rax
movq -40(%rbp), %rdx
movslq -44(%rbp), %rsi
shlq $3, %rsi
addq %rsi, %rdx
movq %rax, -16(%rbp)
movq %rdx, -24(%rbp)
movq -16(%rbp), %rax
movsd (%rax), %xmm0 ## xmm0 = mem[0],zero
movq -24(%rbp), %rax
addsd (%rax), %xmm0
movsd %xmm0, -8(%rbp)
movsd -8(%rbp), %xmm0 ## xmm0 = mem[0],zero
movsd %xmm0, -56(%rbp)
movq -40(%rbp), %rax
movslq -44(%rbp), %rdx
movq -56(%rbp), %rsi
movq %rsi, (%rax,%rdx,8)
解决方案
它是内联的,但没有优化掉,因为您使用-O0
(默认)编译。这会生成用于一致调试的 asm,允许您在任何行的断点处停止时修改任何 C++ 变量。
这意味着编译器会在每条语句之后从寄存器中溢出所有内容,并重新加载下一条所需的内容。所以更多的语句来表达相同的逻辑=更慢的代码,无论它们是否在同一个函数中。 为什么 clang 会为这个简单的浮点求和(使用 -O0)产生低效的 asm?更详细地解释。
通常-O0
不会内联函数,但它确实尊重__attribute__((always_inline))
.
最终分配的 C 循环优化帮助解释了为什么基准测试或调整-O0
是完全没有意义的。 这两个版本对于性能来说都是荒谬的垃圾。
如果它没有被内联,就会有一条call
指令在循环内调用它。
asm 实际上是在寄存器中为const WrappedDouble& left
and创建指针right
。(非常低效,使用多条指令而不是一条指令lea
。这addq %rdx, %rax
是其中一个指令的最后一步。)
然后它将这些指针参数溢出到堆栈内存,因为它们是真正的变量并且必须在调试器可以修改它们的内存中。这就是movq %rax, -16(%rbp)
和%rdx
...正在做的事情。
在重新加载和取消引用这些指针之后,addsd
(add scalar double)结果本身会溢出到堆栈内存中的本地,带有movsd %xmm0, -8(%rbp)
. 这不是命名变量,而是函数的返回值。
然后它被重新加载并再次复制到另一个堆栈位置,然后最终从堆栈arr
中i
加载,连同 , 的double
结果一起operator+
存储到arr[i]
with 中movq %rsi, (%rax,%rdx,8)
。(是的,LLVM 使用 64 位整数mov
来复制double
那个时间。早期使用 SSE2 movsd
。)
所有这些返回值的副本都在循环携带的依赖链的关键路径上,因为下一次迭代读取arr[i-1]
. 那些约 5 或 6 个周期的存储转发延迟确实与 3 或 4 个周期的 FPadd
延迟相加。
显然,这是非常低效的。 启用优化后,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 中剩下的任何东西!如果添加 a ((double*)arr)[0] = 1;
,clang 将在循环之前加载第一个元素。
不幸的是,编译器不知道如何为您的前缀和计算做得更好。请参阅Intel cpu 上的 SSE和SIMD 前缀和的并行前缀(累积)和,以了解将其加速另一个可能为 2 的因素和/或并行化它的方法。
我更喜欢英特尔语法,但如果您愿意, Godbolt 编译器资源管理器可以为您提供 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)
在现代 OS X 系统上, Apple XCodegcc
真的是变相的 clang/LLVM。
推荐阅读
- wordpress - 将 x 添加到 meta_value 并在 Woocommerce 中显示结果
- markdown - 如何用标点符号链接到锚点?
- python - 气流中带有配置变量的动态 Dag
- python - 无法添加包路径
- ios - 在swift ios中再次运行应用程序后核心数据显示故障记录?
- reactjs - 执行条件语句时 OnPress 失败
- java - Eclipse 窗口构建器插件随机崩溃
- json - 我如何在reactJS中的详细页面中显示相关项目
- r - 多元线性回归列表上的 Wald 检验
- django - 未找到带有参数 '('',)' 的 'editPost' 的反向操作。尝试了 1 种模式:['editPost/(?P
[^/]+)/$']