cpp_weekly icon indicating copy to clipboard operation
cpp_weekly copied to clipboard

RAII and tail call optimization (TCO)

Open lcarlier opened this issue 8 months ago • 7 comments

Channel

C++ weekly

Topics

I have noticed that when using RAII the compiler isn't able to issue tail call optimization https://godbolt.org/z/WbP63ETE9

It would be interesting to explain the reason why and that if that's true that RAII has actually that cost or if we can prevent it.

Before this exercise I was not even aware of TCO so that could also be part of the episode

Length

Should be rather short.

lcarlier avatar May 08 '25 06:05 lcarlier

It seems like your use of std::optional is making things more difficult for the compiler/optimizer to see through the code in this case, if we instead make the RAIIWrapper itself handle the optional-ness it actually seems to generate better code: https://godbolt.org/z/4591qP311

Compiler optimizations can improve over time or with small code tweaks like this. I think the problem might be that your original code has two different ways of avoiding the close call, firstly by std::optional and secondly by the destructor checking the fd value. My change uses a single source of truth, and eliminates the case of an optional with a value of a bad fd.

LB-- avatar May 08 '25 19:05 LB--

I personally prefer to use the std::optional return value because it makes the interface very clear i.e. the caller is responsible to check the return value.

Now looking at the solution you are proposing, you are changing how the RAIIFile object is valid or not. If I'm correct, this is not related to TCO right?

The main difference I see in assembly on my example is the following. With TCO i.e. with the C style implementation we have

.L7:
        mov     edi, eax
        add     rsp, 8
        jmp     close

(We have TCO because of jmp) And without TCP i.e. with the RAII style implementation we have

.L7:
        mov     edi, eax
        call    close
        add     rsp, 8
        ret

(We do not have TCO because of the call)

It is related on how the close is invoked by the compiler. I got not idea why the compiler cannot see that this is the only object on the stack and no further cleanup is required.

lcarlier avatar May 09 '25 08:05 lcarlier

If your intent is for RAIIFile to always be valid, then remove the check in the destructor, and again the codegen changes more: https://godbolt.org/z/Wr3anPG81

LB-- avatar May 10 '25 01:05 LB--

Indeed, that reduce the code further. Good point.

However still no TCO. I still don't get why the compiler doesn't do it here.

Any ideas why?

lcarlier avatar May 11 '25 21:05 lcarlier

You're right, if I make an equivalent change in the other code the issue persists. But maybe it's not that big of an issue since adding a simple read call to both makes the generated assemblies identical: https://godbolt.org/z/1P4K5a1xa

I also notice that with the original code, adding noexcept to the openFile function changes the generated assembly for some reason. However, in the link I shared here, there is no effect like that. I suspect the case of opening and immediately closing a file just isn't something the gcc devs thought needed to behave the same with or without using std::optional.

LB-- avatar May 12 '25 06:05 LB--

That's interesting, when adding the noexcept it seems that TCO is not applied at all. I should probably have a look at the standard to see the rules about TCO. Anyway, it might be interesting to make an episode about TCO because I believe not much people are aware of it. I stumbled on it by chance.

lcarlier avatar May 12 '25 21:05 lcarlier

Semi-relevant episode just aired today: C++ Weekly - Ep 481 - What is Tail Call Elimination (Optimization)?

LB-- avatar May 19 '25 15:05 LB--