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

时间:2018-03-25 10:13:21

标签: c++ clang x86-64 compiler-optimization abi

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

如果我正确地读到这个,那就是发生的事情:

  1. getStuff传递指向foo堆栈(rsp + 40)的指针以用于其返回值,因此在getStuff返回rsp + 40之后}到rsp + 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将从中读取的堆栈中的位置。

    此外: 这是在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
    

1 个答案:

答案 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调用约定中销毁该对象的副本是有效的。