将右值传递给非引用参数,为什么编译器不能删除副本?

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());
}

编译(使用 clang 6.0.0 for x86_64 on Linux 所以 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

如果我没看错的话,这就是正在发生的事情:

  1. getStuff 被传递给 foo 的堆栈 (rsp + 40) 的指针以用于其 return 值,因此在 getStuff returns rsp + 40rsp + 71 包含 getStuff.
  2. 的结果
  3. 然后立即将此结果复制到较低的堆栈地址 rsprsp + 31
  4. 然后调用
  5. foo,它将从 rsp.
  6. 中读取其参数

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

test1():                              # @test1()
        sub     rsp, 32
        mov     rdi, rsp
        call    getStuff()
        call    foo(Big)
        add     rsp, 32
        ret

想法是:让 getStuff 直接写入堆栈中 foo 将从中读取的位置。

还有: 这是由 vc++ 在 windows 上针对 x64 编译的相同代码(具有 12 个整数而不是 8 个)的结果,这看起来更糟,因为 windows x64 ABI 通过并且 returns 参考,所以副本完全未使用!

_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

你是对的; 这看起来像编译器missed-optimization。如果没有重复项,您可以报告此错误 (https://bugs.llvm.org/)。

与流行的看法相反,编译器通常不会生成最佳代码。它通常足够好,现代 CPU 在不过度延长依赖链的情况下非常擅长处理多余的指令,尤其是关键路径依赖链(如果有的话)。

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


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 字节 load/store 而不是 2x 16 字节可能并不能证明 MSVC / ICC / clang 的 vzeroupper 成本是合理的,对于这么小的函数。 vzeroupper 在主流英特尔 CPU 上很便宜(只有 4 微指令),我确实使用 -march=haswell 来调整它,而不是 AMD 或 KNL,因为它更贵。


相关:x86-64 Windows 通过隐藏指针传递大型结构,以及 return 以这种方式传递它们。被调用者拥有 pointed-to 内存。 ()

通过在第一次调用 getStuff() 之前简单地为临时保留 space + shadow-space 并允许被调用者销毁临时,这种优化仍然可用,因为我们没有以后需要。

不幸的是,这实际上不是 MSVC 在这里或相关情况下所做的。

另请参阅@BeeOnRope 在 Why isn't pass struct by reference a common optimization? 上的回答和我对此的评论。如果您尝试设计一个调用约定来避免通过隐藏的 const-reference 进行复制,那么确保 copy-constructor 始终可以 运行 在 non-trivially-copyable 对象的合理位置是有问题的(调用者拥有内存,如果需要,被调用者可以复制)。

但这是 non-const 引用(被调用方拥有内存)最好的情况的示例,因为调用方希望将对象移交给被调用方。

虽然有一个潜在的陷阱:如果有任何指向此对象的指针,让被调用者直接使用它可能会引入错误。考虑其他一些执行 global_pointer->a[4]=0; 的函数。如果我们的被调用者调用 that 函数,它会意外地修改我们被调用者的 by-value arg.

因此,让被调用者销毁我们在 Windows x64 调用约定中的对象副本只有在逃逸分析可以证明没有其他对象具有指向该对象的指针时才有效。