Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can Tail Call Optimization and RAII Co-Exist?

I can't think of a true RAII language that also has tail call optimization in the specs, but I know many C++ implementations can do it as an implementation-specific optimization.

This poses a question for those implementations that do: given that destructors are invoked at the end of a automatic variable's scope and not by a separate garbage collection routine, doesn't it violate TCO's constraint that a recursive call must be the last instruction at the end of a function?

For example:-

#include <iostream>  class test_object { public:     test_object() { std::cout << "Constructing...\n"; }     ~test_object() { std::cout << "Destructing...\n"; } };  void test_function(int count);  int main() {     test_function(999); }  void test_function(int count) {     if (!count) return;     test_object obj;     test_function(count - 1); } 

"Constructing..." would be written 999 times and then "Destructing..." another 999 times. Ultimately, 999 test_object instances would be automatically allocated before the unwind. But assuming an implementation had TCO, would 1000 stack frames exist or just 1?

Does the destructor after the recursive call collide with the defacto TCO implementation requirements?

like image 682
Louis Jackman Avatar asked Jul 22 '13 16:07

Louis Jackman


People also ask

Does V8 support tail call optimization?

Last notes. Tail-call optimization is a part of the ES2015-ES6 specification. Supporting it isn't a NodeJS thing, it's something the V8 engine that NodeJS uses needs to support.

Does GCC do tail call optimization?

Regarding functions call optimization, gcc can do tail-call elimination to save the cost of allocating a new stack frame, and tail recursion elimination to turn a recursive function to non-recursive iterative one.

What languages support tail call optimization?

This kind of function call is called a tail call, and languages like Haskell, Scala, and Scheme can avoid keeping around unnecessary stack frames in such calls. This is called tail call optimization (TCO) or tail call elimitation.

Does F# have tail call optimization?

F# is a functional-first language and its compiler is designed to provide tail-call optimization if possible. The most efficient way is to turn the recursive function into a function with a loop.


2 Answers

Taken at face-value, it would certainly seem like RAII works against TCO. However, remember that there are a number of ways in which the compiler can "get away with it", so to speak.

The first and most obvious case is if the destructor is trivial, meaning that it is the default destructor (compiler-generated) and all the sub-objects have trivial destructors too, then the destructor is effectively non-existent (always optimized away). In that case, TCO can be performed as usual.

Then, the destructor could be inlined (it's code is taken and put directly in the function as opposed to being called like a function). In that case, it boils down to just having some "clean-up" code after the return statement. The compiler is allowed to re-order operations if it can determine that the end-result is the same (the "as-if" rule), and it will do so (in general) if the re-ordering leads to better code, and I would assume TCO is one of the considerations being applied by most compilers (i.e., if it can re-order things such that the code becomes suitable for TCO, then it will do it).

And for the rest of the cases, where the compiler cannot be "smart enough" to do it on its own, then it becomes the responsibility of the programmer. The presence of this automatic destructor call does make it a bit harder for the programmer to see the TCO-inhibiting clean-up code after the tail call, but it doesn't make any difference in terms of the ability of the programmer to make the function a candidate for TCO. For example:

void nonRAII_recursion(int a) {   int* arr = new int[a];   // do some stuff with array "arr"   delete[] arr;   nonRAII_recursion(--a);  // tail-call }; 

Now, a naive RAII_recursion implementation might be:

void RAII_recursion(int a) {   std::vector<int> arr(a);   // do some stuff with vector "arr"   RAII_recursion(--a);  // tail-call };  // arr gets destroyed here, not good for TCO. 

But a wise programmer can still see that this won't work (unless the vector destructor is inlined, which is likely in this case), and can rectify the situation easily:

void RAII_recursion(int a) {   {     std::vector<int> arr(a);     // do some stuff with vector "arr"   }; // arr gets destroyed here   RAII_recursion(--a);  // tail-call }; 

And I'm pretty sure you could demonstrate that there are essentially no cases where this kind of trick could not be used to ensure that TCO can be applied. So, RAII merely makes it a bit harder to see if TCO can be applied. But I think programmers that are wise enough to design TCO-capable recursive calls are also wise enough to see those "hidden" destructor calls that would need to be forced to occur before the tail-call.

ADDED NOTE: Look at it this way, the destructor hides away some automatic clean-up code. If you need the clean-up code (i.e., non-trivial destructor), you will need it whether you use RAII or not (e.g., C-style array or whatever). And then, if you want TCO to be possible, it must be possible to do the cleaning up before doing the tail-call (with or without RAII), and it is possible, then it is possible be force the RAII objects to be destroyed before the tail-call (e.g., by putting them inside an extra scope).

like image 84
Mikael Persson Avatar answered Oct 30 '22 15:10

Mikael Persson


If the compiler perform the TCO then the order in which destructors are called is changed with respect to when it doesn't do TCO.

If the compiler can prove that this reordering doesn't matter (e.g. if the destructor is trivial) then as per the as-if rule it can perform TCO. However, in your example the compiler can't prove that and will not do TCO.

like image 33
Cassio Neri Avatar answered Oct 30 '22 13:10

Cassio Neri