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
返回rsp + 40
之后}到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
答案 0 :(得分:3)
你是对的; 这看起来像编译器的遗漏优化。如果还没有重复,您可以报告此错误(https://bugs.llvm.org/)。
与流行的看法相反,编译器通常不会制作最佳代码。它通常足够好,现代CPU非常善于在过多延长依赖链时过度使用指令,尤其是关键路径依赖链(如果有的话)。
x86-64 SysV如果它们不适合打包到两个64位整数寄存器中,则按值在堆栈上传递大型结构,并且它们通过隐藏指针返回。编译器可以而且应该(但不会)提前计划并将返回值临时重用为调用foo(Big)
的stack-args。
gcc7.3,ICC18和MSVC CL19也错过了这种优化。 :/我把你的代码放到on the Godbolt compiler explorer with gcc/clang/ICC/MSVC。 gcc使用4x push qword [rsp+24]
进行复制,而ICC使用额外的指令将堆栈对齐32。
使用1x 32字节加载/存储而不是2x 16字节可能无法证明MSVC / ICC / clang vzeroupper
的成本,对于这么小的函数。 vzeroupper
在主流英特尔CPU(仅4 uops)上很便宜,而且我确实使用-march=haswell
来调整它,而不是AMD或KNL,因为它更贵。
相关:x86-64 Windows通过隐藏指针传递大型结构,并以这种方式返回它们。被调用者拥有指向的内存。 (What happens at assembly level when you have functions with large inputs)
在第一次调用getStuff()
之前,只需为临时+阴影空间预留空间,并允许被调用者破坏临时值,因为我们以后不需要它,因此仍然可以使用此优化。
不幸的是,这并不是MSVC在这里或在相关案件中所做的事情。不幸的是。
另请参阅@ BeeOnRope的回答,以及Why isn't pass struct by reference a common optimization?上的评论。如果你试图通过传递隐藏的const-reference来设计一个避免复制的调用约定,那么确保copy-constructor总能在一个理想的位置运行非平凡可复制的对象是有问题的(调用者拥有内存,如果需要,被叫者可以复制。
但是这是非const引用(被调用者拥有内存)最好的一个例子,因为调用者想要将对象移交给被调用者。
但是有一个潜在的问题:如果有任何指向此对象的指针,让被调用者直接使用它可能会引入错误。考虑global_pointer->a[4]=0;
的其他一些函数。如果我们的被叫方调用那个函数,它会意外地修改我们被调用者的按值arg。
因此,如果转义分析可以证明其他任何东西都没有指向此对象的指针,那么让被调用者在Windows x64调用约定中销毁该对象的副本是有效的。