And C++11 just got reference counting as an official library option.
Reference counting is better than mark-and-sweep GC for several use cases:
* Real time code where you don't ever want a GC to steal cycles. I know that a lot of research has been done to decrease the amount of time stolen, but it's always non-zero.
* Immediate, deterministic clean-up of resources as soon as they are no longer referenced: If you have a limited number of file handles, for instance, you want them closed ASAP, and not when some GC decides it's time.
* No performance penalty for having weak references. I use this in asset management: A map of asset handles to weak references to currently loaded assets. If an asset is no longer used, it's deallocated immediately. Having weak references in a GC system can increase processing complexity.
> * Real time code where you don't ever want a GC to steal cycles. I know that a lot of research has been done to decrease the amount of time stolen, but it's always non-zero.
Real time code shouldn't allocate/deallocate memory, much less from a GC'able pool. With that constraint, it's possible to have real time code that coexists with a GC, such as RTSJ's NoHeapRealtimeThread or Eventrons, with an effective cost of zero cycles taken by GC from the realtime code.
Not talking "hard real time" here, but game code where you want to prevent frame glitches.
In C++ you can also replace the allocator to pull items from a pool, so that the "allocation" is "grab the first item from the pool linked list" and "deallocation" is "link this item back into the pool." The first case costs two pointer reads and one write, the second case costs two pointer writes and one read.
This lets you use objects as if they're dynamically allocated while still keeping very costs in allocation/deallocation.
Immediate cleanup is not necessarily an advantage. For example, it makes releasing a reference inside a critical section very dangerous, because you have no idea how much, or even what, work is going to be done. (Don't ask me how I know.)
Critical sections are really designed to wrap only a few lines of code. Basically nothing nontrivial should be done within a critical section, IMO.
If you're dealing with multithreading, the only safe thing to do with references is to put them in a list of "things to release later." And then do that from the main thread.
GC does make this easier, sure. But creating a "release list" is not hard. Making a GC not stall the program at awkward times is actually a lot harder.
Reference counting steal cycles all over the place. The counting itself steals cycles, and the effect of dropping a reference is unpredictable: whichever module happens to drop the last reference to an object which is the last gateway to a large graph of objects will suffer a storm of recursive reference count drops and deallocations.
If you have a limited number of file handles, you may want them closed ASAP, and not when some reference-counting mechanism or GC decides. Reference counting is not ASAP. Typically, you have some smart pointers which will drop the reference in relation to some lexical scope. That could be too late: the file handle object could be in scope over some long running function, and so the file is held open. The fix is to call the close method on the object, and then let refcounting reap the object later. (Woe to you if you decide to manually take over reference counting and start hand-coding acquire and release calls. Been there, debugged that.)
I implemented weak hash tables in a garbage collector; it's neither complicated nor difficult. Under GC, we use weak referencing for essential uses that requires the semantics, not as a crutch for breaking circular references.
The effect of dropping a reference is sometimes predictable. For example, Color cannot root a large object graph, so dropping a reference to Color will deallocate at most one object. At least it does not require nonsense like Android's allocation-in-onDraw warning.
I worked on the now-deprecated GC for Cocoa frameworks, and we made heavy use of weak references for out-of-line storage. This put us at risk for cycles: if A references B through a weak-to-strong global map, and B in turn references A, we have an uncollectible cycle even under GC. This represented a large class of bugs we encountered under GC.
So both GC and RC have their classes of cycles and unpredictable behavior. I've come to believe that these techniques are more related than we'd like to admit, and the real difference is in their second order effects. For example, GC enables lockless hash tables, which require hazard pointers or other awkward techniques under RC. On the other hand, RC enables cheap copy-on-write, which is how Swift's collections can be value types.
An atomic increment/decrement takes so little time as to make this irrelevant. If you're in such a tight loop that you care about a single increment when calling a function (to pass a parameter in), you should have inlined that function and preallocated the memory you're dealing with.
I'm talking about general use of smart pointers, which means that there's a function call involved with the smart pointer value copy, and throwing an increment in is trivial by comparison.
>whichever module happens to drop the last reference to an object which is the last gateway to a large graph of objects
When writing games, I don't think I ever had a "large graph of objects" get dropped at some random time. Typically when you drop a "large graph" it's because you're clearing an entire game level, for instance. Glitches aren't as important when the user is just watching a progress bar.
And you can still apply "ownership semantics" on graphs like that, so that the world graph logically "owns" the objects, and when the world graph releases the object, it does so by placing it on the "to be cleared" list instead of just nulling the reference.
Then in the rare case where something is holding a reference to the object, it won't just crash when it tries to do something with it. In this rare case a release could trigger a surprise extra deallocation chain, as you've suggested.
If that's ever determined to be an issue (via profiling!) you can ensure other objects hold weak references to each other (which is safer anyway), in which case only the main graph is ever in danger of releasing objects -- and it can queue up the releases and time-box how many it does per frame.
Honestly having objects reference each other isn't typically the best answer anyway; having object listeners and broadcast channels and similar is much better, in which case you define the semantics of a "listener" to always use a weak reference, and every time you broadcast on that channel you cull any dead listeners.
Aside from all of that, if you're using object pools, you'd need to deallocate thousands, maybe tens of thousands, of objects in order for it to take enough time to glitch a frame. Meaning that in typical game usage you pretty much never see that. A huge streaming world might hit those thresholds, but a huge streaming world has a whole lot of interesting challenges to be overcome -- and would likely thrash a GC-based system pretty badly.
Reference counting in C++ can be done really well even in the absence of language support.
For example, with the Qt library you can pass objects around by value, yet behind the scenes everything is reference counted with automatic copy-on-write. It's the best of all worlds. You get easy, value-based coding (no pointers), speed (because deep down everything is a reference), and deterministic destruction (no GC). http://doc.qt.io/qt-5/implicit-sharing.html
I'm curious if any languages have adopted a Qt-style approach natively.
Yes, Swift works this way. Arrays, dictionaries, and sets are value types and cannot be implicitly shared, which eliminates a large class of bugs. It's kind of amazing, isn't it!
> For example, with the Qt library you can pass objects around by value, yet behind the scenes everything is reference counted with automatic copy-on-write.
PHP does exactly this, but for arrays and strings only (objects are implicitly passed by reference). So you can pass arrays by value with no performance penalty, as they are actually passed by reference. A COW mechanism ensures you end up with a local copy only if you write to an argument; such mechanism is disabled when passing arguments byref.
This is incorrect due to the existence of reference cycles.
You can easily create a cycle of objects not reachable by any of your roots in your object graph. The ref counts won't ever reach 0 so to collect it you still need a gc pass periodically, imposing stop times. For the same reason you must never rely on ref counting to clean up file objects etc.
Sorry, but that's nonsense. Cycles don't just magically appear, you write them.
So writing refcounting code simply means being aware of this when designing the more complicated data structures in your code to use weak backreferences.
File objects are not secretly stashed in complicated graphs to prevent their destruction and you very much can rely on their behavior. GC passes to clean up cycles is something you got confused about: that's what GC does (because unrooted cycles are very much an issue there too!), not refcounting where you always have to break the cycles yourself, manually, preferably when designing the data structures.
> Cycles don't just magically appear, you write them.
That's like saying memory leaks doesn't magically appear, you write them. In real code, ref cycles are everywhere and it is not trivial to know beforehand what code will generate cycles. And don't give the spiel about how that's only something that affects bad programmers.
All modern Objective-C codebases and a high percentage of large C++ codebases use refcounting without a cycle collector. Some of them have memory leaks, and there are some quite ugly cases like [1], but most of them have it under control. So it's not like relying on pure refcounting is impractical in the real world; it's just a bit more difficult to deal with than full GC.
At the very least, Objective-C, Swift and PHP still use reference counting.