6

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!

Noam Rodrik
  • 552
  • 2
  • 16
  • 3
    How are you applying `noexcept`? Are you using `noexcept(true)`? Also, checking debug build code is mostly meaningless. The compiler can/will add code that it/you can use for debugging purposes. – NathanOliver Aug 20 '21 at 14:57
  • Turn on optimization - live - https://godbolt.org/z/69GTooYKz Non-optimized code is sometimes larger to make debugging easier and should not be used as an example of whether a compile is generating good or bad code. – Richard Critten Aug 20 '21 at 15:04
  • 2
    `noexcept` doesn't "disable exceptions", it changes the behaviour of exception -- to have them call `std::terminate` – M.M Aug 20 '21 at 15:11
  • @NathanOliver Yes, I'm using noexcept(true) :) – Noam Rodrik Aug 20 '21 at 15:50
  • @RichardCritten I don't want to max out on optimizations, since this won't help me learn the natural behavior of the compiler. Instead, I changed it to -O1, minimal optimizations, and it is still bigger than without noexcept... – Noam Rodrik Aug 20 '21 at 15:50
  • @M.M You are correct! I wrote it not clear. Here's a quote by the CppCoreGuidelines: "Declaring a function noexcept helps optimizers by reducing the number of alternative execution paths. It also speeds up the exit after failure." By disabling, I meant that it doesn't use all the resources a function needs to unwind with in case an exception has been called. – Noam Rodrik Aug 20 '21 at 15:52

2 Answers2

6

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.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • The non-throwing allocation is still bloating the binary anyways, but I suppose with the optimizer off it really is futile. Thanks for the informative comment! – Noam Rodrik Aug 20 '21 at 15:58
2

Without nothrow, your function just acts as a front end to the allocation function you call. It doesn't have any real behavior of its own. In fact, in a real executable, if you do link-time optimization there's a pretty good chance that it'll completely disappear.

When you add noexcept, your code is silently transformed into something roughly like this:

auto* allocated_int()
{
    try { 
        return new int{};
    }
    catch(...) { 
        terminate();
    }
}

The extra code you see generated is what's needed to catch the exception and call terminate when/if needed.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111