Reference counting has high runtime overhead, especially atomic reference counting with multiple threads. If you overwrite a pointer, you'd need to also look up and update two counts and do all that in a manner that is effectively atomic -- and that is complex on current hardware. I've heard that Swift programs could have as much as 40% runtime overhead from ARC.
I still think that reference counting is promising though. First because it meshes well with static analysis memory-management techniques such as inference of uniqueness and borrowing -- that can optimise away RC altogether. (and Swift's compiler already does some of that). I have not seen any work that could optimise away tracing GC in a similar way.
Second, because I believe that it would be possible to design hardware with object-memory addressing that would performs atomic reference counting with no additional runtime cost.
ref counting is expensive in multi-threaded applications. Overall it would have worse performance. When it comes to predictability: deallocating a linked list (for instance) would have to deallocate all of the elements. Dealing with reference cycles is also not simple, either.
Linked-list freeing need not all happen in one go. There can be a queue of free-but-not-zeroed, push RC=0 stuff there instead of recurring on referents, anyone doing memory allocation work can pop a few items off that queue and decrement the counts of their referents, etc.
That loses timeliness, adds problem of queue management.
A weird trick I stumbled across years ago is to not strictly pop from the head of the stack/queue -- instead choose randomly from the last N elements of push end of the stack. This seemed to blunt the sort of growth that you get from "visiting wrong". I never did figure out the theory of why this worked.
Because the industry has plenty of experience with referece counting as the very first GC algorithm, already in the early 1960's, in early Lisp implementations, BASIC, Cedar, and several other languages.
The predictable runtime performance is also a myth, because they never take into account the use of NUMA memory, lock contention, possible stack overflow and stop the world in the case of cascaded deletions in naive implementations.
Can you elaborate on how cascaded deletions "stop the world" with reference counting? I understand how GC could lead to arbitrarily large latencies for whatever task triggers that kind of cascaded deletions, but as I understand it, "stop the world" usually means that no threads are allowed to execute application code.
Imagine a graph or tree data structure where the deletion of a node causes a cascade deletion of all child nodes, which also causes a deletion of their children and so on.
This is proportional to the data structure being deleted.
Unless you use techniques to move the deletion into background threads, e.g. C++/WinRT with COM AddRef/Release, the thread will be "blocked" doing busy work cleaning all those nodes, running the cleanup code (destructors, deinit, whatever), node after node.
Yes, that's exactly what I said I understood. When Go (and Java) people say "stop the world" for GC, they mean all goroutines/threads stop, not just one. I don't think you get that with even naive reference counting.
Places that were doing C++, but found out that the right GC, and JIT compiler, can achieve good enough performance for their business case.
There is nothing "special GC" about it, the failure is to assume there is only one way to implement GC algorithms, as if there is only one way to implement hash tables, tree re-balancing algorithms, .... and then place all languages into the same bucket.
Then we have the modern times with AI driven code generation, where no one cares how their agents are actually doing the work, with what kinds of resource management approaches.
Immediate reclamation, yes. There do exist a number of reference-allocation algorithms that performs deferred reclamation similar to how tracing GC does.
There do exist algorithms for resolving them, and that process can too be deferred, but also restricted to subgraphs and only when cycles are possible.
Not that much more. Drop the last reference to e.g. a tree and you're doing an arbitrary amount of work to free it, right there. People resort to hacks like sending messages to "freeing threads" dedicated to that.
Either the size of the data structures created is limited and known, or it isn't. Whether the work is done in a refcount-decrementing recursive descent or a tracing garbage collector doesn't change that part.
And a refcounted tree freeing won't necessarily run at the end of the current scope, that depends on what other work is being done currently, and the size of the tree is a subset of all allocations until last user drops it.
and yet whether or not other work is being done currently is in your control, thus predictable (unless you chose to give control out of your hands e.g. by introducing two threads racing against each other); same regarding the subset
Because that is precisely the trade-off we're discussing.
With immediate reference-counted reclamation, dropping the last reference can put an arbitrarily large amount of destruction and deallocation work on the current thread. If you defer that reclamation to get it off the critical path, you have introduced a collector that reclaims unreachable objects later.
Whether that collector discovers garbage by tracing or receives zero-reference objects from reference counting is an implementation detail. In both cases, memory reclamation is automatic and deferred.
So the supposed distinction "with ARC I know when I pay the reclamation cost, with GC I don't" only holds if you're willing to pay an unbounded reclamation cost synchronously when the last reference is dropped.
(though of course a swap is a swap - but you can "trigger" it depending on your memory or file access pattern)