Skip to content

Commit c832d41

Browse files
committed
jit: accelerate active-MT table length
1 parent c42339b commit c832d41

10 files changed

Lines changed: 370 additions & 27 deletions

notes/b1.2.1-roadmap-and-progress-2026-07-18.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,53 @@ Development sequencing remains Linux-first. The complete macOS and Windows
299299
test/fix matrix is deferred until b1.2.1 is otherwise release-ready; incidental
300300
cross-build or compatibility smokes before then do not expand the active
301301
platform scope.
302+
303+
## 2026-07-25 active-MT length performance checkpoint
304+
305+
The bounded generated table-length path now has a separate x64 JIT-only ABI.
306+
It requires the exact `TMPREF IN1` root in the universe-aware current TG,
307+
published JIT entry, exact physical actor/state ownership, and a caller-owned
308+
table-vector epoch. JLOOP's live `jit_base` keeps GC bodies retained across the
309+
call; the epoch retains any paired array/hash generation captured by the
310+
helper. Root, generation, carrier and ownership are revalidated before the
311+
integer result escapes. Direct/interpreter calls fail closed, and the general
312+
rooted helper remains available for adversarial roots and non-x64 fallback.
313+
314+
This removes the counted table lease and global SMR reader from the x64
315+
generated hot path without weakening the lockless contract. On a core-pinned
316+
single-core nine-sample run of 20 million traced `#table` operations, active-MT
317+
length fell from 64.125 ns to 9.332 ns. The stock pre-MT `IR_ALEN` path remains
318+
unchanged at 2.461 ns. Thus the new path is about 6.87x faster than the first
319+
bounded implementation and 3.79x the stock lowering; the remaining
320+
absolute/ratio gap stays explicit performance debt.
321+
322+
Focused coverage includes direct misuse rejection, actual generated admission,
323+
generation-change retry and side-exit replay, exact nested epoch restoration,
324+
unchanged wait/allocation/lease accounting, `TMPREF IN1`, and the absence of
325+
active-MT `IR_ALEN`. This is a Linux-first checkpoint; macOS and Windows remain
326+
deferred until the rest of b1.2.1 is release-ready.
327+
328+
The final Linux source passed clean focused M5/M6 gates, 20 repeated C-fixture
329+
runs, five repeated IR runs, 30 heavy six-writer/two-GC-worker resize runs, and
330+
the broader resize/JIT-read/weak-finalizer/remote-stack/worker-activation
331+
matrix. Strict GCC and Clang `-Wall -Werror` builds passed. Clang ASan and
332+
UBSan each passed their strict build, 20 focused repetitions, ten heavy stress
333+
repetitions, and the mixed concurrency matrix before the default build was
334+
restored.
335+
336+
The interpreter wait is not closed by this optimization. Audit proved that a
337+
paused resize owner can replace a source with `FORWARD` before installing the
338+
value in its successor, with the only surviving value held in an owner-local C
339+
variable. Re-yielding or redispatching cannot manufacture that fact. Exact
340+
paused-owner completion needs a persistent resize descriptor published before
341+
retirement/forwarding, with durable per-slot moving state and helper-safe
342+
successor accounting.
343+
344+
The safest structural precursor is a versioned help lane for ordinary shared
345+
new-key insertion. Its descriptor must publish immutable table/key/generation
346+
facts before the lane becomes visible and use intent-before-side-effect phases
347+
for freecount reservation, `KEYLOCK`, collision linking, key publication and
348+
rescan handoff. Pre-MT/private insertion stays stock. FINREG remains a separate
349+
follow-on because its indistinguishable `FINCLAIM` also transfers into the
350+
order-list/finalizer transaction and cannot be closed honestly by the ordinary
351+
new-key descriptor alone.

notes/bounded-rooted-table-length-jit-2026-07-25.md

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,32 @@ the ordinary single-threaded hot trace unchanged.
5050
After MT activation, supported table-length recording emits:
5151

5252
- a generated-frame `TMPREF IN1` `TValue` root for the table;
53-
- a nonthrowing `CALLS` to `lj_tab_len_rooted_try()`; and
53+
- on x64, a nonthrowing `CALLS` to `lj_tab_len_forjit_try()`; and
5454
- a guard that side exits when the result is `LJ_TAB_LEN_RETRY`.
5555

