Let's assume we have two mutually recursive functions f1 and f2:
#include <stdio.h> void f1(int n); void f2(int n); void f1(int n) { printf("%d\n", n); if (n == 0) printf("Blastoff!\n"); else f2(n); } void f2(int n) { f1(n-1); } int main() { f1(1000000); }When you compile this code without any optimizations provided by a compiler and try to run it you will most probably quickly get a segmentation fault (or similar error, depending on your platform). What happens here is the application running out of stack for storing function callbacks.
In Lisp you can solve this problem by rewriting functions to return continuations instead of values. In C you can simulate continuations by the following means:
1. Making functions to operate on pointers to variables instead of actual variables, which allows the application to preserve their values between function calls.
2. Returning a pointer to the next function to be called instead of calling it directly, which makes the use of application stack unnecessary.
So rewritten f1 and f2 can be presented in the following form:
#include <stdio.h> typedef void *(*Fun)(int *n); void *f1(int *n); void *f2(int *n); void *f1(int *n) { printf("%d\n", *n); if (*n == 0) { printf("Blastoff!\n"); return NULL; } else { return f2; } } void *f2(int *n) { *n = *n - 1; return f1; } int main() { int n = 1000000; Fun f = f1; while (f != NULL) { f = (Fun)f(&n); } return n; }Now the main function iteratively goes through subsequent function calls until the halting condition (NULL) is met. At the end of iteration variable n holds 0.
As a side note, gcc is able to optimize the code of mutually recursive functions quite effectively. If you compile the first example with gcc -O2 the resulting code will not crash, because the compiler replaces all call instructions with jmp.
P.S. I would like to thank Johnny, my friend and a true programming expert, who inspired me to write this post. I was also inspired by Carlos Oliveira's post on trampolines.
No comments:
Post a Comment