Wrong, we did it.
Wrong, we did it.
Reference counting is usually not too bad from a complexity standpoint, but doesn't perform well for contended structures and has the cycle-collection problem, which may or may not matter depending on the algorithm.
I guess my point is: if you've solved this problem, the rest of us would love to hear how you did it :)
This was neither hard, nor impossible and surprisingly (<-- irony) we have excellent performance with a very low memory footprint (in other words, all the problems discussed in the post don't exist for us).
We have a custom memory allocator for scalability. As for memory management we either don't copy it (zero-copy) or when we have to track the lifetime, we use reference counted memory (which does not have the problems you point out when correctly implemented and used).
We don't have (yet) a mapreduce-like feature, but this is coming.
Actually the most difficult part was the network layer.
We have a workable version with which you can play here: http://www.wrpme.com/ Feel free to contact me in private if you have more questions.
Given that traversals require more or less one inc/dec per element, the ultimate efficiency of your algorithm is bounded by the throughput of atomic operations to a small number of memory locations (pretty low on x86 where they require cache-to-cache transfers, higher on something like a GPU where atomic functional units are colocated with the last-level cache), and by the amount of other work you're doing per element. For something like a linked list (or skip list) traversal, the inc/dec represents a 100-200% increase in the number of instructions per element, since the amount of other work is so low. In contrast, on a garbage collected system you can vary the collection frequency to achieve arbitrary amortized overhead.
So the overhead of ref counting is pretty variable, and depends on:
* Whether you can elide refcounts in some cases
* How much other work you're doing per element traversed
* The contention distribution across the elements in your
data structure (the first few elements of a linked
list, for example, are highly contended because
everybody starts there)
* How many concurrent accessors you have
* How often the accessors are hammering on the data
structure (vs. doing other things in the program)
* The throughput of atomic operations on your platform
To my mind, none of these have to do with 'correct' implementation or usage, they simply depend on the algorithm and the application. If your application consists of 1000 processors maintaining a sorted set of integers in a skip list, I don't see an easy way around making refcounting perform well. What am I missing?Now our implementation is a graph of small services (we call them nano-daemons) that slice a request into elementary tasks. nano-daemons communicate between them using messages that are copied around. This means a little memory overhead but absolutely no access contention. As you know C++ is very good at using the stack for that kind of allocation and the stack is FAST.
The actual content is only copied in the final phase into the network card's buffer. The reference counting is used to make sure the data is kept alive long enough, even if a parallel removal requested arrived meanwhile. The content isn't actually manipulated in parallel. An update implies the creation of a new buffer, reducing further the contention.
So in other words, if you have many parallel gets on the same data, you have, in theory, a little contention caused by the reference counter being incremented.
In practice, reference counting doesn't show up in profiling (so far). Actually most time is spent in the asynchronous callbacks waiting for I/O completion, something which we are very proud of! :)
We've benchmarked our software on a grid of 200 servers with 12 cores each and it does much better than other products, especially for entries larger than 8 kiB.
If you think about it, you realize that somehow, at some point, you will have a bottleneck, whatever design you choose.
However if the data set is constantly changing, because either you are churning through a lot of short lived memory objects or you have structures e.g. trees spanning a very large number of pages, then you have a memory management problem to address. Either way you are constantly blowing your L2 (and possibly L3) caches. Naturally, limiting those page swaps is the key to high performance. Concurrency exacerbates this demand (which is present even for single threaded servers) by placing isolation requirements on pages and data. So the name of the game is memory page management, e.g. "GC" in Dr. Click speak. His choice of telegraphic word is also highly informed: he is nailing the fact that removal of garbage is basically what the allocators all must struggle with and that the GC makes or breaks the allocator (for such high performance systems).
So I would flip Click's pithy statement and state verbosely:
High performance concurrent computing's requirement for data locality and isolation inevitably require very sophisticated memory allocation and memory pool management sub-systems. And beyond simple but effective heuristics such as object creation time (for locality) any further significant gains to be had can only be delivered by the garbage collection mechanisms of the memory manager.
This is not the same as:
"All concurrent algorithms are very easy to write with a GC and totally impossible using explicit free."
So if you've written a concurrent algorithm with an explicit free, congratulations, but that doesn't mean the statement is wrong.