首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >将rvalue传递给非ref参数,为什么编译器不能删除副本?

将rvalue传递给非ref参数,为什么编译器不能删除副本?
EN

Stack Overflow用户
提问于 2018-03-25 10:13:21
回答 1查看 181关注 0票数 4
代码语言:javascript
复制
struct Big {
    int a[8];
};
void foo(Big a);
Big getStuff();
void test1() {
    foo(getStuff());
}

编译(在Linux上使用clang6.0.0用于x86_64,标志:-O3 -march=broadwell)

代码语言:javascript
复制
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

如果我读得对,这就是正在发生的事情:

  1. getStuff被传递到foo的堆栈(rsp + 40)的指针以用作其返回值,因此在getStuff返回rsp + 40rsp + 71后包含getStuff的结果。
  2. 然后立即将此结果复制到较低的堆栈地址rsp,并将其复制到rsp + 31
  3. 然后调用foo,它将从rsp读取其参数。

为什么下面的代码不是完全等价的(为什么编译器不生成它)?

代码语言:javascript
复制
test1():                              # @test1()
        sub     rsp, 32
        mov     rdi, rsp
        call    getStuff()
        call    foo(Big)
        add     rsp, 32
        ret

这样做的目的是:让getStuff直接写入foo将要读取的堆栈中的位置。

另外:下面是vc++在windows上为x64编译的相同代码(12个ints而不是8个)的结果,这似乎更糟糕,因为windows x64 ABI通过引用传递和返回,因此副本完全未使用!

代码语言:javascript
复制
_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
EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2018-03-25 10:59:02

您是对的;,这看起来像是编译器错过的优化。如果还没有复制,您可以报告此bug (https://bugs.llvm.org/)。

与流行的观点相反,编译器通常不会做出最优的代码。它通常足够好,而现代CPU在不延长依赖链(如果有关键路径依赖链的话)的情况下,能够很好地处理多余的指令。

如果不适合打包到两个64位整数寄存器中,则x86-64 SysV通过堆栈上的值传递大型结构,并通过隐藏指针返回。编译器可以也应该(但不会)提前计划,并将返回值临时重用为调用foo(Big)的堆栈-args。

gcc7.3、ICC18和MSVC CL19也错过了这种优化。/我把你的代码放在用gcc/clang/ICC/MSVC开发的龙门编译器浏览器上。gcc使用4x push qword [rsp+24]复制,而ICC使用额外的指令对齐堆栈32。

对于一个如此小的函数,使用1x32字节的加载/存储而不是2x16字节可能无法为MSVC / ICC / clang使用vzeroupper的成本。vzeroupper在主流英特尔CPU上很便宜(只有4个uop),我确实用-march=haswell来调优,而不是用更贵的AMD或KNL。

相关: x86-64 Windows通过隐藏指针传递大型结构,并以这种方式返回它们。被叫人拥有指向记忆。(当您拥有具有大输入的函数时,在程序集级别上会发生什么?)

这种优化仍然可用,只需在第一次调用getStuff()之前为临时+阴影空间预留空间,并允许被调用者销毁临时空间,因为我们以后不需要它。

不幸的是,这并不是MSVC在这里所做的或者在相关情况下所做的事情。

也见@BeeOnRope的答复,以及我在为什么通过引用传递结构不是一种常见的优化?上的评论。如果您试图设计一个避免通过隐式const-引用传递复制的调用约定(调用方拥有内存,被调用者可以在需要时复制),那么确保复制构造函数始终可以在正常的位置运行,对于不可复制的对象来说是有问题的。

但这是一个例子,在这种情况下,非const引用(被调用方拥有内存)是最好的,因为调用方希望将对象传递给被调用方。

但是,有一个潜在的缺点:如果有指向该对象的指针,让被调用方直接使用它可能会引入。考虑一些执行global_pointer->a[4]=0;的其他函数。如果被调用者调用该函数,它将意外地修改被调用者的值arg。

因此,只有在转义分析可以证明没有任何其他对象具有指向该对象的指针时,才允许被调用方破坏Windows调用约定中对象的副本。

票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/49474685

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档