From our previous lecture, we can see that our machine requires five registers:
=M=
: the program text
=env=
: the lexical context [variable-value pairs]
=k=
: the control context [list of frames]
=val=
: the result value from evaluating
the contents of =M=
=param-val=
: the value of a function's parameter
=M=
is a pointer into the program code. =k=
holds a stack, which can be implemented as a pointer into a separate
array. The other three are registers that (may) directly point to
allocate data structures such as closures and lists.
Let us name the following expressions
M1 = (lambda (x) (+ x 3)) M2 = (lambda (f) (+ (f 7) 4)) M3 = (lambda (z) (- z y))and consider the evaluation of
(M1 (M2 (let (y 2) M3)))We will study the evaluation of this expression by looking at ``snapshots'' of the machine at various stages.
We have evaluated M1
and are in the process of evaluating
the argument to the resulting closure.
=k= = [appR -> <M1,empty>] =env= = empty =val= = <M1,empty>where
=val=
shares its contents with =k=
.
We have evaluated the left and right terms from Snapshot 1, and are
about to apply the closure formed from M2
.
=k= = [appR -> <M2,empty> , appR -> <M1,empty>] =env= = empty =val= = <M3,[<y,2>]>
We are just done evaluating the subtraction inside M3
, which is bound to f
.
=k= = [+R -> 4 , appR -> <M1,empty>] =env= = [<z,7> , <y,2>] =val= = 5Note that we have opened up the environment of the closure bound to
f
in showing the value of =env=
.
We are in the midst of the addition inside the closure
<M1,empty>
; the x
has just been
evaluated.
=k= = [+R -> 3] =env= = [<x,9>] =val= = 9However, recall that there are several old fragments of environment still to be found in memory, such as
[<z,7> ,
<y,2>]
from Snapshot 3.
If we look carefully in the final step, there are many items that were formerly in the environment that are unnecessary for the remaining evaluation. However, these unnecessary items are still present in memory and could potentially cause our program to exhaust available memory before finishing its task. Hence, we should try to recycle such unnecessary memory. Id est:
cons
cell; we copy this into the new memory 2, and
repeat the procedure along each component of the cell. The process is
repeated when memory 2 is exhausted, switching the rôles of the
two parts.
This method might make intuitive sense, but what if we have sharing in
our language? In LC, we currently have no way of checking sharing
constraints (as with eq?
in Scheme), but it is reasonable
to assume we might be called upon to do so. In addition, if we
duplicated objects, we would in fact use more space in the new half
than in the old one, which would ruin the purpose of our attempt at
recycling memory. To prevent this, when we visit a cell, we have to
indicate that it has been forwarded; then, if it is visited
again, the appropriate sharing relationship can be mimicked in the new
half.
Thus, with the help of this process, which is called garbage collection, if the two memory banks are of equal size, and if there are indeed unreachable objects in the exhausted space, then we will have space left over in the new bank, and we can proceed with our allocation. However, there are two problems:
(define gc (lambda (ptr) (cond ((null? ptr) null) ((cons? ptr) (cons_mem1 (gc (car_mem2 ptr)) (gc (cdr_mem2 ptr)))))))but this loses sharing. So we have to break
cons_mem1
up
into its two constituent parts: allocation and initialization.
((cons? ptr) (let ((new (alloc ...))) (make-as-forwarded ptr) (init_mem1 new (gc (car_mem2 ptr)) (gc (cdr_mem2 ptr))) new))However, this still doesn't check for forwarding. A simple modification takes care of that:
((cons? ptr) (if (forwarded? ptr) ... (let ((new (alloc ...))) (make-the-orange-thing) (init_mem1 new (gc (car_mem2 ptr)) (gc (cdr_mem2 ptr))) new)))
In summary, the traditional view of garbage collection is roughly as follows:
Recently, a new view of garbage collection has been emerging. In this view,
alpha
, the program evaluation
will work for all possible values in that cell. In particular, it
will work if the cell's content is replaced by the null pointer.
Doing so frees all memory that the cell (may) point to.
The new view is logical: it distinguishes between truly live and provably live cells, between truth and provability. As always, the latter is an approximation of the former. It is another indication of how tightly logic and computation are intertwined.
Shriram Krishnamurthi / shriram@cs.rice.edu