3 ms·
ARC is a reasonably straightforward feature that can largely be thought of as a syntactic pre-pass. Figure out which fields on objects need to have refcount in
by kannanvijayan 12y ago
ARC is a reasonably straightforward feature that can largely be thought of as a syntactic pre-pass. Figure out which fields on objects need to have refcount instrumentation, insert the appropriate incref and decref before and after the write, respectively.
That said, the runtime overhead of reference counting is significant and works against the cache architecture of modern CPUs. In a refcounted system, a simple field update Update(obj, offset, target) becomes the following:
Object *o = obj[offset]
if (--(o->refcount) == 0)
AddToBackgroundFreeList(o);
++target->refcount;
obj[offset] = target;
In assembly, in the best case, this roughly translates to:
1. Memory load (to get obj[offset])
2. Memory load (toget obj[offset]->refcount)
3. Decrement (refcount of old value)
4. Memory store (to update obj[offset]->refcount)
5. Test (to see if old value is now dead)
6. Memory increment (to update target->refCount - 1 instruction on x86/x64, 3 on ARM)
7. Store
Let's assume the best case: the old object being dereferenced does not become dead, and the branch predictor is good with the Test, so (5) is the cost of an ALU instruction and no misprediction penalty.
However, memory accesses at (2) and (4) are at arbitrary locations on the heap. The old value in the field points to an object anywhere on the heap, and the new value being stored points to an object anywhere on the heap. Neither of these memory locations are as likely to be as relevant to the current working set as the memory of the object being written to. After (2) and (4), you've gone through a bit of cache churn.
Now, this is happening on every single write. It's a HUGE overhead. Secondly, because refcounting doesn't really have an understanding of the object heap as a whole, it's generally impossible to do moving memory management for these languages. This means one of two things: either an expensive |malloc| for all allocations, or if you're using pool-based allocation - fragmentation of memory.
A modern GC reduces this overhead quite a bit. The state-of-the-art currently in reducing the overhead here is the combination of generational GC with card-marking for the older generations. Memory allocation cost is literally 3-4 instructions, and there's no fragmentation. Most object writes are cheap, since most activity happens in the nursery where write barriers don't need to be updated. The average write overhead for pointer-writes into the nursery is an extra load and a bit-test, but this extra load is from the same memory space that the field-write will be writing to, so either it's already in cache or going to be pulled in soon. Pointer writes to non-nursery objects have a larger overhead, but this is still smaller than doing the corresponding refcount checks.
The big advantage of refcounting is how this overhead is split up across program runtime. Proper GC has the issue of pause times due to garbage collection. Modern designs have basically eliminated pause times as it relates to general computing, but it can still be an issue in games etc. where a GC in the middle of frame, even a few ms long, can cause a frame skip.
Refcounting has its uses and its advantages, but it's dangerous to think of it as having no runtime overhead. It has a HEAVY cost on performance and it thrashes memory and it works against CPU cache architectures.
- tdg 12y agoI don't get how your code works. Why do you decrement first, then increment? Regardless, your explanation is mostly correct. There will be an incref/decref pair for most accesses. However, LLVM (and Apple's refcount implementation) does 2 very important optimizations: - LLVM can recognize (via static code analysis) redundant incref/decref pairs and take them out. - I can't find the link for it right now, but Objective-C usually stores refcount inside the pointer (in other words, the memory address) itself. So as long as your refcount is small enough (which it will be 99% of the time), incref and decrefs don't thrash caches but work on the value already in register.
- kannanvijayan 12y agoMy code doesn't work under race conditions. You're right the increment should be done before and the decrement after to avoid races. I just banged out the logic without giving it too much thought. On removing redundant incref/decref pairs - this is nice, but it seems mostly a benefit because of the "automatic" part, not the refcounting part. Developers dealing with manual refcounting would probably recognize places where refcount manipulation is redundant and just elide them. A naive automation ends up inserting them everywhere, and this helps bring that back to what a "smart developer" would have written anyway. On your second point, I've heard about this trick - in particular in smalltalk implementations. It's something I haven't fully looked into. It seems like the in-pointer refcount would only be valid if the pointer never escaped the function frame. More generally, you'd have to be able to guarantee that a particular pointer is the only reference to an object before the refcount could be stored in the pointer bits itself. If you have more insight into the implementation of this, I'd be interested in hearing it. This "small refcount pointer tagging" thing is something I've been meaning to understand better but haven't gotten around to.
- tdg 12y agoHmm, turns out I am wrong about the second point. Obj-C does not store the refcount in the pointer address: https://www.mikeash.com/pyblog/friday-qa-2013-09-27-arm64-and-you.html https://www.mikeash.com/pyblog/friday-qa-2013-09-27-arm64-an... They store it in isa field. Still, as long as you also call any virtual method of the object (or due to cache lines, use any field), it will still be pretty fast.