Wednesday, October 8, 2014

Lessons Learned From a Generational GC in Racket

I recently wrote a second garbage collector in racket, using the plai gc2 module, and I would recommend it over the older gc module. It allows for use of more racket functions, provides for creation of randomized test programs which use the subset of racket the plai gc module supports, and makes it possible to unit test your program.

I found that it was fairly easy to implement the overarching points of collection, but this project definitely drilled home the insanely stateful nature of a collection system. There tended to be a need for information to be shared promiscuously. Testing was difficult and generally took the form of using automated tests to detect a problem, and then time-consumingly adding in print-debugging and hooks to trace it down to the broken system. The closure environments and interesting pointers were the most painful to debug.


With closure environments, I learned that even in sequential computing it is very possible to get a race condition. While the mutator/user-facing side of the collector was atomic, the problem lied in the fact that I was using a cons chain to store the closure environment members and that collection could be triggered during the allocation of any of the individual elements. Since I hadn't expected that, the roots that were updated were discarded. I needed to wrap all references in that operation in "roots" so that my collector updated them appropriately.

With the interesting pointers, it took some exploration of a way to force racket to allow me to work with internal functions so that I could set up a delicate set of old-to-new references, make sure that I scanned the remember set correctly, move them, and figure out why my updates wasn't working. It ended up being that I had miscounted my three layers of indirection(my helper returned the location of the interesting pointer as recorded in my data structure, which you dereference to get the interesting pointer, which you dereference to get the cell) and was not actually doing anything in the update method.

After that, I was able to test the collector with randomized tests to the point that no errors were seen. Heap memory was exhausted quite frequently, and even when the heap was bumped up so space didn’t run out, racket stopped the application when it hit 256mb of memory used in creating some very large con chains for closures. Compared to my mark-sweep collector, the space overhead of a generational collector was significant. The need to store closures on the heap in the gc2 module added to this overhead, but I will admit that I enjoyed that new “burden.”

Overall, racket’s gc2/mutator2 languages for the plai module are quite well done. The fact that I was able to garbage collect, or not, variables in closures by collecting the closures themselves was very rewarding when one considers that this platform is designed for students who want to implement a simple semi-space copying or mark-sweep collector. I scaled it up to a generational collector with a partitioned nursery to allow objects to have time to die, including remembered sets and closure cons chains, and the framework handled very well.

My only complaint would be not with the module, but with the documentation surrounding how the module works around your code. While the abstraction works well, one must puncture it to allow unit tests and to output heap snapshots over time, and to use the visualizer on very large heaps. It took a while to figure out how to get access to functions in my collection program from within test files. After that was done, creation of stub “heaps” and arbitrarily declaring things roots or allocating randomly into the heap and seeing how my helper functions behaved was trivial.

Overall it was a great educational opportunity, and I would recommend inclusion of an exercise using plai in any class for which it is appropriate.

No comments:

Post a Comment