|
| 1 | +# Pre-grace typed semantic destruction |
| 2 | + |
| 3 | +## Status and scope |
| 4 | + |
| 5 | +This tranche lets the main-TG SWEEP owner perform semantic destruction of an |
| 6 | +unmarked, rootless, typed small-arena allocation before the arena-wide physical |
| 7 | +reuse grace. It is deliberately restricted to the three fixed-layout kinds |
| 8 | +whose immutable arena identity is already published: |
| 9 | + |
| 10 | +- `LFUNC0`, |
| 11 | +- `LFUNC1`, and |
| 12 | +- `CLOSED_UV`. |
| 13 | + |
| 14 | +The semantic destructor releases the closure or closed-upvalue resources and |
| 15 | +subtracts its GC charge immediately. The allocation boundary, READY bit, |
| 16 | +destructor kind, and body bytes remain physically present until the normal |
| 17 | +arena quarantine grace completes. No cell is reused early. |
| 18 | + |
| 19 | +This removes the need to recognize a special closure/upvalue pair. Every start |
| 20 | +is admitted, validated, destroyed, or retained independently from its immutable |
| 21 | +kind and exact extent. In particular, an adjacent closed-upvalue disagreement |
| 22 | +does not veto destruction of an independently valid `LFUNC1`, and no C-call or |
| 23 | +FFI-shape matcher is involved. |
| 24 | + |
| 25 | +## Why the ordinary retirement path was not enough |
| 26 | + |
| 27 | +The earlier sidecar path classified each dead typed start as `WHITE -> |
| 28 | +RETIRED`, incremented `reclaim_deferred`, requested a full arena grace, and ran |
| 29 | +the semantic destructor only after grace. That is the general safe fallback, |
| 30 | +but closure-heavy active-GC workloads repeatedly pay its per-object retirement, |
| 31 | +deferred-accounting, and post-grace finish costs. |
| 32 | + |
| 33 | +A typed object may be destroyed earlier only when the collector can prove both |
| 34 | +that no semantic mark producer was omitted and that its body cannot be mutated |
| 35 | +or reclaimed while it is inspected. Merely observing a global exclusivity bit, |
| 36 | +an empty queue, or a sealed arena is not such a capability. The new path |
| 37 | +therefore combines a locally acquired global gate, an SMR read lease, an exact |
| 38 | +arena seal, current-cycle root/fixpoint evidence, and a per-object lifetime |
| 39 | +claim. Any failed predicate falls back to the existing retirement path. |
| 40 | + |
| 41 | +## Exclusive admission transaction |
| 42 | + |
| 43 | +`lj_gc_sweep_gc2_arena_unmarked()` keeps its original two-argument interface |
| 44 | +and non-exclusive behavior. Main-TG sweep-owner progress calls the new internal |
| 45 | +`lj_gc_sweep_gc2_arena_unmarked_exclusive()` entry point. Secondary TGs continue |
| 46 | +to use the original path. |
| 47 | + |
| 48 | +The exclusive wrapper attempts the following capability: |
| 49 | + |
| 50 | +1. locally tries to acquire `mt_gc_exclusive`; |
| 51 | +2. after that succeeds, tries to enter one outer SMR reader; |
| 52 | +3. flushes pending roots and repairs the ownership spine; |
| 53 | +4. verifies the SWEEP phase, bridge/root snapshot, activation state, main-TG |
| 54 | + identity, internal arena allocator, zero mutator entry/live/active counts, |
| 55 | + configured/active worker state, closed JIT gate, idle recorder, empty |
| 56 | + recovery/assist/weak/SSB/grey/thread/table/finalizer work, and the exact arena |
| 57 | + owner/flags; |
| 58 | +5. locally seals the target arena; |
| 59 | +6. clears only the completed snapshot generation's count-zero `PENDING` bit; |
| 60 | +7. rechecks the complete certificate and exact remote state `CLOSED|SEALED`; |
| 61 | +8. holds that seal across the bounded arena scan; and |
| 62 | +9. unseals with `keep_pending=1`, leaves SMR, and releases the global gate on |
| 63 | + every exit. |
| 64 | + |
| 65 | +Losing either global-gate or SMR admission disables only early body access. It |
| 66 | +still runs the original sidecar-only classifier before owner progress moves the |
| 67 | +arena from NEEDSWEEP to quarantine. In particular, an SMR conflict cannot skip |
| 68 | +classification and accidentally quarantine a still-WHITE dead start. |
| 69 | + |
| 70 | +The completed SWEEP root snapshot naturally leaves a conservative |
| 71 | +`CLOSED|PENDING` arena state. Clearing it is sound only inside the complete |
| 72 | +certificate above. A new publisher first installs its count/`PENDING` intent; |
| 73 | +it therefore defeats either the clear/recheck or a later exact per-object |
| 74 | +commit check. The path never waits for a producer and never treats a pre-held |
| 75 | +`mt_gc_exclusive` value as locally owned authority. |
| 76 | + |
| 77 | +## Claim, validate, commit |
| 78 | + |
| 79 | +No target header or body byte is read while its lifetime is merely `LIVE`. |
| 80 | +Admission for one supported start is: |
| 81 | + |
| 82 | +1. derive the exact extent from the immutable sidecar kind; |
| 83 | +2. check only global state and arena side planes, expecting `LIVE`, `WHITE`, an |
| 84 | + unmarked exact start, READY, root `NONE`, recovery `IDLE`, no late bit, and |
| 85 | + exact `CLOSED|SEALED` remote state; |
| 86 | +3. atomically claim `LIVE -> DESTRUCT`; |
| 87 | +4. execute the sequentially consistent fence paired with rescue admission; |
| 88 | +5. repeat the complete global, arena, and exact-start predicate, now expecting |
| 89 | + `DESTRUCT`; |
| 90 | +6. validate the function/upvalue header and exact layout under that body lease; |
| 91 | +7. execute the final fence and repeat the complete predicate; |
| 92 | +8. atomically commit `DESTRUCT -> FREE`, then `WHITE -> FREEING`; and |
| 93 | +9. dispatch directly from the immutable kind to `lj_func_free()` or |
| 94 | + `lj_func_freeuv()` exactly once. |
| 95 | + |
| 96 | +Recovery may cancel the tentative claim with `DESTRUCT -> RESCUE`. A lost claim |
| 97 | +or commit accepts `RESCUE` and a recovery-restored `LIVE` as ordinary outcomes; |
| 98 | +recovery owns the durable work publication and restoration. Rollback never |
| 99 | +steals that ownership. Unexpected lifetime loss or failure of the exact WHITE |
| 100 | +commit is a release-build fail-stop, not an assertion-only continuation. |
| 101 | + |
| 102 | +The SC lifetime handshake prevents both a body reader and a racing rescue |
| 103 | +publisher from missing one another. The arena seal alone is not used as a body |
| 104 | +lease. Once `DESTRUCT -> FREE` succeeds, no competing semantic destructor can |
| 105 | +acquire the object, and the direct typed call performs accounting once. An |
| 106 | +observed exact `FREE` plus `FREEING` outcome is terminal ownership, not a reason |
| 107 | +to dispatch again. |
| 108 | + |
| 109 | +## Accounting and physical completion |
| 110 | + |
| 111 | +Pre-grace dispatch calls the normal semantic destructor with the lifetime |
| 112 | +already committed to `FREE`. Deferred arena free therefore: |
| 113 | + |
| 114 | +- subtracts the exact `sizeLfunc(0)`, `sizeLfunc(1)`, or `sizeof(GCupval)` |
| 115 | + charge once; |
| 116 | +- preserves the body, allocation boundary, READY bit, and destructor identity; |
| 117 | + and |
| 118 | +- creates no `RETIRED`/`reclaim_deferred` ticket. |
| 119 | + |
| 120 | +The sweep still requests an arena grace. Post-grace quarantine recognizes the |
| 121 | +already-owned `FREE|FREEING` start, removes its physical discovery state and |
| 122 | +kind, and does not charge or dispatch it again. |
| 123 | + |
| 124 | +If any global, arena, sidecar, body, cdata, permanent-retention, or lifetime |
| 125 | +predicate disagrees, the unchanged fail-closed path classifies the start as |
| 126 | +`RETIRED`, increments the deferred ticket, and validates/destructs it after |
| 127 | +grace. A concurrent semantic mark rescues `RETIRED -> LIVE` with the existing |
| 128 | +accounting repair. Unsupported kinds are never inspected pre-grace. |
| 129 | + |
| 130 | +## Compatibility boundary |
| 131 | + |
| 132 | +There is no public LuaJIT API or ABI change and no new bytecode. The added |
| 133 | +exclusive entry point is internal. The original arena-scan function retains |
| 134 | +its historical zero return contract. |
| 135 | + |
| 136 | +The separately documented temporary custom-`lua_Alloc` policy is unchanged: |
| 137 | +`lua_newstate()` currently uses the internal arena allocator and |
| 138 | +`lua_setallocf()` remains a no-op. Restoring arbitrary allocator callbacks is |
| 139 | +still required after the beta boundary described in |
| 140 | +`lua-alloc-temporarily-disabled-2026-07-10.md`; this optimization neither |
| 141 | +claims nor weakens that future requirement. |
| 142 | + |
| 143 | +## Validation |
| 144 | + |
| 145 | +Focused coverage constructs real rootless `LFUNC0`, `LFUNC1`, and adjacent |
| 146 | +closed-upvalue allocations, then verifies: |
| 147 | + |
| 148 | +- exact pre-grace semantic accounting and `FREE|FREEING` ownership; |
| 149 | +- unchanged body bytes, block boundaries, READY bits, kinds, and root `NONE` |
| 150 | + before grace; |
| 151 | +- removal of physical discovery metadata after grace with no second charge; |
| 152 | +- independent closure destruction when the adjacent upvalue has an injected |
| 153 | + cdata disagreement, followed by only that upvalue's post-grace destruction; |
| 154 | + and |
| 155 | +- ordinary retirement when the global exclusive bit was pre-held instead of |
| 156 | + locally acquired or when the locally acquired global gate then lost SMR |
| 157 | + admission. |
| 158 | + |
| 159 | +After the claim-before-body implementation, the forced-clean focused fixture |
| 160 | +passed once and that exact frozen binary passed 20/20. After the later |
| 161 | +release-build fail-stop and SMR-loss fallback hardening, a clean default rebuild |
| 162 | +and the expanded forced-clean focused fixture passed in 22.98 seconds. The |
| 163 | +following post-claim matrix also passed first try: |
| 164 | + |
| 165 | +| Target | Result | Time | |
| 166 | +| --- | --- | ---: | |
| 167 | +| `m2_arena_sweep` | PASS | 1.47 s | |
| 168 | +| `m6_jit_fnew_bump` | PASS | 22.94 s | |
| 169 | +| `m3_gc2_recovery` | PASS (normal, paranoia, default restore) | 131.82 s | |
| 170 | +| `m3_gc2_paranoia` | PASS (all C fixtures, stock 509/509, no-JIT 387/387, restore) | 93.08 s | |
| 171 | +| `m6_jit_gc2_readiness` | PASS | 1.29 s | |
| 172 | +| `m3_gc2_worker_scheduler` | PASS | 42.72 s | |
| 173 | + |
| 174 | +The known nonfatal GCC `gc2_root_rescan_later`/`la_load32_acq` |
| 175 | +`-Wstringop-overflow` diagnostic remains unchanged. |
| 176 | + |
| 177 | +## Performance and follow-up |
| 178 | + |
| 179 | +Five independent `BENCH_SCALE=.1 closures_upval` processes were measured for |
| 180 | +each interpreter/mode after the final default rebuild. Each process retains the |
| 181 | +benchmark's own best-of-five inner timing: |
| 182 | + |
| 183 | +| Mode | Current ns/op (five runs; median) | Stock `/usr/bin/luajit` ns/op (five runs; median) | Ratio | |
| 184 | +| --- | --- | --- | ---: | |
| 185 | +| GC active | 487.01, 487.09, 489.12, 494.25, 488.80; **488.80** | 37.91, 37.60, 37.63, 37.45, 37.34; **37.60** | **13.00x** | |
| 186 | +| GC stopped | 102.83, 113.84, 105.72, 101.42, 94.52; **102.83** | 18.43, 20.68, 20.84, 20.93, 21.11; **20.84** | **4.93x** | |
| 187 | + |
| 188 | +The stopped measurements require an explicit wrapper. `BENCH_GC_MODE=stop` |
| 189 | +is ignored by the current benchmark (it recognizes only `generational` and |
| 190 | +`incremental`), while a simple `-e 'collectgarbage("stop")'` is undone by the |
| 191 | +benchmark's next explicit `collect`. The valid wrapper runs each real |
| 192 | +`collect`, immediately stops automatic GC again, and was probed with |
| 193 | +`collectgarbage("isrunning") == false` before sampling: |
| 194 | + |
| 195 | +```sh |
| 196 | +-e 'local real_collectgarbage=collectgarbage; collectgarbage=function(op,arg) local r=real_collectgarbage(op,arg); if op=="collect" then real_collectgarbage("stop") end; return r end; real_collectgarbage("stop")' |
| 197 | +``` |
| 198 | + |
| 199 | +The current active-minus-stopped delta is 385.97 ns/op; stock's is 16.76 |
| 200 | +ns/op. The active median is also 12.72% slower than the preceding rootless-kind |
| 201 | +checkpoint's valid 433.63 ns/op active measurement while stock remained stable. |
| 202 | +The repeated full predicate/claim work has therefore not yet amortized the |
| 203 | +retirement it removes. This tranche is a correctness and ownership prerequisite, |
| 204 | +not a claim that the b1.2.0 performance gate has passed; both base closure |
| 205 | +allocation and active collector work remain release blockers. |
| 206 | + |
| 207 | +The next likely closure-churn work is to reduce post-grace per-start scanning |
| 208 | +and then restore active-MARK inline FNEW only under an explicit current-cycle |
| 209 | +proto/environment/upvalue traversal certificate. Relaxing the mark lease or |
| 210 | +blindly raising collector thresholds would trade away the safety established |
| 211 | +here and is not an acceptable optimization. |
0 commit comments