Search code examples
c++performanceoptimizationclangnoexcept

C++ function with noexcept in this case is actually slower?


I'm trying to experiment with code by myself here on different compilers. I've been trying to lookup the advantages of disabling exceptions on certain functions (via the binary footprint) and to compare that to functions that don't disable exceptions, and I've actually stumbled onto a weird case where it's better to have exceptions than not.

I've been using Matt Godbolt's Compiler Explorer to do these checks, and it was checked on x86-64 clang 12.0.1 without any flags (on GCC this weird behavior doesn't exist).

Looking at this simple code:

auto* allocated_int()
{
    return new int{};
}

int main()
{
    delete allocated_int();

    return 0;
}

Very straight-forward, pretty much deletes an allocated pointer returned from the function allocated_int().

As expected, the binary footprint is minimal, as well:

allocated_int():                     # @allocated_int()
        push    rbp
        mov     rbp, rsp
        mov     edi, 4
        call    operator new(unsigned long)
        mov     rcx, rax
        mov     rax, rcx
        mov     dword ptr [rcx], 0
        pop     rbp
        ret

Also, very straight-forward. But the moment I apply the noexcept keyword to the allocated_int() function, the binary bloats. I'll apply the resulting assembly here:

allocated_int():                     # @allocated_int()
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     edi, 4
        call    operator new(unsigned long)
        mov     rcx, rax
        mov     qword ptr [rbp - 8], rcx        # 8-byte Spill
        jmp     .LBB0_1
.LBB0_1:
        mov     rcx, qword ptr [rbp - 8]        # 8-byte Reload
        mov     rax, rcx
        mov     dword ptr [rcx], 0
        add     rsp, 16
        pop     rbp
        ret
        mov     rdi, rax
        call    __clang_call_terminate
__clang_call_terminate:                 # @__clang_call_terminate
        push    rax
        call    __cxa_begin_catch
        call    std::terminate()

Why is clang doing this extra code for us? I didn't request any other action but calling new(), and I was expecting the binary to reflect that.

Thank you for those who can explain!


Solution

  • Why is clang doing this extra code for us?

    Because the behaviour of the function is different.

    I didn't request any other action but calling new()

    By declaring the function noexcept, you've requested std::terminate to be called in case an exception propagates out of the function.

    allocated_int in the first program never calls std::terminate, while allocated_int in the second program may call std::terminate. Note that the amount of added code is much less if you remember to enable the optimiser. Comparing non-optimised assembly is mostly futile.

    You can use non-throwing allocation to prevent that:

    return new(std::nothrow) int{};
    

    It's indeed an astute observation that doing potentially throwing things inside non-throwing function can introduce some extra work that wouldn't need to be done if the same things were done in a potentially throwing function.

    I've been trying to lookup the advantages of disabling exceptions on certain functions

    The advantage of using non-throwing is potentially realised where such function is called; not within the function itself.