声明一个空的析构函数会阻止编译器调用memmove()来复制连续的对象

眠りネ*_*ネロク 6 c++ optimization assembly gcc x86-64

考虑以下定义Foo:

struct Foo {
    uint64_t data;
};
Run Code Online (Sandbox Code Playgroud)

现在,考虑以下定义Bar,它具有相同的数据成员Foo,但具有一个空的 用户声明的析构函数:

struct Bar {
    ~Bar(){} // <-- empty user-declared dtor
    uint64_t data; 
};
Run Code Online (Sandbox Code Playgroud)

使用gcc 8.2 -O2,功能copy_foo():

void copy_foo(const Foo* src, Foo* dst, size_t len) {
    std::copy(src, src + len, dst);
}
Run Code Online (Sandbox Code Playgroud)

得到以下汇编代码:

copy_foo(Foo const*, Foo*, size_t):
        salq    $3, %rdx
        movq    %rsi, %rax
        je      .L1
        movq    %rdi, %rsi
        movq    %rax, %rdi
        jmp     memmove
.L1:
        ret
Run Code Online (Sandbox Code Playgroud)

上面的汇编代码调用memmove()以执行连续Foo对象的副本.但是,下面的函数copy_bar()与对象完全相同copy_foo(),但对于Bar对象:

void copy_bar(const Bar* src, Bar* dst, size_t len) {
    std::copy(src, src + len, dst);
}
Run Code Online (Sandbox Code Playgroud)

生成以下汇编代码:

copy_bar(Bar const*, Bar*, size_t):
        salq    $3, %rdx
        movq    %rdx, %rcx
        sarq    $3, %rcx
        testq   %rdx, %rdx
        jle     .L4
        xorl    %eax, %eax
.L6:
        movq    (%rdi,%rax,8), %rdx
        movq    %rdx, (%rsi,%rax,8)
        addq    $1, %rax
        movq    %rcx, %rdx
        subq    %rax, %rdx
        testq   %rdx, %rdx
        jg      .L6
.L4:
        ret
Run Code Online (Sandbox Code Playgroud)

此汇编代码不会调用memmove(),而是单独执行复制.

当然,如果Bar定义为:

struct Bar {
    ~Bar() = default; // defaulted dtor
    uint64_t data;
};
Run Code Online (Sandbox Code Playgroud)

然后,两个函数都会产生相同的汇编代码,因为Foo还有一个默认的析构函数.

是否有任何理由为什么用户声明类中的空析构函数会阻止编译器生成memmove()复制该类的连续对象的调用?

Jus*_*tin 8

std::memmove只能用于TriviallyCopyable的对象,这需要一个简单的析构函数.普通的析构函数要求析构函数不是用户提供的.

在您的代码中Bar:

struct Bar {
    ~Bar(){} // <-- empty user-declared dtor
    uint64_t data; 
};
Run Code Online (Sandbox Code Playgroud)

析构函数是用户提供的,因此Bar不是TriviallyCopyable.因此,编译器生成调用通常是不正确的std::memmove.


通过as-if规则,编译器理论上可以检测到析构函数为空,因此等同于微不足道,但很明显,这种优化不包含在实现中std::copy.

libstdc ++实现std::copy使用std::is_trivially_copyable定义的等价物来报告,Bar因为它不是简单的可复制的.启用此优化将要求libstdc ++具有特殊类型特征来检测此特殊情况,这通过写入可以轻易避免~Bar() = default;

  • @SergeyA:它在C++抽象机器中是不正确的,但在任何正常/合理的asm编译输出中都不行.但它正在寻找/正在进行此优化的C++库代码.我想你可以争辩说libstdc ++可以知道它自己的`std :: memmove`实现(就GNU C`__builtin`函数来说可能?)在这种情况下是安全的,但实际上这个答案正在使得编译器需要在编译后找到它,因为库代码不能这样做. (2认同)

Nat*_*ica 7

当你声明自己的析构函数时,该类不再是简单的可破坏的,也不是简单的可复制的. std::memmove要求传递给的对象是可以轻易复制的,因此不能再在类上使用它.

该标准并不要求实现检查并查看您的析构函数是否实际上是非平凡的,它只是默认为所有用户定义的析构函数都是非常重要的.

如果你的析构函数真的很小,那么就没有理由写一个.


Fra*_*eux 5

文档std::memmove说:

如果对象不是TriviallyCopyable,则不指定memmove的行为,并且可能未定义.

TriviallyCopyable要求:

  • 每个拷贝构造函数都很简单或被删除
  • 每个移动构造函数都是微不足道的或删除的
  • 每个复制赋值运算符都很简单或被删除
  • 每个移动赋值运算符都很简单或删除至少一个复制构造函数,移动构造函数,复制赋值运算符或移动赋值运算符未被删除
  • 琐碎的非删除析构函数

一个简单的析构函数需要:

  • 析构函数不是用户提供的(意思是,它是隐式声明的,或者在其第一个声明中显式定义为默认值)
  • 析构函数不是虚拟的(也就是说,基类析构函数不是虚拟的)
  • 所有直接基类都有微不足道的析构函数
  • 类类型(或类类型数组)的所有非静态数据成员都具有简单的析构函数

通过添加用户提供的析构函数,您的类型不再是可复制的并且将其传递给std::memmove未指定或未定义的行为.