【问题标题】:passing rvalue to non-ref parameter, why can't the compiler elide the copy?将右值传递给非参考参数,为什么编译器不能省略副本?
【发布时间】:2018-09-03 14:35:01
【问题描述】:
struct Big {
    int a[8];
};
void foo(Big a);
Big getStuff();
void test1() {
    foo(getStuff());
}

编译(在 Linux 上使用 clang 6.0.0 for x86_64 so 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 + 40rsp + 71 之后包含getStuff 的结果。李>
  2. 然后,此结果会立即复制到较低的堆栈地址 rsp 一直到 rsp + 31
  3. 然后调用foo,它将从rsp 中读取其参数。

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

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

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

还有: 这是 vc++ 在 windows for x64 上编译的相同代码(用 12 个整数而不是 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

【问题讨论】:

  • 你能提供你正在编译的调用约定吗?
  • System V amd64,已将该信息添加到问题中。
  • 不确定这是否有帮助,但标准说函数参数不能是 NRVO 的。所以至少有一个省略是不可能发生的(当然,除非在 as-if 规则下。)
  • 根据 System V ABI struct Big 并不是很大。它实际上是INTEGER 类的边界大小。你可以试试更大的尺寸吗?
  • 我的立场已得到纠正,显然int 成员的错位导致结构被归类为MEMORY

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


【解决方案1】:

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

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

x86-64 SysV 如果大型结构不适合打包到两个 64 位整数寄存器中,则按值在堆栈上传递它们,然后它们通过隐藏指针返回。编译器可以而且应该(但不)提前计划并重用临时返回值作为调用 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 字节加载/存储而不是 2x 16 字节可能无法证明 MSVC / ICC / clang 的 vzeroupper 成本是合理的。 vzeroupper 在主流 Intel CPU 上很便宜(只有 4 uop),我确实使用 -march=haswell 来调整它,而不是更昂贵的 AMD 或 KNL。


相关:x86-64 Windows 通过隐藏指针传递大型结构,并以这种方式返回它们。被调用者拥有指向的内存。 (What happens at assembly level when you have functions with large inputs)

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

不幸的是,这实际上并不是 MSVC 在这里或相关案例中所做的。

另请参阅@BeeOnRope 的答案,以及我的 cmets onit,Why isn't pass struct by reference a common optimization?。如果您尝试设计一个调用约定来避免通过隐藏的 const 引用进行复制(调用者拥有内存,被调用者可以如果需要,请复制)。

但这是一个例子,其中非常量引用(被调用者拥有内存)是最好的,因为调用者希望将对象交给被调用者。

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

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

【讨论】:

  • 感谢您的回答。我仍然怀疑,我觉得这很简单,编译器在这里会很好。我会报告一个错误,我们会看到... R.e. Windows 点,这可能解释了为什么我最终会与 Windows 人员就函数调用边界处的副本进行交叉讨论。
  • @John_C:这对人类来说很简单,但对编译器来说却不那么简单,尤其是在为实际目标体系结构发出代码之前在通用内部表示中优化的可移植编译器。在我提交的几个错过优化的错误报告中,gcc 开发人员表示很难修复,因为编译的后期阶段需要知道早期阶段没有传递的东西,反之亦然。 (例如,RTL 优化器不知道值是否已签名:gcc.gnu.org/bugzilla/show_bug.cgi?id=82267#c1,以及后续错误 85038。
  • 出于兴趣,我在 Windows 上尝试了这个,它也做了一个副本!详情见原帖
  • @John_C:是的,我并不感到惊讶。它仍然是基本相同的优化,尽管在您只需传递指针的 Windows 上它更容易。我不确定在 Windows 上是否允许被调用者破坏数据的指向副本,即它是否“拥有”它有一个指针指向的 args 与堆栈(当然是寄存器 args),或者是否通过 const 引用。 const-ref 将允许传递指向非堆栈内存的指针,至少如果您确定被调用者没有指向您传递的静态或堆内存的指针,因为 arg 值不能别名。
  • 更新:在 Windows x64 中,被调用者“拥有”隐藏引用传递的参数的指向内存。所以调用者总是必须进行复制,除非过程间优化让它知道特定的被调用者不会通过将 arg 用作暂存空间来破坏 arg。
猜你喜欢
  • 2016-04-11
  • 2011-05-07
  • 2020-12-19
  • 2020-09-04
  • 1970-01-01
  • 1970-01-01
  • 2015-01-26
  • 1970-01-01
  • 2011-04-03
相关资源
最近更新 更多