tail call elimination tail calls and their elimination
play

Tail call elimination Tail calls and their elimination Michel - PDF document

Tail call elimination Tail calls and their elimination Michel Schinz Loops in functional languages The problem Several functional programming languages do not have an Unfortunately, recursion is not equivalent to the looping explicit looping


  1. Tail call elimination Tail calls and their elimination Michel Schinz Loops in functional languages The problem Several functional programming languages do not have an Unfortunately, recursion is not equivalent to the looping explicit looping statement. Instead, programmers resort to statements usually found in imperative languages: recursive recursion to loop. function calls, like all calls, consume stack space while For example, the central loop of a Web server written in loops do not... Scheme might look like this: In our example, this means that the Web sever will (define web-server-loop eventually crash because of a stack overflow – this is (lambda () clearly unacceptable! (wait-for-connection) (fork handle-connection) A solution to this problem must be found... (web-server-loop))) 3 4 The solution Tail calls The reason why the recursive call of web-server-loop could be replaced by a jump is that it is the last action taken by the function : (define web-server-loop In our example, it is obvious that the recursive call to web- (lambda () server-loop could be replaced by a jump to the (wait-for-connection) beginning of the function. If the compiler could detect this (fork handle-connection) case and replace the call by a jump, our problem would be (web-server-loop))) solved! Calls in terminal position – like this one – are called tail This is the idea behind tail call elimination . calls . This particular tail call also happens to target the function in which it is defined. It is therefore said to be a recursive tail call . 5 6

  2. Tail calls examples Tail call elimination In the functions below, all calls are underlined. Which ones are tail calls? When a function performs a tail call, its own activation (define map frame is dead, as by definition nothing follows the tail call. (lambda (f l) (if (null? l) Therefore, it is possible to first free the activation frame of a l function about to perform such a call, then load the (cons (f (car l)) parameters for the call, and finally jump to the function’s tail call (map f (cdr l)))))) code. (define fold This technique is called tail call elimination (or (lambda (f z l) optimisation ), abbreviated TCE . (if (null? l) z (fold f (f z (car l)) (cdr l))))) recursive tail call 7 8 TCE example TCE example Without tail call elimination, each recursive call to sum makes the stack grow, to accommodate activation frames. Consider the following function definition and call: 0 0 0 0 (define sum (lambda (z l) (1 2 3) (1 2 3) (1 2 3) (1 2 3) (if (null? l) 1 1 1 z (2 3) (2 3) (2 3) (sum (+ z (car l)) (cdr l))))) (sum 0 (list3 1 2 3)) 3 3 How does the stack evolve, with and without tail call (3) (3) elimination? 6 time () 9 10 TCE example Tail call optimisation ? With tail call elimination, the dead activation frames are freed before the tail call, resulting in a stack of constant size. Tail call elimination is more than just an optimisation! 0 1 3 6 Without it, writing a program that loops endlessly using (1 2 3) (2 3) (3) () recursion and does not produce a stack overflow is simply impossible. For that reason, full tail call elimination is actually required in some languages, e.g. Scheme. In other languages, like C, it is simply an optimisation performed by some compilers in some cases. time 11 12

  3. TCE example (define succ (lambda (x) (add 1 x))) (define add Without TCE With TCE ...) Tail call elimination succ: succ: ; ... ; ... LINT R27 add LINT R27 add for minischeme LINT R1 1 LINT R1 1 LOAD R2 R30 8 LOAD R2 R30 8 LINT R29 ret LOAD R29 R30 4 JMPZ R27 R0 LOAD R30 R30 0 unlink ret: JMPZ R27 R0 activation LOAD R29 R30 4 frame LOAD R30 R30 0 JMPZ R29 R0 14 Implementing TCE Identifying tail calls Tail call elimination is implemented by: To identify tail calls, we first assume that all calls are marked with a unique number. We then define a function T 1. identifying tail calls in the program, that returns the marks corresponding to the tail calls. 2. compiling those tail calls specially, by deallocating the For example, given the following expression: activation frame of the caller before jumping to the called function. (lambda (x) (if 1 (even? x) 2 (g 3 (h x)) 4 (h 5 (g x)))) We already know how to compile tail calls, but we did not explain yet how to identify them. T produces the set { 2,4 }. 15 16 Identifying tail calls T [ (lambda ( args ) body 1 … body n ) ] = T’ [body n ] Tail call elimination in where the auxiliary function T’ is defined as follows: uncooperative T’ [ (let ( defs ) body 1 … body n ) ] = T’ [body n ] T’ [ (if e 1 e 2 e 3 ) ] = T’ [e 2 ] ∪ T’ [e 3 ] environments T’ [ m ( e 1 e 2 … e n ) ] when e 1 is not a primitive = { m } T’ [ anything else ] = ∅ 17

  4. TCE in various environments Benchmark program To illustrate how the various techniques work, we will use a benchmark program in C that tests whether a number is even, using two mutually tail-recursive functions. When generating assembly language, it is easy to perform When no technique is used to manually eliminate tail calls, TCE, as the target language is sufficiently low-level to it looks as follows. And unless the C compiler performs tail express the deallocation of the activation frame and the call elimination – like gcc does with full optimisation – it following jump. crashes with a stack overflow at run time. When targeting higher-level languages, like C or the JVM, int even(int x) { this becomes difficult – although recent VMs like .NET’s return x == 0 ? 1 : odd(x - 1); support tail calls. We explore several techniques that have } been developed to perform TCE in such contexts. int odd(int x) { return x == 0 ? 0 : even(x - 1); } int main(int argc, char* argv[]) { printf("%d\n", even(300000000)); } 19 20 Single function approach Single function approach in C typedef enum { fun_even, fun_odd } fun_id; int wholeprog(fun_id fun, int x) { start: switch (fun) { The “single function” approach consists in compiling the case fun_even: whole program to a single function of the target language. if (x == 0) return 1; fun = fun_odd; This makes it possible to compile tail calls to simple jumps x = x - 1; goto start; within that function, and other calls to recursive calls to it. case fun_odd: This technique is rarely applicable in practice, due to if (x == 0) return 0; fun = fun_even; limitations in the size of functions of the target language. x = x - 1; goto start; } } int main(int argc, char* argv[]) { printf("%d\n", wholeprog(fun_even, 300000000)); } 21 22 Trampolines Trampolines in C typedef void* (*fun_ptr)(int); struct { fun_ptr fun; int arg; } resume; void* even(int x) { if (x == 0) return (void*)1; With trampolines, functions never perform tail calls resume.fun = odd; directly. Rather, they return a special value to their caller, resume.arg = x - 1; return &resume; informing it that a tail call should be performed. The caller } performs the call itself. void* odd(int x) { if (x == 0) return (void*)0; For this scheme to work, it is necessary to check the return resume.fun = even; value of all functions, to see whether a tail call must be resume.arg = x - 1; performed. The code which performs this check is called a return &resume; } trampoline . int main(int argc, char* argv[]) { void* res = even(300000000); while (res == &resume) res = (resume.fun)(resume.arg); printf("%d\n",(int)res); } 23 24

Recommend


More recommend