C++ 将右值传递给非ref参数,为什么可以';编译器没有删除副本吗?
编译(在Linux so System V ABI上使用针对x86_64的clang 6.0.0,标志:C++ 将右值传递给非ref参数,为什么可以';编译器没有删除副本吗?,c++,clang,x86-64,compiler-optimization,abi,C++,Clang,X86 64,Compiler Optimization,Abi,编译(在Linux so System V ABI上使用针对x86_64的clang 6.0.0,标志:-O3-march=broadwell)到 如果我读对了,这就是正在发生的事情: getStuff被传递一个指向foo堆栈(rsp+40)的指针,用于其返回值,因此getStuff之后返回rsp+40到rsp+71包含getStuff的结果 然后,该结果立即复制到较低的堆栈地址rsp到rsp+31 然后调用foo,它将从rsp读取其参数 为什么下面的代码不是完全等效的(为什么编译器不生成它)
-O3-march=broadwell
)到
如果我读对了,这就是正在发生的事情:
getStuff
被传递一个指向foo
堆栈(rsp+40
)的指针,用于其返回值,因此getStuff
之后返回rsp+40
到rsp+71
包含getStuff
的结果rsp
到rsp+31
foo
,它将从rsp
读取其参数getStuff
直接写入堆栈中foo
将从中读取的位置
此外:
下面是vc++在windows for x64上编译的相同代码(12个整数而不是8个整数)的结果,这似乎更糟糕,因为windows x64 ABI通过引用传递和返回,因此副本完全未使用
test1(): # @test1()
sub rsp, 32
mov rdi, rsp
call getStuff()
call foo(Big)
add rsp, 32
ret
你是对的编译器似乎错过了优化。如果还没有重复的错误,您可以报告此错误() 与流行的观点相反,编译器通常不会生成最佳代码。它通常已经足够好了,而且现代CPU在不太长依赖链的情况下非常擅长处理多余的指令,特别是关键路径依赖链(如果有) 如果大型结构不适合打包到两个64位整数寄存器中,则在堆栈上按值传递大型结构,并通过隐藏指针返回。编译器可以也应该(但不应该)提前计划并将返回值临时重用为调用
foo(Big)
的堆栈参数
gcc7.3、ICC18和MSVC CL19也未进行此优化。:/我把你的密码放上去了。gcc使用4x
push-qword[rsp+24]
进行复制,而ICC使用额外的指令将堆栈对齐32
使用1x 32字节加载/存储而不是2x 16字节可能无法证明MSVC/ICC/clang的vzeroupper
对于如此小的函数的成本是合理的vzeroupper
在主流Intel CPU(只有4个UOP)上很便宜,我确实使用了-march=haswell
来调整它,而不是AMD或KNL,因为后者更贵
相关:x86-64 Windows通过隐藏指针传递大型结构,并以这种方式返回它们。被调用方拥有指向的内存。() 在第一次调用
getStuff()
之前,只需为临时+阴影空间保留空间,并允许被调用方销毁临时空间,因为我们以后不需要它,这种优化仍然可用
不幸的是,MSVC在这里或相关案例中并不是这么做的
另请参见@BeeOnRope的答案和我的评论。如果您试图设计通过传递隐藏常量引用来避免复制的调用约定(调用者拥有内存,被调用者可以在需要时进行复制),那么确保复制构造函数始终在非平凡可复制对象的合理位置运行是有问题的
但这是一个非常量引用(被调用方拥有内存)最好的例子,因为调用方希望将对象交给被调用方
不过,有一个潜在的问题:如果有指向此对象的指针,让被调用方直接使用它可能会导致错误。考虑一些其他函数:<代码> GULALL指针[A](4)=0;<代码>。如果被调用方调用该函数,它将意外地修改被调用方的by值arg
因此,只有在转义分析可以证明没有其他东西有指向该对象的指针时,才允许被调用方在Windows x64调用约定中销毁该对象的副本。您可以提供您编译的调用约定吗?System V amd64已将该信息添加到问题中。不确定这是否有帮助,但是标准规定函数参数不能是NRVO的。因此,至少有一种省略是不可能发生的(当然,除了在“似乎”规则下)。根据系统V,ABI
struct Big
并不是真正的大。它实际上是INTEGER
类的边界大小。你能试试更大的尺寸吗?我已经纠正了,显然int
成员的不对中导致结构被归类为MEMORY
,谢谢你的回答。我仍然怀疑,我觉得这是一件非常简单的事情,编译器在这里会非常好。我会报告一个错误,我们会看到。。。R.e.关于windows的观点,这也许可以解释为什么我最终会与windows人员讨论函数调用边界处的副本。@John_C:这是对人类来说很简单但对编译器来说却不那么简单的事情之一,特别是在为实际目标体系结构发出代码之前在通用内部表示中进行优化的便携式编译器。在我提交的几个遗漏的优化bug报告中,gcc开发人员说很难修复,因为编译的后期需要知道一些早期阶段没有传递的东西,反之亦然。(例如,RTL优化器不知道值是否有符号:,并跟踪bug 85038。出于兴趣,我在windows上尝试了这个方法,它也复制了一个!有关详细信息,请参阅原始帖子。)details@John_C:是的,我一点也不惊讶。它基本上仍然是相同的优化,尽管在Windows上只需传递指针就更容易了。我不是s在Windows上确定是否允许被调用方删除数据的指向副本,即是否“拥有”其指针指向的参数,如stack by args(当然也可以是register args),或者是否为by const reference。const ref将允许
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
test1(): # @test1()
sub rsp, 32
mov rdi, rsp
call getStuff()
call foo(Big)
add rsp, 32
ret
_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