Noah
Noah

Reputation: 1759

Why does the act of introducing a destructor result in worse codegen? (Passed by reference instead of by value in a register)

Take this simple example:

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;
}

foo_has_destruct gets slightly worse codegen, because the destructor seems to force tmp onto the stack:

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

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

But why does this need to be the case given that the destructor is 1) trivially inlinable and 2) empty?

Is there any way to include a destructor at zero cost?

Upvotes: 5

Views: 156

Answers (1)

user17732522
user17732522

Reputation: 76829

The Itanium C++ ABI calling convention defines that a type with non-trivial destructor must be passed on the stack and ~has_destruct_t() {} is always a non-trivial destructor. A trivial destructor must either be implicitly-declared or defaulted on its first declaration (~has_destruct_t() = default).

The rule is necessary to make copy elision work if a prvalue is passed as function argument. Copy elision requires that the address of the function parameter is the same as the address of the temporary object materialized from the the prvalue function argument and that's an observable property.

So the caller needs to provide memory for the temporary object (and at the same time the function parameter object) to assure equal addresses.

This copy elision is mandatory since C++17 for types with non-trivial (and non-deleted) destructor, but was also allowed in all previous C++ standard editions, which the Itanium C++ ABI made use of.


If you don't intent to do anything in the destructor body, then don't declare it at all, except if you need to also make it virtual, in which case virtual ~has_destruct_t() = default; will do.

Upvotes: 9

Related Questions