【问题标题】:Overly eager C++ union zero-initialization with constexpr使用 constexpr 过度渴望 C++ 联合零初始化
【发布时间】:2020-06-19 11:23:23
【问题描述】:

下面是一个标记联合模板“Storage”的简化示例,它可以假定两个类型 L 和 R 包含在一个联合中,加上一个 bool 指示它们中的哪一个被存储。实例化使用两种不同大小的类型,较小的一种实际上是空的。

#include <utility>

struct Empty
{
};

struct Big
{
        long a;
        long b;
        long c;
};

template<typename L, typename R>
class Storage final
{
public:
        constexpr explicit Storage(const R& right) : payload{right}, isLeft{false}
        {
        }

private:
        union Payload
        {
                constexpr Payload(const R& right) : right{right}
                {
                }
                L left;
                R right;
        };

        Payload payload;
        bool isLeft;
};

// Toggle constexpr here
constexpr static Storage<Big, Empty> createStorage()
{
        return Storage<Big, Empty>{Empty{}};
}

Storage<Big, Empty> createStorage2()
{        
        return createStorage();
}
  • 构造函数用 Empty 初始化 R 成员,并且只为该成员调用联合的构造函数
  • 联合从不默认初始化为一个整体
  • 所有构造函数都是 constexpr

因此,函数“createStorage2”应该只填充 bool 标签,而不要理会联合。所以我希望编译结果带有默认优化“-O”:

createStorage2():
        mov     rax, rdi
        mov     BYTE PTR [rdi+24], 0
        ret

GCC 和 ICC 都会生成类似的东西

createStorage2():
        mov     rax, rdi
        mov     QWORD PTR [rdi], 0
        mov     QWORD PTR [rdi+8], 0
        mov     QWORD PTR [rdi+16], 0
        mov     QWORD PTR [rdi+24], 0
        ret

将整个 32 字节结构归零,而 clang 生成预期的代码。您可以使用https://godbolt.org/z/VsDQUu 重现此内容。只有当您从“createStorage”静态函数中删除 constexpr 时,GCC 才会恢复到所需的 bool 标记初始化,而 ICC 保持不变并且仍然填充所有 32 个字节。

这样做可能不违反标准,因为未使用的位“未定义”允许任何事情,包括设置为零和消耗不必要的 CPU 周期。但是很烦人,如果您首先出于效率原因引入了工会,并且您的工会成员的规模差异很大。

这里发生了什么?如果从构造函数和静态函数中删除 constexpr 不是一种选择,是否有任何方法可以解决此问题?

附注:即使所有 constexpr 都被删除,ICC 似乎也会执行一些额外的操作,如https://godbolt.org/z/FnjoPC

createStorage2():
        mov       rax, rdi                                      #44.16
        mov       BYTE PTR [-16+rsp], 0                         #39.9
        movups    xmm0, XMMWORD PTR [-40+rsp]                   #44.16
        movups    xmm1, XMMWORD PTR [-24+rsp]                   #44.16
        movups    XMMWORD PTR [rdi], xmm0                       #44.16
        movups    XMMWORD PTR [16+rdi], xmm1                    #44.16
        ret                                                     #44.16

这些 movups 指令的目的是什么?

【问题讨论】:

  • FWIW,clang 做你想做的事:godbolt.org/z/hZdGZn
  • 如果你避免列表初始化会有什么不同吗?例如Storage&lt;Big, Empty&gt;(Empty{}) 而不是 Storage&lt;Big, Empty&gt;{Empty{}} ?
  • @NathanOliver:是的,也注意到了这一点。这一观察证实不需要额外的初始化开销。
  • @BenVoigt:不,它没有。

标签: c++ constexpr zero-initialization


【解决方案1】:

(这只是我的猜测,但评论太长了)

这是怎么回事?

由于构造函数是constexpr,因此Payload 作为一个整体可能在编译时具有一些计算值。然后,在运行时,返回完整的Payload。据我所知,编译器不需要识别编译时值的某个部分是未初始化的并且它不应该为它生成任何代码。

在一些疯狂的编译器中,甚至可能发生编译时 Payload 在未初始化的部分中有垃圾值,然后它会产生例如:

createStorage2():
        mov     rax, rdi
        mov     QWORD PTR [rdi], 0xbaadf00d
        mov     QWORD PTR [rdi+8], 0xbaadf00d
        mov     QWORD PTR [rdi+16], 0xbaadf00d
        mov     QWORD PTR [rdi+24], 0
        ret

一般constexpr 不喜欢未初始化的值,但联合是一种解决方法。

【讨论】:

  • 这就是我的意思:C++ 标准中肯定没有任何内容禁止将未定义位初始化为它喜欢的任何内容。它仍然会产生正确的输出,只是使用比必要更多的 CPU 周期。
  • OTOH,即使 C++ 标准没有为任何优化规定任何特定的内容,我们都希望优化编译器能做一项合理的工作,至少消除最局部的冗余,而我们认为不应该这样做以牺牲可读性和简单性为代价手动优化源代码。这里看起来很失败。
  • 我认为优化这个并不容易。编译器必须显式跟踪它在编译期间保存的所有 constexpr 值的所有未初始化字节,以识别它不应该为这些生成 mov addr, constant 语句。这比用垃圾初始化它更难。
  • 明确跟踪需要什么?可以在编译时计算 constexpr 值,并且编译器需要有一种方法可以在需要时将值放置在适当的位置。例如使用一系列语句(GCC、Clang)。从源地址复制预先计算的值将是一种替代方法(ICC,似乎?)。在任何情况下,每个单独的值都需要一个单独的序列或源地址。在使用序列的情况下,它甚至不需要知道目标的大小,只需要知道它的位置。你能举例说明为什么这会被过度简化吗?
  • 您的Payload 就是这样一个例子。它计算的 constexpr 值为(以 64 位为单位):[garbage, garbage, garbage, 0x0]。然后,一个天真的编译器创建 64 位 mov 指令,其中一些将这些垃圾值加载到内存中。更聪明的编译器需要将您的Payload 记住为[&lt;uninit&gt;, &lt;uninit&gt;, &lt;uninit&gt;, 0x0],并以跳过mov 指令生成的特殊方式处理这些 值。 与 gargabe 的对比就是我的意思。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-10-23
  • 1970-01-01
  • 2018-07-26
  • 1970-01-01
  • 1970-01-01
  • 2018-04-27
  • 2023-03-17
相关资源
最近更新 更多