56+
The generated-only helper cross-checks the dispatch `tg_hint` against the
57+
universe-aware current TG, requires the exact `tg->tmptv` root, live JIT entry,
58+
physical actor and state ownership, and enters an owner-written table-vector
59+
epoch before acquiring either structural root. JLOOP keeps `jit_base`
60+
published across ordinary generated calls, so destructive GC2 reclamation
61+
cannot begin and `tmptv` remains an enumerated exact root. The per-TG epoch
62+
independently prevents an old array or node generation from being reclaimed
63+
after it is captured. Root, paired generation, carrier, JIT and owner facts are
64+
all rechecked before returning the scalar result.
65+
66+
This narrower ABI removes the global SMR reader and counted table-body lease
67+
from the generated hot path. The general `lj_tab_len_rooted_try()` remains
68+
unchanged for adversarial C roots and non-x64 generated fallback; direct or
69+
interpreter calls to the x64 ABI fail closed.
70+
71+
The common dense/no-hash shape also has an exact bounded shortcut: after the
72+
paired snapshot it accepts `asize-1` only when the final positive array slot is
73+
a valid non-nil Lua value, then performs the same paired-current check. Empty
74+
tables are exact zero. A boundary hole or any hash part uses the full widening
75+
and binary search; internal/malformed observations request retry. FINCLAIM
76+
classification and the owner-written epoch operations are inline on this hot
77+
path, while their existing external entry points remain available elsewhere.
78+
5679
The side exit replays the current bytecode in the interpreter. The bounded
5780
helper has made no Lua-visible mutation, so replay does not duplicate an
5881
effect. This lowering is shared by direct table `#`, Lua 5.2-compatible
@@ -72,8 +95,10 @@ The focused C fixture covers:
7295
- table-root replacement;
7396
- exact-owner loss and an invalid owner;
7497
- bounded generated-code retry, side exit, and interpreter replay; and
75-
- balanced SMR, lease, root-descriptor, and dynamic-root-anchor accounting,
76-
with no table wait-counter movement and no helper allocation.
98+
- direct-call rejection by the generated-only ABI;
99+
- exact nested table-read depth/epoch restoration; and
100+
- balanced SMR, vector epoch, lease, root-descriptor, and dynamic-root-anchor
101+
accounting, with no table wait-counter movement and no helper allocation.
77102

78103
The JIT IR gate checks all live traces. It requires stock `IR_ALEN` and no
79104
rooted helper before MT, then requires `TMPREF IN1` plus the rooted `CALLS` and
@@ -104,11 +129,35 @@ correctness and hot-path performance.
104129
The important common-case property is exact: pre-MT traces retain stock
105130
`IR_ALEN`, so this change adds no generated-code instruction to that path.
106131

107-
A preliminary single-run active-MT microbenchmark measured roughly 64 ns per
108-
`#table`, versus roughly 2.8 ns on the pre-MT stock lowering (about 23x). That
109-
is an absolute bounded cost, not performance parity, and it remains explicit
110-
optimization debt. The result is not a release benchmark or a substitute for
111-
the required reproducible three-sample performance gate.
132+
A nine-sample, core-pinned 20-million-iteration microbenchmark measured
133+
2.461 ns per pre-MT `#table` and 9.332 ns for the x64 active-MT generated
134+
path. The committed general rooted ABI measured 64.125 ns in the same run, so
135+
the generated-only epoch path is about 6.87x faster than the first bounded
136+
implementation and 3.79x the stock pre-MT lowering. A nearby active-MT rooted
137+
point read remained 64.361 ns, confirming that the length improvement is the
138+
removal of general lease/SMR machinery rather than a benchmark artifact.
139+
140+
This is a reproducible microbenchmark result, not yet the complete b1.2.1
141+
three-sample suite gate. The remaining roughly 6.9 ns absolute delta is explicit
142+
parity debt and the 3.79x ratio is still above the planned 3x gate, but the
143+
former 25x cliff is gone and incremental optimization is now realistic.
144+
145+
## Linux validation
146+
147+
The final production-source diff fingerprint was
148+
`849fcdffb449db02d4803478a92c386f497a443ff33b4b0c19abe291b8814df7`.
149+
Clean focused M5 and M6 gates passed, followed by 20 repeated C-fixture runs
150+
and five repeated JIT-IR runs. A six-writer/two-GC-worker length-and-resize
151+
stress with 8,192 resize rounds and 4,096 observer rounds passed 30/30, and the
152+
broader resize, traced-read, weak-finalizer, remote-stack-GC and worker-
153+
activation matrix passed.
154+
155+
Both GCC and Clang built the helper/assertion profile cleanly with
156+
`-Wall -Werror`. Clang ASan and UBSan each passed a strict helper build, the
157+
focused fixture 20/20, the heavy concurrent stress 10/10, and a mixed
158+
length/JIT-read/weak-finalizer/remote-GC run. The normal default build was then
159+
restored. This evidence is deliberately Linux/x64-only; the complete macOS and
160+
Windows pass remains deferred until b1.2.1 is otherwise release-ready.
112161

