首页 > 解决方案 > 空默认构造函数与隐式定义的不同机器代码

问题描述

鉴于以下结构...

#include <type_traits>

struct C {
    long a[16]{};
    long b[16]{};

    C() = default;
};

// For godbolt
C construct() {
    static_assert(not std::is_trivial_v<C>);
    static_assert(std::is_standard_layout_v<C>);

    C c;
    return c;
}

...gcc(x86-64 Linux 上的 10.2 版)启用了优化(在所有 3 个级别)为:生成以下程序集[1]construct

construct():
        mov     r8, rdi
        xor     eax, eax
        mov     ecx, 32
        rep stosq
        mov     rax, r8
        ret

一旦我提供空的默认构造函数......

#include <type_traits>

struct C {
    long a[16]{};
    long b[16]{};

    C() {}  // <-- The only change
};

// For godbolt
C construct() {
    static_assert(not std::is_trivial_v<C>);
    static_assert(std::is_standard_layout_v<C>);

    C c;
    return c;
}

...生成的程序集更改为单独初始化每个字段,而不是原始中的单个 memset:

construct():
        mov     rdx, rdi
        mov     eax, 0
        mov     ecx, 16
        rep stosq
        lea     rdi, [rdx+128]
        mov     ecx, 16
        rep stosq
        mov     rax, rdx
        ret

显然,这两个结构都不是微不足道的,而是标准布局。只是 gcc 错过了优化机会,还是从 C++ 语言的角度来看还有更多?


该示例是生产代码的精简版本,其中在性能上确实存在重大差异。


[1] 神箭:https ://godbolt.org/z/8n1Mae

标签: c++gccoptimizationx86-64c++20

解决方案


虽然我同意这似乎是一个错失的优化机会,但我注意到语言层面的一个不同之处。隐式定义的构造函数是constexpr,而您的示例中的空默认构造函数不是。来自cppreference.com

也就是说,[隐式定义的构造函数]调用此类的基类和非静态成员的默认构造函数。如果这满足 constexpr 构造函数的要求,则生成的构造函数是 constexpr (C++11 起)。

long因此,作为is的数组的初始化constexpr,隐式定义的构造函数也是。但是,用户定义的不是,因为它没有标记constexpr。我们也可以通过尝试制作construct示例的功能来确认这一点constexpr。对于隐式定义的构造函数,这没有任何问题,但对于空的用户定义版本,它无法编译,因为

<source>:3:8: 注意:'C' 不是聚合,没有普通的默认构造函数,也没有不是复制或移动构造函数的 'constexpr' 构造函数

正如我们在这里看到的:https ://godbolt.org/z/MnsbzKv1v

因此,为了解决这种差异,我们可以创建空的用户定义构造函数constexpr

struct C {
    long a[16]{};
    long b[16]{};

    constexpr C() {}
};

有点令人惊讶的是,gcc 现在生成了优化版本,即与默认构造函数完全相同的代码:https ://godbolt.org/z/cchTnEhKW

我不知道为什么,但constexpr在这种情况下,这种差异实际上似乎对编译器有所帮助。所以虽然看起来 gcc 应该能够在不指定的情况下生成相同的代码constexpr,但我想知道它可能是有益的是件好事。


作为对这一观察的额外测试,我们可以尝试使隐式定义的构造函数变为 non-constexpr并查看 gcc 是否无法进行优化。我能想到的一种尝试测试的简单方法是C从具有非constexpr默认构造函数的空类继承:

struct D {
    D() {}
};

struct C : D {
    long a[16]{};
    long b[16]{};

    C() = default;
};

实际上,这会生成再次单独初始化字段的程序集。一旦我们 make D() constexpr,我们就会得到优化的代码。请参阅https://godbolt.org/z/esYhc1cfW


推荐阅读