Most importantly, think first, change code later. The temptation to dive into the code after noticing a bug can be very strong. Resist it. You have a puzzle in front of you: the interpreter produces the wrong answer when you type some particular expression. Experiment with similar expressions. Try to find the conditions under which the bug is exhibited. If the problem happens when a particular expression is given as an argument to a user-defined function, does the problem occur when given to a primitive function? If the problem happens using the [[global]] statement, does it happen if you use [[set]] instead? And so on. Before you go into the program, you should have gathered enough evidence from probing the interpreter to incriminate some specific section of code. Appendix [->] shares some war stories of our own from the debugging of the root tracking code in Appendix [->]. The most important message is think first.

Garbage collection

Software due Friday, October 22, at 12:10AM (a.k.a. Thursday night at midnight).
Write-ups due on paper Friday, October 22, at 5:10PM in Maxwell Dworkin 231.

The purposes of this exercise are

This is a long assignment and will count 100 points: double the value of a typical assignment. Before you begin work,

Your GC study is divided into five stages. We have implemented the first two stages. You should be able to implement each of the final three stages separately. Make sure you understand the entire stage before beginning work on it. The code you need to get started is in ~cs152/asst/gc.

If you have trouble with one of the stages, seek help. Don't just go on to the next stage and assume things will work out.

  1. Stage 1: Arenas. In stage 1, we observe that [[mkSx]] is the only function that allocates S-expressions with [[malloc]]. We have changed that [[malloc]] to [[allocSx]]. [[allocSx]] uses a linked list of ``arenas.'' For our mark-and-sweep collector, the arena is the unit in which the memory-management code gets memory from the operating system. The collection of all arenas will constitute the ``managed heap.''

    Each arena is an array holding some constant number of S-expressions, with mark bits, e.g.: <>= #define ARENASIZE 24 struct markedsx { /* S-expression with mark bit */ struct sx sx; unsigned live:1; }; typedef struct arena { struct markedsx pool[ARENASIZE]; struct arena *tail; } @ Notice that arenas are chained on a linked list; the head of that list is called [[heap]]. One arena is always the current free arena, and the variable [[freeArena]] points to it. The heap pointer [[hp]] points to next available S-expression in the current free arena. The limit pointer [[heap_limit]] points just beyond the last S-expression in the current free arena, so at any moment the number of free S-expressions in the current free arena is [[heap_limit - hp]]. <>= struct markedsx *hp, *heap_limit; struct arena *heap, *freeArena; @ Here's a picture of what the arenas look like in the middle of an allocation cycle:

    Dark shaded areas are used, while light areas are available for allocation. Except for the current free arena, arenas are either entirely used or entirely available. [[heap_limit - hp]] gives the size of the light area in the current arena. Of course, this picture doesn't make sense for the stage 1 we have implemented---in the implementation we give you, the current arena is also always the last one. Also, once we have a garbage collector, ``used'' and ``available'' will be only approximations to what the dark and light areas represent.

    We have implemented a function [[allocSx]], which takes an S-expression (that is, a free cell) from the current free arena. If there are no S-expressions remaining in the current arena, it calls [[growHeap]] to add a new arena to the tail of the list of all arenas.

  2. Stage 2: Roots. The next step in preparing a collector is to identify the roots: all the places where pointers might lead to live S-expressions. Another way to say it is we have to know what [[Sx]]s might possibly be used after a call to [[allocSx]]. These certainly include every [[Sx]] reachable from every [[rho]] of every invocation of [[eval]], as well as various temporary values and environments. Some S-expressions are also live during parsing.

    To solve this stage, we have broken the problem down by type.

    In stage 3, you'll have to trace pointers starting at all of the roots listed above. To help ensure that all roots are visible, we have implemented a new primitive, [[show-roots]], whose only purpose is to help debug the garbage collector. [[show-roots]] finds and prints out all the roots, with identifying commentary. It should be possible to call [[(show-roots)]] at any time in any Scheme program. Our [[show-roots]] is incomplete, but you may wish to complete it if you have trouble. Here's an example that illustrates that [[s]] in [[evalList]] is live, with value 6: <<*>>= -> (+ (* 2 3) (begin (show-roots) 7)) ROOTS FOR GARBAGE COLLECTION: S-expression () -- permanent representation of '() S-expression T -- permanent representation of 'T Env -- permanent global environment Input -- the most recent input S-expression -- function about to be applied in eval S-expression 6 -- temporary value s in evalList S-expression -- function about to be applied in eval Values () -- arguments of function about to be applied 13 -> @ At the time [[(show-roots)]] is evaluated, [[evalList]] has already been called to evaluate [[(* 2 3)]], and the result of that evaluation ([[6]]) is sitting in the local variable [[s]].

    Here's a similar example: <>= -> (set list3 (lambda (x y z) (cons x (cons y (cons z '()))))) -> (list3 (+ 1 2) (* 1 2) (/ 1 (begin (show-roots) 2))) ROOTS FOR GARBAGE COLLECTION: S-expression () -- permanent representation of '() S-expression T -- permanent representation of 'T Env -- permanent global environment Input -- the most recent input S-expression -- function about to be applied in eval S-expression 3 -- temporary value s in evalList S-expression 2 -- temporary value s in evalList S-expression -- function about to be applied in eval S-expression 1 -- temporary value s in evalList S-expression -- function about to be applied in eval Values () -- arguments of function about to be applied (3 2 0) -> @

    You can see some other examples that use [[show-roots]] online. To see what [[show-roots]] is supposed to do in any particular function, you can run ~cs152/bin/scheme-ms, which has the [[show-roots]] primitive. @

  3. Stage 3: Basic mark-and-sweep collection. In this stage, you will implement a mark-and-sweep collector for S-expressions. We have broken this exercise into several steps.

    Remember that the purpose of the garbage collector is to limit the amount of memory we have to request from the C heap. If you call [[malloc]] anywhere but from within [[growHeap]], you are doing something wrong. (But you don't have to eliminate any existing calls to [[malloc]]; we've taken care of that in stage 1.)

    To implement the Stage 3 collector, I had to add or change 196 lines of code, of which 54 lines are devoted to debugging.

    Major debugging hints: If there is something wrong with your allocator or collector, you will fail to mark some live cells, and programs will go wrong at a later time when you try to re-use a cell that is already in use. A good way to debug such problems is as follows:

    1. Call [[showRoots]] from your [[gc]] routine.
    2. Find a way of identifying each S-expression as garbage or non-garbage (e.g., an extra field in [[struct markedsx]]).
    3. Sweep all S-expressions after each mark phase, noting that non-[[live]] cells are garbage.
    4. Mark newly allocated cells as non-garbage.
    5. Write a procedure [[validate]] such that [[validate(s)]] halts with an error meeesage when [[s]] is garbage, and returns [[s]] otherwise.
    6. Use [[validate]] on all arguments and results of [[eval]]. If you ever find an S-expression that's garbage, your collector is broken.
    This technique will help you discover GC problems, albeit at some cost in performance.

  4. Stage 4: Garbage-collector performance. If the amount of live data is just slightly less than the number of S-expressions in all the arenas, the system will have to garbage-collect after every few allocations in order to keep re-using those few available cells. This ``garbage-collector thrashing'' can make it very difficult to get any useful work done. The ratio of heap size to live data is traditionally called [[Gamma]].

    A good memory manager should control the size of the heap. Create a [[targetGamma]] in your program so that after a collection, the arena list holds enough arenas so that the ratio of heap size to live data is at least [[targetGamma]]. That is, a [[targetGamma]] of 1.0 will make the collector thrash, a [[targetGamma]] of 2.0 will offer twice as much heap as live data, and so on.

    As you did in the closure tracing assignment, use a global variable target-gamma in your Scheme interpreter so that you can control [[targetGamma]] from within a Scheme program. Because the interpreters don't have floating-point support, you'll have to use an integer, so let target-gamma be 100 times [[targetGamma]], so that, for example, executing [[(set target-gamma 175)]] will set [[TargetGamma]] to 1.75. The initial value of [[target-gamma]] should be 100, so that your Stage 4 interpreter will behave just like your Stage 3 interpreter.

    Measure the amount of work done by the collector for different values of [[targetGamma]]. Plot a graph showing work per allocation as a function of [[targetGamma]]. You may want to plot work vs measured [[Gamma]] as well. [[~cs152/bin/jgraph]] is a good choice for plotting graphs, or you can use [[gnuplot]] or do it by hand.

    To help with your measurements, we are providing code that will spit out test cases using merge sort and insertion sort. If you call ~cs152/bin/mergetest 18 it will print out a definition of merge sort and a call to sort an 18-element array, so you can try

    ~cs152/bin/mergetest 56 | ./a.out
    for example, to try out your own interpreter. (And you can see that it works with the regular Scheme interpreter.) A similar script called inserttest lets you try the exact same test but using an insertion sort. You will probably want to sort arrays of many different sizes as you gather your measurements.

    Derive a formula to express the cost of garbage collection as a function of [[Gamma]]. The cost should be measured in GC work per allocation. For a very simple approximation, assume a fixed percentage of whatever is allocated becomes garbage by the next collection. How does your formula compare with your measurements?

    To implement Stage 4, I had to add or change only 9 lines of code in my Stage 3 interpreter.

    If you are not able to get your own collector working, you can take the Stage 4 measurements using my collector, which you will find at ~cs152/bin/scheme-ms. (Naturally, I expect to get credit in your README file :-)

  5. Stage 5: Copying garbage collection. In this stage, you will convert the collector to a copying collector. This will mean: When you grow the heap, be sure to do so in units of [[ARENASIZE]] (i.e., be sure the total heap size is always a multiple of [[ARENASIZE]]), so as to facilitate comparisons with your mark-and-sweep collector.

  6. Stage 6: Copying-collector performance. Add [[target-gamma]] to your Stage 5 collector, and repeat the measurements and the derivation you implemented in Stage 4. Remember that [[Gamma]] is the ratio of the total heap size to the amount of live data, not the ratio of the size of a semi-space to the amount of live data.
We reserve the right to use different values of [[ARENASIZE]] to exercise your collector, so you should be sure that your program works with any positive value of [[ARENASIZE]].

It is quite difficult to test a garbage collector, so the bulk of your grade will be based on your ability to convince us that you have implemented a correct collector. Don't forget to explain what you have done, and don't leave your explanation for the last minute! If you want to use noweb to help explain what you are doing, you might want to start with the noweb source for the stage 1 collector.

Extra credit

For massive extra credit, you may solve any or all of these problems:

What to submit

Submit files for all four stages, plus a write-up on paper. Your writeup should include: We expect you to explain the implementation of each of the phases separately. Explain the "whats" and "whys" of your approach. Be concise and clear.

If you are unable to complete the entire assignment, you can still get partial credit for intermediate stages.

If you wish, you may also turn in a file named transcript that contains test cases for your solutions.