Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Declaring an empty destructor prevents the compiler from calling memmove() for copying contiguous objects

Consider the following definition of Foo:

struct Foo {
    uint64_t data;
};

Now, consider the following definition of Bar, which has the same data member as Foo, but has an empty user-declared destructor:

struct Bar {
    ~Bar(){} // <-- empty user-declared dtor
    uint64_t data; 
};

Using gcc 8.2 with -O2, the function copy_foo():

void copy_foo(const Foo* src, Foo* dst, size_t len) {
    std::copy(src, src + len, dst);
}

results in the following assembly code:

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

The assembly code above calls memmove() in order to perform the copy of the contiguous Foo objects. However, the function below, copy_bar(), which does exactly the same as copy_foo(), but for Bar objects:

void copy_bar(const Bar* src, Bar* dst, size_t len) {
    std::copy(src, src + len, dst);
}

generates the following assembly code:

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

This assembly code doesn't call memmove(), but performs the copy by itself.

Of course, if Bar is instead defined as:

struct Bar {
    ~Bar() = default; // defaulted dtor
    uint64_t data;
};

Then, both functions result in identical assembly code, since Foo also has a defaulted destructor.

Is there any reason why user-declaring an empty destructor in a class prevents the compiler from generating a call to memmove() to copy contiguous objects of that class?

like image 894
ネロク・ゴ Avatar asked Sep 11 '18 19:09

ネロク・ゴ


2 Answers

std::memmove can only be used on objects which are TriviallyCopyable, which requires a trivial destructor. Trivial destructors require that the destructor is not user-provided.

In your code for Bar:

struct Bar {
    ~Bar(){} // <-- empty user-declared dtor
    uint64_t data; 
};

The destructor is user-provided, so Bar is not TriviallyCopyable. Thus it would be incorrect in general for the compiler to generate a call to std::memmove.


By the as-if rule, the compiler could theoretically detect that the destructor is empty and thus equivalent to being trivial, but it's apparent that this optimization is not included in the implementation of std::copy.

The implementation of std::copy from libstdc++ uses the equivalent of std::is_trivially_copyable which is defined to report Bar as not trivially copyable. Enabling this optimization would require libstdc++ to have a special type trait to detect this special case which is trivially avoidable by writing ~Bar() = default;

like image 173
Justin Avatar answered Sep 28 '22 16:09

Justin


When you declare your own destructor that class is no longer trivially destructable nor trivially copyable. std::memmove requires that the object passed to is trivially copyable so it can't be used on the class anymore.

The standard doesn't pose a requirement on the implementation to check and see if your destructor is actually non trivial, it just defaults to all user defined destructors being non trivial.

If your destructor is truly trivial then there is no reason to write one.

like image 43
NathanOliver Avatar answered Sep 28 '22 17:09

NathanOliver