为什么引入析构函数的行为会导致更糟糕的代码生成?(通过引用而不是寄存器中的值传递)

Noa*_*oah 5 c++ abi calling-convention micro-optimization compiler-optimization

举个简单的例子:

struct has_destruct_t {
    int a;
    ~has_destruct_t()  {}
};

struct no_destruct_t {
    int a;
};


int bar_no_destruct(no_destruct_t);
int foo_no_destruct(void) {
    no_destruct_t tmp{};
    bar_no_destruct(tmp);
    return 0;
}

int bar_has_destruct(has_destruct_t);
int foo_has_destruct(void) {
    has_destruct_t tmp{};
    bar_has_destruct(tmp);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

foo_has_destruct代码生成稍差一些,因为析构函数似乎强制tmp进入堆栈:

foo_no_destruct():                   # @foo_no_destruct()
        pushq   %rax
        xorl    %edi, %edi
        callq   bar_no_destruct(no_destruct_t)@PLT
        xorl    %eax, %eax
        popq    %rcx
        retq
foo_has_destruct():                  # @foo_has_destruct()
        pushq   %rax
        movl    $0, 4(%rsp)
        leaq    4(%rsp), %rdi
        callq   bar_has_destruct(has_destruct_t)@PLT
        xorl    %eax, %eax
        popq    %rcx
        retq
Run Code Online (Sandbox Code Playgroud)

https://godbolt.org/z/388K1EfYa

但是,考虑到析构函数是 1)普通内联的并且 2)空的,为什么需要这样的情况呢?

有没有办法以零成本包含析构函数?

use*_*522 9

Itanium C++ ABI 调用约定定义具有非平凡析构函数的类型必须在堆栈上传递,并且~has_destruct_t() {}始终是非平凡析构函数。普通析构函数必须在其第一个声明 ( ~has_destruct_t() = default) 中隐式声明或默认。

如果纯右值作为函数参数传递,则该规则对于使复制省略起作用是必要的。复制省略要求函数参数的地址与从纯右值函数参数具体化的临时对象的地址相同,这是一个可观察的属性。

因此调用者需要为临时对象(同时也是函数参数对象)提供内存以确保地址相等。

自 C++17 起,对于具有非平凡(且不可删除)析构函数的类型,这种复制省略是强制性的,但在 Itanium C++ ABI 使用的所有以前的 C++ 标准版本中也允许这种复制省略。


如果您不想在析构函数体中执行任何操作,则根本不要声明它,除非您还需要 make 它virtual,在这种情况下virtual ~has_destruct_t() = default;就可以了。

  • @NathanOliver https://eel.is/c++draft/basic#class.temporary-3 是引入临时对象的附加权限,有效地使复制省略对于这些情况不是强制性的。这是在 OP 的 `no_destruct_t` 中可以传入寄存器的唯一原因。从技术上讲,从 C++17 开始,没有这种特殊情况,确实只有一个对象,并且参数中的纯右值表达式直接初始化函数参数对象,但这对于解释“复制省略”的含义并不是很直观在上下文中。 (2认同)