4 ms·
I think you're mostly correct about the { acyclic } pragma, except that the "Otherwise without" part needs a provisio "if static type analysis doesn't prove tha
by KMag 6y ago
I think you're mostly correct about the { acyclic } pragma, except that the "Otherwise without" part needs a provisio "if static type analysis doesn't prove that objects of the type cannot participate in cycles".
I've worked on a system that used a collector based on that Bacon and Arjan paper linked in the article. It's a very interesting paper. It has something like seven colors as opposed to the classic three color collector.
One of the colors is for objects where static type analysis shows that cycles can't exist: ClassA has references to only objects of ClassB and ClassC, ClassC has references to only objects of ClassD, and classes B and D don't contain any references, so objects of any of these classes can't participate in cycles and are acyclic types. If ClassD had references to objects of type ClassA, then there would be a possibility of cycles. Note that tree structures are very common, and static type analysis (without linear/affine types) cannot rule out cycles in recursive data structures such as trees.
Edit: a quick search appears to show Nim has at least some limited support for affine types in its type system, so trees might be a bad example of where { acyclic } is helpful.
Presumably, the { acyclic } pragma is an escape hatch for these cases where the human knows some invariant that isn't visible from just type analysis. As you allude, the GC algorithm is still correct without this information, just less efficient.
I also agree that it looks like they run a variant of the GC algorithm when passing a reference between threads: run a variant of the algorithm that doesn't actually free unreachable objects and pretend to decrement the reference count on the passed object. If the GC algorithm shows that all objects reachable through the passed object would have been GC'd had you decremented its reference count, then it's safe to pass the reference between threads.
On a side note, my use case of a Bacon-Arjan type collector was in a codebase for a domain-specific language with a functional-reactive programming model. We actually used the GC for pruning dependency graphs, to remove nodes that didn't produce externally visible effects. Someone somewhere in a big C++ codebase for the DSL interpreter missed incrementing a reference count, resulting in live nodes being pruned from the graph under rare circumstances. We put a lot of effort into trying to find the missing refcount increment, but ended up ripping out the Bacon-Arjan collector and going with a dead simple mark-and-sweep. We only needed to run the GC in the rare occasions that the graph topology changed, so the performance difference wasn't very noticeable.
- elcritch 6y agoAh, interesting to know that Nim's system uses some form of affine/linear types. I'm not sure of the extent or details as I've been busy making use of other parts of Nim, but I really want to read up on linear types. Nim's type system is powerful enough to pass write effects which would be pretty handy for first class immutability [1]. Not sure how much that overlaps with linear types though or if it's an orthogonal aspect. On a side note, my use case of a Bacon-Arjan type collector ... Interesting story! I enjoy reading those, and I know the Nim team had a lot of work to get ARC & ORC working well, which also required a lot of work on move/sink as well in the stdlib. I could see it being very difficult to implement in a big C++ code base. From the article it seems M&S can even be a bit faster than ARC, but with higher latency, more memory usage, and harder to target various platforms like WASM. I'm glad as it makes Nim excellent for usage on microcontrollers! No worries on shared heaps or unpredictable GC pauses. 1: https://nim-lang.org/araq/writetracking_2.html https://nim-lang.org/araq/writetracking_2.html
- KMag 6y agoThe total CPU overhead of M&S should be proportional to the number of live objects times the rate of object allocation. (Traditionally, collections were triggered every so many allocations or so many bytes allocated, but heuristics based on the amount of free heap would give similar behavior.) The total CPU overhead of reference counting is proportional to the rate of reference creation and destruction. You can really bring this down if your compiler is told or can figure out which references can be borrowed. The best case for reference counting is where the compiler is told (or can infer) which references are borrowed, and the code is written in a style that mostly borrows references, and rarely mutates data structures. One interesting hybrid approach is "one bit reference counting". You have a 1-bit flag in the object header that gets set when a reference to the object gets copied. If a reference gets destroyed, and the object doesn't have the flag set, that means that the referenced object is garbage. Lots of objects never have more than one reference to them, and this little tweak allows for more immediate reclamation of many objects, which allows you to get away with doing tracing GC less often.