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.