4.9 Memory Management
4.9.2 Zone Memory
An extension of the stack-based memory management provides an interesting middle ground between explicit un-managed management, and automatic managed memory. Although stack memory management is very often considered to be un-managed it is in fact a very simple memory management model.
In the common case the stack discipline is managed at the function call level. The stack grows with each new function call, with new allocations pushed onto the stack (i.e. growing the stack), and then symmetrically released, or popped, from the stack when the call returns - the stack frame[97]. Function arguments, and return values, may also be handled simply and elegantly using the stack discipline 7. The stack provides
a memory management system where data is added and removed in a last-in-rst-out fashion. Stack based memory allocation is simple and ecient, and is often directly supported by hardware. As well as providing an ecient execution model, stack allocation provides a simple programming model, making it straightforward for programmers to reason about, from a time and space perspective.
Unfortunately stack-based memory is also temporally and spatially inexible. Data is either relatively short lived (the duration of the function call), or alternatively must be copied, incurring a substantial performance overhead. In practice stack memory is also often quite limited in size.
The temporal and spatial limitations of the stack are highlighted in the following example, which attempts to return a higher order function (HOF), with the a1 variable captured by the closure.
(bind-func stack_example (lambda ()
;; stack allocate array
(let ((a1 (array 1.0 2.0 3.0 4.0)))
;; return higher order function
(lambda (a2) (* a1 a2)))))
Listing 31: Stack with HOF
Listing 31 highlights a common problem with stack based memory management for languages supporting higher order functions. a1, a closed variable in the lambda returned by stack_example, is stack allocated, and will be released as soon as stack_example returns. The lambda returned from stack_example is presumably designed to be called at some future time - a future time when a1 will no longer point to valid memory. Furthermore, memory must also be allocated for the higher order function that is to be returned (a reference to its environment for example).
One solution is to extend the stack discipline to life-cycles beyond a single function call by introducing the concept of a memory zone. Zone's are stored in a stack of zones where each new Zone is pushed onto the stack as it is created. New memory allocations are always made from the topmost zone of the zone-stack. When the topmost zone is popped from the zone-stack any memory allocated against that zone is automatically freed.
This style of zone stacking is a natural t for zones of lexical extent. For example, consider an expression (memzone size body) which pushes a new zone of size pre- allocated bytes onto the zone stack. Any allocations requested in body are registered against the pre-allocated memory in new zone. When the execution of body is complete, the new zone is popped from the zone-stack and the memory of size bytes is immediately freed.
(bind-func zone_example (lambda ()
(memzone 1024
;; zone allocate array
(let ((a1 (array 1.0 2.0 3.0 4.0)))
;; return zone allocated closure
(lambda (a2) (* a1 a2))))))
Listing 32: Memory Zone with closure
The function zone_example in Listing 32 pushes a new memory zone (1024 bytes) onto the zone-stack. Both a1, and the closure being returned, are allocated from the new zone. Unfortunately zone_example exhibits the same problematic behaviour as stack_example - the new zone is popped from the stack at its lexical extent (just before zone_example returns). The result being that just as with stack_example the returned closure, and/or its bound variables point to invalid memory locations. The zone_example does however improve on stack_example in two important aspects; rstly, it is now clear where the closure being returned is allocated from; secondly, a zone can be pre-allocated with any available system memory, circumventing common stack size limitations.
The additional temporal and spatial exibility aorded by Zone memory allows the caller, rather than the callee to push the new memory zone. Memory allocations made in the callee will be made against the topmost zone, the zone pushed in the caller, and will therefore still be valid in the caller once the callee returns.
(bind-func zone_example (lambda ()
;; zone allocate array
(let ((a1 (vector 1.0 2.0 3.0 4.0)))
;; return zone allocated closure
(lambda (a2) (* a1 a2)))))
;; print the result of a1 * a2
(bind-func call_zone_example (lambda ()
(memzone 1024
(let ((f (zone_example))
(a2 (vector 2.0 2.0 2.0 2.0)))
(println "result:" (f a2)) void))))
Listing 33: Memory Zone.
In Listing 33 call_zone_example pushes a new zone (1024 bytes) onto the zone-stack and then calls zone_example. Both a1 and the returning closure are both allocated against the topmost zone (i.e. the one created by call_zone_example. The closure is returned from zone_example and bound to f. a2 is then allocated into the topmost zone. f is then called with argument a2 and the result of (f a2) (also a vector allocated against the topmost zone), is printed to the log. Finally the (memzone 1024 ...) lex- ical extent is reached and all of the memory allocated is cleared in a single free before call_zone_example returns.