23 karma · joined May 27, 2022
But my main point was that the presence of a GC has nothing to do with how close a language is to the metal. Memory management strategy and low-level capabilities are two separate things.
> actually, you don't know
I know because I’ve verified it.
The cost isn't bounded either. Dropping the last reference to the head of a list or the root of a tree frees the whole structure at that block exit, and its size is a runtime property.
And it isn't only block exits. Every assignment to a variable or field holding a reference decrements the old target, and so does removing an element from a container. Swift's ARC doesn't even promise the scope boundary: the optimizer may release right after the last use, which is why withExtendedLifetime exists.
By the same "where" criterion a non-concurrent tracing GC is predictable too, because it can only run at allocation points. That doesn't tell you which allocation will trigger it, just as knowing the block exits doesn't tell you which one will free.
It’s the same with ARC. You also don’t know when the counter will reach zero.
It doesn't. I've been building one as a plain header-only library (SGCL, https://github.com/pebal/sgcl - my project): the heap is traced precisely through per-type pointer maps the collector builds at runtime by elimination (a word ever seen holding a non-heap value is data, for good), marking and sweeping run concurrently with the mutators and in parallel over helper threads, cycles are generational, and nothing is ever moved. What it can't do without the compiler is the stacks, which it scans conservatively, like Go before 1.4.
On the "abstract model" point: you're right that it isn't 100% faithful either. The collector reads words of objects while other threads write them, relies on word-sized stores being what every real platform makes them, and reads stacks it does not own. The rules the program has to keep are few (a tracked pointer lives on a stack or in a managed object, never shares storage with data, destructors don't touch peers) and debug builds check them, but it is engineering on top of the platforms, not the standard.
On the sweep: it's true that a non-moving collector's sweep is proportional to the number of dead objects, and a moving one's isn't. In practice that cost is small and parallel: the sweep walks per-page state bitmaps (about 1 ns per object), runs the destructors of the dead objects, and is spread over helper threads while the mutators keep running - nobody waits for it. What moving actually buys you is bump allocation and locality. Per-type pages with thread-local free bitmaps get allocation to about 5 ns per object on one thread and 9 ns on 24 threads, without a pause and without moving anything; ZGC does 3.6 ns and 26 ns on the same machine. On binary-trees at depth 21, ZGC is ahead on one thread (2.5 s vs 4.5 s) but with a 1.1 GB heap against 340 MB, and on four threads they tie (1.6 s each). Go, which is also non-moving, sits at 6.3 s / 219 MB there. So "non-moving" is not what decides it; the cost of the barrier, the marking and the sweep spread over cores decides it.
On destructors: they run on the collector's threads, in parallel, not on one thread, and the rule is the same as Oilpan's - a destructor must not touch other managed objects, because they may be dying in the same sweep. I don't have a Clang plugin to enforce it statically; there is a runtime check in debug builds and an explicit escape hatch (if_alive()) for the one legitimate case, a destructor asking whether a peer is still there. Oilpan's static verification is the nicer answer to that particular problem.
Where the approaches really differ is how the collector finds the pointers. Oilpan needs a Trace() method per class (or, as someone suggested above, reflection to generate them). SGCL builds a pointer map per type at runtime by elimination - a word that is ever found holding a value that isn't a managed address is data and leaves the map for good - so plain structs with tracked pointers in them just work, at the price of a couple of rules (no union of a pointer with data, stacks scanned conservatively). Different trade-off, not obviously worse.
Can you give an example of such GC libraries?
> Whoever made that claim? Gamedevs particulary have been writing custom memory allocators since decades precisely because they know free() is not free and malloc() isn't fast.
Game developers use engines based on the GC.
> It's not an illusion, you literally use control over memory management.
The shared_ptr does not provide full control.
> What they don't want is random stalls in odd frames.
You can have fully concurrent GC, without any stalls.
You can't write concurrent code without atomic operations — you need them to ensure memory consistency, and concurrent GCs for Java also rely on them. However, atomic loads and stores are cheap, especially on x86. What’s expensive are atomic counters and CAS operations — and SGCL uses those only occasionally.
Java’s GCs do use state-of-the-art technology, but it's technology specifically optimized for moving collectors. SGCL is optimized for non-moving GC, and some operations can be implemented in ways that are simply not applicable to Java’s approach.
I’ve never tried modeling SGCL's algorithms in TLA+.