Skip to content

Build1 publisher2 min readPublished

Mark Shannon proposes a two-generation incremental GC after CPython reverted his 3.14 collector

Mark Shannon told the Python Language Summit that GC takes 11.67% of CPython's runtime and proposed a collector that is both generational and incremental. His 3.14 incremental collector was reverted over memory pressure, so the hybrid must match its short pauses on less memory.

The Engineer · Build desk

Illustration accompanying Mark Shannon proposes a two-generation incremental GC after CPython reverted his 3.14 collector

What happened

  • Shannon's 3.14 collector cut peak collection pauses to tens of milliseconds, against about 3 seconds for the generational collector CPython uses now.
  • That collector was non-generational, keeping the whole heap in a single generation, a layout that carries a cost in memory use.
  • His replacement design has one young and one old generation and alternates collection increments between the two.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure Servers and applications with user interfaces, the workloads Shannon named as most hurt by pauses, stay on the multi-second-pause collector until a hybrid ships.
  • decision Without a hybrid, CPython has to pick a default that favours either memory or pause time, because each existing collector wins on only one of the two.
  • cost JIT speedups enlarge the collector's share of runtime: halving the interpreter's 30.6% would lift collection from 11.67% to about 13.8% if nothing else changed.

Shannon defined a metric before he pitched a design, and I think that is the right order of work. Scavenge effectiveness is objects collected divided by objects visited [8]. Inverted, the generational collector's 0.3% means it visits about 333 objects for every one it frees, and the reverted collector's 1% means about 100 [1]. Put plainly, the garbage collector spends nearly all its visits on objects that are not garbage [1].

Shannon described the collector as a "backup" that "should be much faster" [5]. Reference counting already frees dead objects, so the collector only has to find unreachable cycles [5].

The collector reverted from 3.14 got its better ratio from which spaces it scanned [1]. New objects go into fixed-size spaces, and a space can only be scavenged after it fills and closes to new allocations [9]. With the whole heap in one generation, it could scavenge old spaces as well as young ones. Spaces that have existed longer tend to yield more collected objects [16].

On pause times it beat its own brief. The goal was to cut maximum pause times by an order of magnitude on larger heaps [2]. Going from around 3 seconds to tens of milliseconds is a factor of at least 30, even if "tens" means 99 [2].

In Shannon's terms, a generation is a run of consecutive spaces, and spaces move in only one direction through generations [10]. A young generation pays off under the generational hypothesis, that most objects die young, though the exact profile depends on the program [11]. In CPython I'd expect the question to be narrower. Reference counting already frees objects that die outside a cycle, so the young side helps only as much as cyclic garbage also dies young [5].

Other runtimes cannot answer that. Asked by Hood Chatham how Python's effectiveness compares with other collectors, Shannon said a direct comparison would be "unfair", because "nobody else does reference-counting and garbage collection" [12]. The 11.67% comes from a single graph, and the write-up does not name the workload behind it [3].

What to watch

  • Pause and memory measurements for the two-generation design on large heaps, set against the 3.13 collector's roughly 3-second peaks and the reverted collector's memory use.
  • Which CPython release, if any, adopts the young-and-old incremental collector as its default.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories