https://dl.acm.org/citation.cfm?id=1028982
This had a number of important corollaries to this. One is that they have opposite performance characteristics: GCs make creating new references fast but then create a large pause when RAM is exhausted and a collection happens, while refcounts make creating new references slow and create a large pause when large object graphs are freed all at once. Another is that nearly all high-performance memory management systems are hybrids of the two: lazy refcounting is basically like implementing a local garbage collector and then feeding the live set of that into the refcounts, while generational garbage collection is like implementing a local refcount (the write barrier) and feeding the results of that into the root set.
Reference counting and address-semantics (which prohibits moving objects, so even if you work around the refcount, you can only do a tracing GC, but you don't get to compact) are deeply ingrained into CPython's C API as well, which is very widely used.
The problem with predictable destruction, we typically called "timely destruction", but more accurate would've been "immediate destruction" and if your language guarantees it, it tends to be great for automatically releasing resources (memory, file handles,...) in a lexically predictable manner. Softening any such guarantee should lead to entertaining resource races magically appearing in the real world. It occurs to me now that I never checked what pypy guarantees if anything?
We (Perl) never really came up with a great strategy for working around this (you tended to get the worst of both worlds in most naive reimplementations of the behavior in a GC-based interpreter) and while I certainly haven't tried to work out a clean argument to the effect, I'm fairly convinced it won't fly at all.
{
# do stuff
my $dbh = DB.connect(...);
# do stuff
LEAVE .disconnect with $dbh;
# do stuff
}
Whenever the scope is left (this could be because of an execution error, or a `return`), the code of the LEAVE phaser will be executed. The `with $dbh` checks whether the $dbh variable contains an instantiated object. If so, it topicalizes (set $_ to it) and calls the disconnect method (`.disconnect` being short for `$_.disconnect`).For more complex uses, and more tuneable finalizing, there is the `FINALIZER` module in the ecosystem: https://modules.raku.org/dist/FINALIZER
Perl is also ref counted. Swift too.
Edit: Also TCL, and PHP is mostly recounted...the "GC" just deals with circular refs.
ObjC had some elision tricks like objc_retainAutoreleasedReturnValue, but more importantly the optimizer was taught about RC manipulation. Swift then extended this with a new ABI that minimizes unnecessary mutations.
The big advantages of this scheme are efficient COW collections and a simpler FFI (very important with Swift). More generally RC integrates better. Imagine teaching the JS GC to walk the Java heap!
Cough. Actually, before ARC the ownership rules really kept RC traffic very low, and you could drive it lower still if you knew a bit about how ownership worked in your code ("these will balance and the object is already owned elsewhere...").
ARC just went mad with trying to give guarantees it really had no business making, and then hoping the compiler would be able to remove them again (difficult with message sending).
This caused some amusing bugs, for example a crash in the following method:
-someMethod:arg and:arg2
{
return 0;
}
How could this possibly crash? All it could possibly do is clear register AX and return.Well, with ARC enabled and in debug mode, it inserted code to first retain all the arguments, and then immediately release all the arguments. Apparently one of the args was a bogus pointer (framework-provided, so out of our control) and so it crashed.
return 0
is now considered "undefined behavior".Whatever.
You can try to defend yourself by stating the call is done by some framework but that just means the problem is still not in the language, it’s in the framework.
Second, whether the behavior is defined or undefined, the compiler has no business touching arguments that the code didn't tell it to touch. If it does so, it isn't fit for purpose and can be criticised as being unfit for purpose.
And the C standard is very clear and adamant that being in compliance with the standard is not, and in many ways cannot be, equivalent to being fit for purpose.
Last not least, I think we are probably not going to agree as to whether dereferencing unused arguments is a valid response to the presence of undefined behavior. See
https://blog.metaobject.com/2018/07/a-one-word-change-to-c-s...
Also, the Swift implementation is a bit questionable if performance is a goal. That is, why not try to remove the memory management from the inner loop? Probably the first thing to try is value types instead of reference types, which are more generally preferred anyway.
I believe all general purpose languages let the code allocate memory and therefore will let you allocate memory in a way inefficient to your task.
Hence why there is so much emphasis on value driven programming alongside protocols at WWDC talks.
Well, and they're often slower since they require mutating the reference count on every single time a field is stored. You can optimize that using lazy reference counting, or not updating refcounts for references on the stack and instead scanning the stack before a collection. But at that point... you're halfway to implementing a tracing collector.
Every refcounting implementation eventually gets "optimized" to the point that it has most of a tracing collector hiding inside of it. I think it's simpler, cleaner, and faster to just start with tracing in the first place.
There's a reason almost no widely used language implementation relies on ref-counting. CPython is the only real exception and they would switch to tracing if they could. They can't because they exposed deterministic finalization to the user which means now their GC strategy is a language "feature" that they're stuck with.
That being said, ref-counting is fine for other kinds of resource management where resources are pretty coarse-grained and don't have cyclic references. For example, I think Delphi uses ref-counting to manage strings, which makes a lot of sense. Many games use ref-counting for tracking resources loaded from disc, and that also works well. In both of those cases, there's nothing to trace through, and the overhead of updating refcounts is fairly low.