【问题标题】:Why does GCC fail to optimize unless the return value has a name?为什么除非返回值有名称,否则 GCC 无法优化?
【发布时间】:2023-04-04 11:55:01
【问题描述】:

考虑这段代码:

#include <array>

class C
{
    std::array<char, 7> a{};
    int b{};
};

C slow()
{
    return {};
}

C fast()
{
    C c;
    return c;
}

GCC 6 到 9 为 slow() 生成非常臃肿的代码:

slow():
        xor     eax, eax
        mov     DWORD PTR [rsp-25], 0
        mov     BYTE PTR [rsp-21], 0
        mov     edx, DWORD PTR [rsp-24]
        mov     DWORD PTR [rsp-32], 0
        mov     WORD PTR [rsp-28], ax
        mov     BYTE PTR [rsp-26], 0
        mov     rax, QWORD PTR [rsp-32]
        ret
fast():
        xor     eax, eax
        xor     edx, edx
        ret

这两个函数的含义有区别吗? Clang 会为两者发出类似 fast() 的代码,而 GCC 4-5 比 6-9 做得更好,但也不是很理想。

构建标志:-std=c++11 -O3

演示:https://godbolt.org/z/rPNG9o


根据此处的反馈作为 GCC 错误提交:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=90883

【问题讨论】:

  • 似乎你应该提出一个 QoI 错误 - 我们在这里无能为力!
  • @LightnessRacesinOrbit 问题是“有什么区别......”只有当答案是“否”时(至少对我来说这不是那么明显)我会转向 gcc 与 QoI 错误
  • ...也许添加语言律师标签?
  • 检查这个...godbolt.org/z/dxrjn6a 的末尾删除{},现在慢与快完全一样。
  • 似乎没有太多逻辑。从类中删除 b。如果a{},那么慢速和快速是2 条指令并且相同。如果a 没有{},那么慢是两条指令,快是12 条指令。 godbolt.org/z/88eLqc

标签: c++ gcc optimization g++


【解决方案1】:

GCC 维护人员同意这是一个错误(错过了优化),它已在 x86_64 的主干中修复(ARM 可能稍后会修复):https://gcc.gnu.org/bugzilla/show_bug.cgi?id=90883

【讨论】:

    【解决方案2】:

    这并不是真正的完整答案,但它可能会提供线索。正如我怀疑的那样,fastslow 在含义上存在细微差别,这可能会使编译器走上不同的道路。如果您将复制构造函数设为私有,您可以看到这一点。

    https://godbolt.org/z/FMIRe3

    #include <array>
    
    class C
    {
        std::array<char, 7> a{};
    
        public:
        C(){}
    
        private:
        C(const C & c){}
    };
    
    // Compiles
    C slow()
    {
        return {};
    }
    
    // Does not compile
    C fast()
    {
        C c;
        return c;
    }
    

    即使有复制省略fast 仍然需要复制构造函数,因为slow 返回一个initialization list,它由调用者显式构造返回值。这些可能会或可能不会最终做同样的事情,但我相信编译器必须做一些处理以确定是否是这种情况。

    有一篇详细的博客文章提供了有关该主题的一些有趣背景

    https://akrzemi1.wordpress.com/2018/05/16/rvalues-redefined/

    但是在 C++17 中行为发生了变化

    #include <array>
    
    class C
    {
        std::array<char, 7> a{};
    
        public:
        C(){}
    
        private:
        C(const C & c){}
    };
    
    C slow()
    {
        return {};
    }
    
    C fast()
    {
        return C();
    }
    

    fast 在 C++11 下编译失败,现在在 C++17 下编译

    https://godbolt.org/z/JG2PkD

    原因是return C()的含义从返回一个临时变量变为在调用者的框架中显式构造对象。

    所以现在在 C++17 中有很大的区别

    C fast(){
        C c;
        return c;
    }
    

    C fast(){
        return C();
    }
    

    因为在第二个中,您甚至不需要复制或移动构造函数即可使用。

    https://godbolt.org/z/i2eZnf

    绝对不是 C++ 101

    【讨论】:

    • fast()的声明和return语句之间添加c={};使其产生与slow()相同的代码;因此,您对从 initializer_list 分配的直觉似乎可以解释这种行为。
    • return C(); 生成的代码与return {}; 相同,因此与initializer_list 本身无关。
    【解决方案3】:

    这两个函数是等价的:返回的对象(更准确地说,是对这些函数的假设调用的结果对象)是通过使用其默认成员初始化程序初始化每个成员来初始化的。

    对于slow

    => 所以调用slow 的结果对象的所有成员都使用它们的默认成员初始化程序dcl.init.aggr]/5.4 进行初始化。

    对于fast

    => 所以调用slow 的结果对象的所有成员都使用它们的默认成员初始化程序[class.base.init]/9.1 进行初始化

    这两个函数的生成的程序集在功能上是等效的。所以 Gcc 生产的程序集是符合标准的。

    在缓慢的情况下,组装只是次优。该对象在两个寄存器上相应地返回给 SystemV x86 abi:rax 和 rdx (edx)。首先,它在地址 [rsp-32] 处将堆栈上的 C 类的对象概念归零。它将a 归零abb 之间的填充字节。然后它将堆栈的这个初始化部分复制到寄存器上。将堆栈归零的方式不是最理想的,无论如何,所有这些操作都等效于fast 程序集的 2 个异或操作。所以这只是一个明显的错误。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-09-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-12
      • 2021-11-22
      相关资源
      最近更新 更多