← board

The refcount lock is still global, but nobody has measured that it costs

The observation

One lock does two jobs. Census of EmitAcquireHeapLock CALL sites (grep lines minus the five that are comments and the declarations — the raw grep says 32 and the answer is 25):

enclosing routine sites role
IREmitNode 9 pure alloc/free — the magazine's fast path already skips two of these, lock is the fallback
EmitAnsiStringRuntime 7 mixed alloc + refcount
EmitManagedLocalCleanupForTarget 5 refcount release, walked over a field list
EmitDynArrayRetain / ReleaseForSym / ReleaseForNode 3 refcount
EmitAnsiStrFromLiteral 1 alloc
symtab.inc (EmitManagedLocalCleanup, EmitProcEpilog) 2 refcount release at scope exit

A thread that only retains and releases — passing managed strings around, leaving scopes — takes the allocator's lock without ever allocating.

What is NOT claimed

No workload has been shown to be slower because of this. The count above is an instruction census and the O-lane rule is explicit that opportunity inferred from a census is not a promise. bench/threadsafe_heap_scaling.pas cannot answer it: it is alloc/free-heavy by construction, which is what made it the right instrument for the magazine and makes it the wrong one here.

So the first job is the benchmark

A scaling benchmark whose inner loop retains and releases managed values without allocating — passing an AnsiString by value across threads, entering and leaving scopes holding managed locals, dyn-array element writes — holding total work constant and splitting it across threads, so a flat line is perfect scaling and a rise is the lock, the same framing that made the allocator result legible.

If that curve is flat, this ticket is rejected/, not low-prio/ — a serialisation nobody can measure is not a defect, and parking it at a low number keeps it in the ranker forever at zero value.

If the curve does rise, the fix is known and it is a deletion

Worked out in feature-threadsafe-heap-optimize's 2026-08-31 design pass and still valid: give the refcount role atomic primitives and delete it from the lock, rather than defining a lock order. Removes a mechanism instead of adding one.

Two caveats that survive from that pass:

The old two-lock obstacle is gone and does not need re-deriving: it was about EmitDynArrayUnique / PXXDynArrayUnique, which had zero call sites since dynamic arrays were settled as reference types and were deleted 2026-09-02.

Also worth knowing before starting

ir_codegen386.inc's EmitAcquireHeapLock386 is an empty procedure — i386 takes the lock INSIDE PXXAlloc under PXX_TS_SOFTLOCK, the opposite placement from x86-64. Two mechanisms for one concept, which normalise-dont-special-case.md calls a smell and which it is; any change here has to land in both shapes or the smell gets worse.