Proper Tail Recursion and Space Efficiency
Total Page:16
File Type:pdf, Size:1020Kb
Proper Tail Recursion and Space Efficiency William D Clinger Northeastern University [email protected] Abstract E ..- (quote c> constants The IEEE/ANSI standard for Scheme requires implementa- I variable references tions to be properly tail recursive. This ensures that portable L lambda expressions code can rely upon the space efficiency of continuation-pass- (if EO El E2) conditional expressions ing style and other idioms. On its face, proper tail recursion I (set! I Eo) assignments concerns the efficiency of procedure calls that occur within (Eo El . ..I procedure calls ..- a tail context. When examined closely, proper tail recur- L ..-’ (lambda (11 . > E) sion also depends upon the fact that garbage collection can TRUE 1 FALSE 1 NUM:z 1 SYM:I be asymptotically more space-efficient than Algol-like stack v$E&3;. .) I * * * allocation. Proper tail recursion is not the same as ad hoc tail call Location optimization in stack-based languages. Proper tail recursion Identifier often precludes stack allocation of variables, but yields a well-defined asymptotic space complexity that can be relied upon by portable programs. Figure 1: Internal syntax of Core Scheme. This paper offers a formal and implementation-indepen- dent definition of proper tail recursion for Scheme. It also shows how an entire family of reference implementations can execution of an iterative computation in constant be used to characterize related safe-for-space properties, and space, even if the iterative computation is de- proves the asymptotic inequalities that hold between them. scribed by a syntactically recursive procedure. The standard’s citation refers to a technical report that uses 1 Introduction CPS-conversion to explain what proper tail recursion meant in the context of the first Scheme compiler [Ste78]. That Tail recursion is a phrase that has been used to refer to var- explanation is formally precise, but it is not entirely clear ious syntactic notions, to particular techniques for imple- how it applies to an implementation that uses a different al- menting syntactic tail recursion, and to the space efficiency gorithm for CPS-conversion or does not use CPS-conversion of those techniques. Syntactically, a call is a tail call if it at all. Most attempts to characterize proper tail recursion appears within a function body that can reduce to the call; in a truly implementation-independent way have been more this is formalized in Section 2. Since the complete call graph informal [Ste78]: is seldom available, a tail call is often said to be tail recur-- siwe regardless of whether it occurs within a cycle in the call Intuitively, function calls do not “push control graph. stack”; instead, it is argument evaluation which Scheme, Standard ML, and several other mostly-function- pushes control stack. al languages rely heavily on the efficiency of tail recursion. Common idioms, notably continuation-passing style (CPS), Using a style of definition proposed by Morrisett and would quickly run out of stack space if tail calls were to Harper [MH97], this paper defines a set of asymptotic space consume space. To ensure that portable code can rely upon complexity classes that characterize proper tail recursion these idioms, the IEEE standard for Scheme [IEESl] says and several related safe-for-space complexity properties. Al- though these complexity classes are defined in terms of spe- Implementations of Scheme are required to be cific reference implementations, they can be used without properly tail-recursive [Ste78]. This allows the depending upon the details or even the existence of imple- mentation-dependent data structures such as stacks or heaps. This provides a solid foundation for reasoning about the asymptotic space complexity of Scheme programs, and also provides implementors with a formal basis for determin- ing whether potential optimizations are safe with respect to proper tail recursion. a ,996 ACM 0-69791~967-4/96/0006...55.00 174 Figure 2: Static frequency of tail calls. These numbers were obtained by instrumenting two compilers: ICC and Twobit lFH95. CH941. The self-tail calls shown for Scheme include all tail calls to known closures, because Twobit has no reason to iecogn’ize self&l calls as a special case. See also Section 14. 2 Tail Calls (define (find-leftmost predicate? tree fail) (if (leaf? tree) Figure 1 shows an internal syntax for the core of Scheme. (if (predicate? tree) The external syntax of full Scheme can be converted into tree ; return this internal syntax by expanding macros and by replacing (fail)) ; tail call vector, string, and list constants by references to constant (let ((continuation storage. (lambda 0 (find-leftmost ; tail call Deflnition 1 The tail expressions of a program written in predicate? Core Scheme are defined inductively as follows. (right-child tree) 1. The body of a lambda expression is a tail expression. fail>>>> (find-leftmost predicate? ; tail call 2. If (if Eo El Ez) is a tail expression, then both El (left-child tree) and E2 are tail expressions. continuation>>>> 3. Nothing else is a tail expression. Figure 3: An example with three tail calls. [KCR98] extends this definition to the syntax of full Scheme. Definition 2 A tail call is a tail expression that is a proce- Although find-leftmost uses an explicit failure continu- dure call. ation, it is not a pure example of continuation-passing style, because its fourth line returns tree to the implicit continu- Figure 2 shows that tail calls are much more common than ation. Returning is equivalent to performing an implicit tail the special case of self-tail calls, in which a procedure calls call to the implicit continuation. itself tail recursively. In Scheme, it is perfectly feasible to write large programs in which no procedure ever returns, and all calls are tail 3 The Essence of Proper Tail Recursion call~.~ This is pure continuation-passing style. Proper tail recursion guarantees that implementations will use only a The essence of proper tail recursion is that a procedure can bounded amount of storage to implement all of the calls return by performing a tail call to any procedure, including that are performed by a program written in this style. itself. This is a kind of dual to the informal characterization To make this precise, we need a proper model of space quoted in Section 1. A tail call does not cause an immediate consumption. This model should allow us to reason about return, but passes the responsibility for returning from the the space needed to run a program, independent of imple- procedure that performs the tail call to the procedure it mentation. For this to be tractable, the space model should is calling. In other words, the activation of a procedure take the form of an asymptotic upper bound on the space extends from the time that it is called to the time that it consumed by an implementation. performs either a return or a tail call. Proper tail recursion constrains but does not determine This is fundamentally different from the traditional view the space model. To obtain a complete model, we must also of procedure calls, in which the activation of a procedure model the space consumed by variables and data, and spec- encompasses the activations of all procedures that it calls. ify the roots that a garbage collector would use to determine whether a variable or datum is reachable. Garbage collec- 4 An Example tion then completes the model. With a reasonable garbage collector, the asymptotic space required for variables and Figure 3 shows a procedure definition that contains three tail data is O(N), where N is the largest number of words oc- calls, of which the last is a self-tail call. Given a predicate, cupied by reachable variables and data at any point in the a binary tree, and a failure continuation of no arguments, program [App92]. find-leftmost searches for the leftmost leaf that satisfies For example, a Scheme programmer can tell that the the predicate. If such a leaf is found, then it is returned space required by find-leftmost is independent of the num- normally. Otherwise the procedure returns by performing a ‘Some compilers do this routinely, using CPS Scheme as their tsr- tail call to the failure continuation or to itself. get language. 175 ber of right edges in the tree, and is proportional to the maximal number of left edges that occur within any directed Cotiguration ::= (v, a) path from the root of the tree to a leaf. If every left child is a I (E, P, K:, 4 leaf, then find-leftmost runs in constant space, no matter how large the tree. I b,P,K,d v E Value ::= c 5 Retention versus Deletion UNSPECIFIED I UNDEFLNED A deletion strategy reclaims storage at statically determined PRIMOP$ points in the program, whereas a retention strategy retains storage until it is no longer needed, as determined by dy- ESCAPE:(a$) namic means such as garbage collection [Fis72]. Algol-like CLOSURE:(+, p) stack allocation is the most important deletion strategy. I(, ::= halt By allowing the lifetime of a variable or value to extend select:(El, &, p, n) beyond the lifetime of the block in which it was declared or created, retention strategies support more flexible program- I assign:(I, p, K) ming styles. It is less well-known that retention strategies I push:((E,. ..), h.. .),T,p,d can also reclaim storage sooner than a deletion strategy, and call:((E,. .), K) can have better asymptotic space efficiency. This is crucial I for proper tail recursion. P E Identifier 3 Location Deletion strategies interfere with proper tail recursion [Ste78, Cha88, App92, ASwS96]. For the example of Section I7 E Location 2 Value 4, allocating continuation on a stack would require O(n) ?r E Permutation space instead of O(1) space, even when every left child is a leaf.