[英]passing rvalue to non-ref parameter, why can't the compiler elide the copy?
struct Big {
int a[8];
};
void foo(Big a);
Big getStuff();
void test1() {
foo(getStuff());
}
编译(在Linux上使用clang 6.0.0 for x86_64,所以System V ABI,标志: -O3 -march=broadwell
)到
test1(): # @test1()
sub rsp, 72
lea rdi, [rsp + 40]
call getStuff()
vmovups ymm0, ymmword ptr [rsp + 40]
vmovups ymmword ptr [rsp], ymm0
vzeroupper
call foo(Big)
add rsp, 72
ret
如果我正确地阅读这个,那就是正在发生的事情:
getStuff
传递一个指向foo
的堆栈( rsp + 40
)的指针用于返回值,所以在getStuff
返回getStuff
rsp + 40
到getStuff
rsp + 71
包含getStuff
的结果。 rsp
到rsp + 31
。 foo
,它将从rsp
读取其参数。 为什么以下代码不完全等效(为什么编译器不会生成它)?
test1(): # @test1()
sub rsp, 32
mov rdi, rsp
call getStuff()
call foo(Big)
add rsp, 32
ret
这个想法是:让getStuff
直接写入foo
将读取的堆栈中的位置。
另外:这是在Windows for x64上由vc ++编译的相同代码(12个int而不是8个)的结果,这看起来更糟,因为windows x64 ABI通过并通过引用返回,因此副本完全未使用!
_TEXT SEGMENT
$T3 = 32
$T1 = 32
?bar@@YAHXZ PROC ; bar, COMDAT
$LN4:
sub rsp, 88 ; 00000058H
lea rcx, QWORD PTR $T1[rsp]
call ?getStuff@@YA?AUBig@@XZ ; getStuff
lea rcx, QWORD PTR $T3[rsp]
movups xmm0, XMMWORD PTR [rax]
movaps XMMWORD PTR $T3[rsp], xmm0
movups xmm1, XMMWORD PTR [rax+16]
movaps XMMWORD PTR $T3[rsp+16], xmm1
movups xmm0, XMMWORD PTR [rax+32]
movaps XMMWORD PTR $T3[rsp+32], xmm0
call ?foo@@YAHUBig@@@Z ; foo
add rsp, 88 ; 00000058H
ret 0
你是对的; 这看起来像编译器的遗漏优化 。 如果还没有重复,您可以报告此错误( https://bugs.llvm.org/ )。
与流行的看法相反,编译器通常不会制作最佳代码。 它通常足够好,并且现代CPU在不过多地延长依赖链时会非常擅长翻阅过多的指令,尤其是关键路径依赖链(如果有的话)。
x86-64 SysV通过堆栈上的值传递大型结构,如果它们不适合打包到两个64位整数寄存器中,并且它们通过隐藏指针返回。 编译器可以而且应该(但不)提前计划并将返回值临时重用为foo(Big)
调用的stack-args。
gcc7.3,ICC18和MSVC CL19也错过了这种优化。 :/我用Gcc / clang / ICC / MSVC将你的代码放在Godbolt编译器资源管理器上 。 gcc使用4x push qword [rsp+24]
进行复制,而ICC使用额外的指令将堆栈对齐32。
对于MSVC / ICC / clang,使用1x 32字节加载/存储而不是2x 16字节可能无法证明vzeroupper
的成本,因为这个函数很小。 vzeroupper
在主流Intel CPU(仅4 vzeroupper
很便宜,而且我确实使用-march=haswell
来调整它,而不是AMD或KNL,它更贵。
相关:x86-64 Windows通过隐藏指针传递大型结构,并以这种方式返回它们。 被调用者拥有指向的内存。 ( 当您具有大输入的函数时,在汇编级别会发生什么 )
在第一次调用getStuff()
之前,只需为临时+阴影空间保留空间,并允许被调用者销毁临时文件,因为我们以后不再需要它,因此仍然可以使用此优化。
不幸的是,这实际上并不是MSVC在这里或相关案例中所做的。
另见@ BeeOnRope的答案,以及我的评论,关于为什么不通过引用传递struct一个常见的优化? 。 如果你试图通过传递隐藏的const-reference来设计一个避免复制的调用约定,那么确保copy-constructor总能在一个理想的位置运行非平凡可复制的对象是有问题的(调用者拥有内存,被调用者可以如果需要复制)。
但这是一个非const引用(被调用者拥有内存)最好的情况的例子,因为调用者想要将对象移交给被调用者。
但是有一个潜在的问题: 如果有任何指向此对象的指针,让被调用者直接使用它可能会引入错误 。 考虑一些其他函数,它执行global_pointer->a[4]=0;
。 如果我们的被调用者调用该函数,它将意外地修改我们的被调用者的按值arg。
因此,如果转义分析可以证明没有其他任何指针指向此对象,那么让被调用者在Windows x64调用约定中销毁该对象的副本是有效的。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.