Cold start: zero GC, 380ms

5 min read
smalltalkvirtual-machinebootstrapsmalltalk-from-scratch

The cold start rewrite is done.

Loading the Smalltalk-80 image used to trigger 40–60 GC collections. The old loader created intermediate C# objects for each JSON entry, resolved references through a dictionary, and let the GC sort out the lifetimes. It worked, but it was slow and produced memory pressure that would only get worse as the image grew.

The new loader reads coldstart.json in a single pass. Each JSON object maps directly to its target slot in the heap — no intermediate representation, no dictionary lookups after the first pass, no objects that exist only to be discarded. References between objects are resolved by OOP index during the first pass itself, because the JSON is emitted in OOP order and every referenced object is guaranteed to appear before the object that references it.

Total load time: 380ms. GC collections during load: zero. The number isn’t a target — it’s what fell out of doing the allocation correctly.

The zero-GC property matters beyond performance. The cold start is the one moment when the interpreter state is most fragile: the object graph is partially initialised, no error handlers are installed, and a GC that moves objects would invalidate any OOP we’d already cached in local variables. By guaranteeing no GC runs during load, we can hold raw OOP values in C# locals throughout the loader without defensive copying.

The loader now hands a fully-initialised heap to the interpreter. The first thing the interpreter does is run the image’s own initialisation expressions — SystemDictionary initialize, InputSensor initialize, and so on — which were previously stubbed. They now run for real, against real objects, through the interpreter loop. The image reaches Transcript show: 'Welcome to Smalltalk-80' in well under a second from cold.

Load time: 380ms. Objects loaded: 4,412. GC collections: 0. Architecture: single-pass, OOP-ordered, no intermediate heap.