113162
## Remaining work
114163

@@ -123,5 +172,12 @@ mechanical proof that no ordinary length caller reaches a peer-dependent wait.
123172
For structural mutation, the next larger table step remains a persistent,
124173
helpable new-key publication descriptor covering collision-chain `KEYLOCK`
125174
and `FINREG` states, followed by resize/range descriptors. Those designs should
126-
also provide the stable generation facts needed to replace the current
127-
active-MT helper cost with cheaper validated fast paths.
175+
also provide the stable old/successor facts needed for exact interpreter
176+
completion while a resize owner is paused.
177+
178+
A yield-and-redispatch loop is not such a mechanism. During resize the current
179+
vectors remain `RETIRING` until owner-only migration completes, and a forwarded
180+
source value can exist only in the paused owner's C local before successor
181+
installation. Exact completion therefore requires a published resize/migration
182+
descriptor with value-preserving per-slot intent, helpable successor
183+
accounting, and idempotent root publication.

src/lj_cdata.c

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,17 +210,14 @@ void LJ_FASTCALL lj_cdata_free(global_State *g, GCcdata *cd)
210210
}
211211
}
212212

213-
#define LJ_CDATA_FINCLAIM_U64 \
214-
((((uint64_t)LJ_TLIGHTUD) << 47) | (((uint64_t)1 << 47) - 1u))
215-
216213
static void cdata_fin_setclaim(TValue *tv)
217214
{
218215
tv_rawstore(tv, LJ_CDATA_FINCLAIM_U64);
219216
}
220217

221218
int lj_cdata_fin_isclaim(cTValue *tv)
222219
{
223-
return tv_rawload(tv) == LJ_CDATA_FINCLAIM_U64;
220+
return lj_cdata_fin_isclaim_inline(tv);
224221
}
225222

226223
#if defined(LJ_CDATA_TEST_HELPERS)

src/lj_cdata.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,15 @@ LJ_FUNC int lj_cdata_fin_claim_held(CTypeFinLease *lease, cTValue *key,
125125
LJ_FUNC int lj_cdata_fin_store_claim_held(CTypeFinLease *lease,
126126
cTValue *key, cTValue *src);
127127
LJ_FUNC int lj_cdata_fin_isclaim(cTValue *tv);
128+
/* FINCLAIM is checked by every shared table snapshot, including generated
129+
** numeric reads. Keep the raw-tag test inline for those hot readers while the
130+
** external helper remains available to cold translation units. */
131+
#define LJ_CDATA_FINCLAIM_U64 \
132+
((((uint64_t)LJ_TLIGHTUD) << 47) | (((uint64_t)1 << 47) - 1u))
133+
static LJ_AINLINE int lj_cdata_fin_isclaim_inline(cTValue *tv)
134+
{
135+
return tv_rawload(tv) == LJ_CDATA_FINCLAIM_U64;
136+
}
128137

129138
#if defined(LJ_CDATA_TEST_HELPERS)
130139
enum {

src/lj_ircall.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ typedef struct CCallInfo {
210210
_(ANY, lj_tab_storetv_forjit_newref, 5, S, PGC, CCI_L|CCI_T) \
211211
_(ANY, lj_tab_storetv_existing_forjit, 4, S, INT, CCI_L|CCI_T) \
212212
_(ANY, lj_tab_len_rooted_try, 2, S, INT, CCI_L) \
213+
_(ANY, lj_tab_len_forjit_try, 2, S, INT, CCI_L) \
213214
_(ANY, lj_tab_keyindex, 2, FL, INT, 0) \
214215
_(ANY, lj_vm_next, 2, FL, PTR, 0) \
215216
_(ANY, lj_tab_len, 1, FL, INT, 0) \

src/lj_record.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2950,7 +2950,11 @@ TRef lj_record_tab_len(jit_State *J, TRef tab)
29502950
{
29512951
if (lj_record_mt_shared_tab(J, tab)) {
29522952
TRef tabroot = rec_tmpref_mode(J, tab, IRTMPREF_IN1);
2953+
#if LJ_HAS_X64_MT_JIT_HELPERS
2954+
TRef len = lj_ir_call(J, IRCALL_lj_tab_len_forjit_try, tabroot);
2955+
#else
29532956
TRef len = lj_ir_call(J, IRCALL_lj_tab_len_rooted_try, tabroot);
2957+
#endif
29542958
emitir(IRTGI(IR_NE), len, lj_ir_kint(J, LJ_TAB_LEN_RETRY));
29552959
return len;
29562960
}

0 commit comments

Comments
 (0)