ARC: retain and release with fetch_add instead of a CAS loop - #402
ARC: retain and release with fetch_add instead of a CAS loop#402DTW-Thalion wants to merge 2 commits into
Conversation
d33e1c2 to
fffa53b
Compare
|
I think this approach is good, but these paths are starting to get quite cluttered. I’d like to have an explicit refcount class that has a from-object static factory method so that the refcount manipulation can be pulled out. That would also let us do the Apple-style refcount-stored-in-the-isa-pointer-with-overflow-in-a-table model. |
fffa53b to
52c5fbe
Compare
… CAS loop The strong-retain and release fast paths spun a compare-exchange loop that re-tried on every lost race, so under contention they wasted work that a single read-modify-write instruction avoids. A strong retain runs while the caller still owns a reference, so the object cannot be at (or reach) the deallocating sentinel; its increment is therefore a single fetch_add. Release becomes a single fetch_sub, handling the last-reference and saturation edges after the fact. The weak-to-strong retain keeps the compare-exchange loop, because it can race a concurrent final release and has to check-and-increment atomically to avoid resurrecting a dying object. Reserve the bit below the weak flag as a guard, so an optimistic increment can never carry a saturating count into the weak flag. FastRefCount.m mirrors the reference-count layout and is updated for the guard bit; the saturation and weak-at-saturation cases it exercises still pass. Measured on a 32-core machine: retain/release falls from 16.1 to 11.3 ns with no contention, and a single shared object under 24 threads from 2143 to 1124 ns.
52c5fbe to
1863a98
Compare
|
The commits on this branch reached master through #403, which was stacked on The explicit refcount class with a from-object factory method suggested above |
|
Correction to the note above: the refcount class is in master. arc.mm has The statement that it was not part of what landed was wrong. |
The strong retain and release fast paths ran a compare-exchange loop that retried on every lost race. A strong retain already holds a live reference, so the object cannot be at the deallocating sentinel and the increment can be a single fetch_add. Release becomes a single fetch_sub, with the last-reference and saturation cases handled after the fact. The weak-to-strong retain keeps its compare-exchange loop, since it can race a concurrent final release and has to test and increment together.
The count manipulation is behind a RefCount class rather than open-coded masks at each call site, obtained with RefCount::fromObject(obj). The weak flag, the guard bit that keeps a saturating increment from carrying into it, the deallocating sentinel and the saturation rule are all stated once, inside that type. Keeping it behind one type is also what would let the count move into spare isa bits, with overflow spilling to a side table, without touching any caller.
Uncontended retain and release drop from 16.1 to 11.3 ns on a 32-core machine with 24 threads active, and a single shared object under 24 threads from 2143 to 1124 ns.
#399 has merged, so the diff here is this change alone.