当前位置: 首页 > 知识库问答 >
问题:

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

江飞章
2023-03-14
struct Big {
    int a[8];
};
void foo(Big a);
Big getStuff();
void test1() {
    foo(getStuff());
}

编译(使用clang 6 . 0 . 0 for x86 _ 64 on Linux so System V ABI,flags:< code >-O3-March = broad well )到

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. 然后将此结果立即复制到较低的堆栈地址rsprsp 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

共有1个答案

齐志勇
2023-03-14

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

与普遍的看法相反,编译器通常不会编写最佳代码。它通常已经足够好了,现代CPU非常善于在不过度延长依赖链时通过多余的指令进行犁找,尤其是关键路径依赖链(如果有的话)。

如果大型结构不适合装入两个64位整数寄存器,x86-64 SysV会在堆栈上按值传递大型结构,然后通过隐藏指针返回。编译器可以而且应该(但不会)提前计划,并重用返回值temporary作为调用< code>foo(Big)的堆栈参数。

gcc7.3、ICC18 和 MSVC CL19 也错过了此优化。:/我把你的代码放在戈德博尔特编译器资源管理器上,用gcc /叮当/ICC/MSVC。gcc 使用 4x 推送 qword [rsp 24] 进行复制,而 ICC 使用额外的指令将堆栈对齐 32。

对于这么小的功能,使用1x 32字节加载/存储而不是2x 16字节可能不值得为MSVC / ICC / clang使用< code > vzeropuper 。< code>vzeroupper在主流英特尔CPU上很便宜(只有4个UOP ),我确实使用了< code>-march=haswell来进行调优,而不是针对AMD或KNL,因为它们更贵。

相关:x86-64 Windows通过隐藏指针传递大型结构,并以这种方式返回它们。被调用方拥有所指向的内存。(当函数有大量输入时,在汇编级会发生什么)

在第一次调用< code>getStuff()之前,只需为临时阴影空间保留空间,并允许被调用方销毁临时阴影空间(因为我们以后不需要它),就可以实现这种优化。

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

另请参阅@BeeOnRope的回答,以及我对“为什么引用传递结构不是一种常见的优化?”的评论?。如果您试图设计一个调用约定来避免通过传递隐藏的常量引用进行复制(调用方拥有内存,被调用方可以根据需要进行复制),那么要确保复制构造函数始终能够在一个正常的位置运行,以处理不可复制的对象是有问题的。

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

不过,有一个潜在的问题:如果有指向这个对象的指针,那么让被调用方直接使用它可能会引入bug。考虑执行global_pointer的其他函数-

因此,仅当转义分析可以证明没有其他对象指向此对象的指针时,让被调用方在 Windows x64 调用约定中销毁我们对象的副本才有效。

 类似资料:
  • 问题内容: 我当时在Swinject上工作,但问题困扰着我。我已经整整一天都被困在那里。我怀疑这是由于Swift是一种静态类型的语言,但我不确定。 我在这个操场上总结了我的问题 我尝试了不同的解决方案,例如test.self或type(of:test),但它们都不起作用。 所以我想我不能用提供为变量的通用参数来调用函数? 问题答案: 与 协议元类型有两种。对于某些协议和符合类型: A 描述协议本身

  • 问题内容: 为什么此代码无法编译? 为什么我不能将类变量传递给? 问题答案: 该操作符对引用类型,像,而不是对象,如。您可能想要类似 旁注:如果编写,您的代码将更加简洁 但是,我不确定是否需要某种方法。

  • 在下面的代码中,我希望将'races.class'作为参数传递给方法'get allraces()'。我该怎么做?

  • 以下内容将无法编译: null 我不明白这里到底出了什么问题。为什么编译器不能从函数参数推导出模板参数? 我需要做什么才能让这个工作?

  • 问题内容: 我正在使用py.test来测试包装在python类MyTester中的某些DLL代码。为了进行验证,我需要在测试期间记录一些测试数据,然后再进行更多处理。由于我有许多test _…文件,因此我想在大多数测试中重用测试器对象的创建(MyTester的实例)。 由于tester对象是获得DLL变量和函数的引用的对象,因此我需要将DLL变量的列表传递给每个测试文件的tester对象(要记录的

  • 我有一个简单的测试设置,如 但当我尝试编译测试时,我会遇到53个错误,比如 实际上并没有传达任何关于问题所在的有用信息。我只能假设在我的构建中没有正确配置某些内容。sbt文件或其他地方。 这段代码确实曾经工作过,在我清理东西的过程中,事情发生了变化,现在它被破坏了,没有好的诊断。 有人能提出要找的东西吗?