ReviewOS

zig-utils/zig-js

Perf: fit ordinary objects in the 128-byte GC slab

#131
Closed chris opened this 9 days ago ·
9 days ago

Related: #97, zig-utils/zig-gc#33. Continues the cold-state split from #123.

Result

Object is now exactly 96 payload bytes / 128 raw GC bytes and occupies the 128-byte slab class. ObjectColdState remains exactly 224 payload / 256 raw bytes. One atomically published ObjectStorageState owns optional cold/exotic state, external named-slot metadata, dense/internal element metadata, and backing-allocator bookkeeping. The direct one-byte backing lock serializes first installation and cross-category backing activation.

A real three-property churn literal is executable-tested to have storageState() == null, as well as no cold, slot, or element state. Four named values remain inline; only the fifth property allocates external slot state. Mutable [[AlreadyResolved]], array-storage stress state, and atomically accessed prototype state remain unpacked.

The accepted full matrix is published at:

The seven-sample exact-parent A/B compares candidate 12aa217c with parent 41bfd83c. Every direct and independent-context object-churn row improves by 1.04–1.65x. The independent 11-sample shared confirmation improves absolute one/four/eight-lane time by 1.19x, 1.39x, and 1.16x respectively. Four-lane shared scaling improves from 0.89x to 1.04x; eight-lane scaling is effectively stable relative to dispersion (0.84x parent, 0.82x candidate) while absolute time improves 16%.

The clean post-change JSC matrix at 12aa217c retains all 30 comparable headline wins. Object-churn medians are 101.799 vs 123.020 ms direct, 168.520 vs 185.412 ms at eight warmed contexts, and 181.819 vs 190.980 ms at eight cold lifecycles. Shared eight-lane time is 1,671.114 ms with 0.70x scaling. Cross-session comparison with the previous published matrix is descriptive only; the exact-parent A/B is the causal evidence.

Landed milestones

  • 2d4758dd: move rare property attributes into cold state (208 -> 200 payload bytes).
  • fe8a2a83: move mixed key order into cold state (200 -> 192).
  • 7b3a0953: move accessors into cold state (192 -> 184).
  • 045eea7b: move restriction ownership into cold state (184 -> 176).
  • 952f39e1: pack ten creation-time behavior tags without packing mutable state (176 -> 168).
  • dd507d56: move the sparse-only logical Array length floor into cold state while dense arrays stay sidecar-free (168 -> 160).
  • 14cbce1d: centralize all element-slice consumers behind the Object representation boundary.
  • 0c8d9a17: move the element list into atomically published lazy side state (160 -> 144).
  • b94da8e7: centralize named-slot consumers behind the Object representation boundary.
  • 41bfd83c: derive inline slot length from shape and move external slot metadata into atomically published lazy state (144 -> 128).
  • 0b82d1c8: merge optional storage and backing bookkeeping behind one lazy wrapper (128 -> 96 payload; raw allocation 160 -> 128).
  • 12aa217c: prove the real three-property churn literal allocates no storage wrapper.
  • 78a3fc5c: publish the accepted matrix, raw evidence, A/B note, README, and benchmark guide.

Validation

  • exact layout proof: @sizeOf(Object) == 96 and Heap.cellAllocationBytes(Object) == 128;
  • inline/external slot migration, every-allocation-failure rollback, GC graph tracing/finalization, delete/rebuild, and real churn-literal wrapper absence;
  • full 775-test execution: all 773 production/semantic tests passed with zero leaks; the two fixed-count pressure expectations were recalibrated to preserve equivalent allocation-byte pressure after the intended cell-size halving and then passed;
  • TSan passes simultaneous slot/element first installation and real GC-backed contended shared-array/shared-object growth;
  • exact-parent A/B and the complete fair JSC comparison matrix pass;
  • documentation site builds successfully.

Backward layout compatibility is explicitly not required before release.

Acceptance criteria

  • Compile-time test proves Heap.cellAllocationBytes(Object) <= 128 and a three-property plain literal allocates no sidecar.
  • Object/array/function/exotic semantics and exact backing-store finalization are covered with no leak increase.
  • Focused normal and TSan object/parallel-GC gates pass, followed by the batched full suite and targeted byte-pressure recalibration.
  • Exact-parent comparison A/B does not regress any directly compared object-churn row, materially improves shared 4/8-lane absolute time, and does not materially regress shared scaling relative to dispersion.
  • One fair full matrix preserves every comparable JSC win; raw samples/report and README/docs contain only its accepted scores.

Remaining shared-realm object-churn scaling work stays in #97; it is no longer an object-layout acceptance blocker.

9 days ago

First independently safe slice shipped in 2d4758dd (perf(object): move attributes into cold state (#131)).

  • moved the atomically published property-attribute map from every Object into its existing lazy cold sidecar;
  • plain literals still allocate no sidecar;
  • reduced Object payload from 208 to 200 bytes (raw collector allocation 240 to 232 bytes; still in the 256-byte class);
  • updated the compile-time size proof plus README/benchmark architecture text without changing the accepted benchmark scores;
  • passed the size test, property-descriptor semantics, GC backing-release test, and no-GIL TSan semantics/frozen-seal-race.js.

The next slices need to move enough unrelated cold groups to cross the actual 128-byte class boundary before running another expensive comparison matrix.

9 days ago

Second cold-state slice shipped in fe8a2a83 (perf(object): move key order into cold state (#131)).

  • moved the atomically published mixed data/accessor key-order list off every object;
  • reduced Object payload from 200 to 192 bytes (224 raw collector bytes, still in the 256-byte slab);
  • preserved shape-only plain literals without a cold sidecar;
  • kept allocation-failure rollback leak-free after the sidecar becomes a new possible allocation stage;
  • passed size, exhaustive key-order OOM, fixed-shape object quickening (4/4 including shared workers), and no-GIL TSan frozen/seal race gates.

README and benchmark docs now describe the 192/224-byte layout while retaining the latest accepted benchmark scores.

9 days ago

Third cold-state slice shipped in 7b3a0953 (perf(object): move accessors into cold state (#131)).

  • moved the accessor-map pointer behind the existing lazy cold sidecar and replaced raw fast-path probes with one atomic helper;
  • reduced Object payload from 192 to 184 bytes (216 raw collector bytes, still in the 256-byte slab);
  • retained exact concurrent tracing/finalization of getter/setter edges and no sidecar on data-only plain literals;
  • passed size, real GC Object-graph tracing, parallel-GC accessor backing release, fixed-shape quickening 4/4, and no-GIL TSan shared-objects/getters-setters.js.

README and benchmark docs describe the 184/216-byte layout without replacing the latest accepted benchmark matrix.

9 days ago

Fourth cold-state slice shipped in 045eea7b (perf(object): move restriction owner into cold state (#131)).

  • moved the opt-in atomic Thread.restrict owner id out of every unrestricted object;
  • reduced Object payload from 184 to 176 bytes (208 raw collector bytes, still in the 256-byte slab);
  • retained the zero-sidecar fast path for unrestricted objects and an exact CAS for competing restriction claims;
  • passed the size proof, both focused restriction unit tests (3/3 with root), and no-GIL TSan api/thread-restrict.js.

README and benchmark docs describe the 176/208-byte layout while retaining the latest accepted benchmark scores.

9 days ago

Landed 952f39e (perf(object): pack rare behavior flags (#131)). Ten creation-time behavior tags now share a u16; mutable promise AlreadyResolved and array-storage stress state deliberately remain direct fields so packed read-modify-write cannot create adjacent-bit races. Object payload drops 176 -> 168 bytes and raw GC allocation drops 208 -> 200 bytes, remaining in the 256-byte slab. README.md and docs/benchmarks.md document the new exact layout while retaining the latest accepted benchmark matrix. Validation: 773/773 unit tests, focused layout/Promise/shell/concurrent-GC gates, and vmstate/regexp-churn-threads.js 1/1 under no-GIL TSan.

9 days ago

Landed dd507d5 (perf(object): move sparse array length into cold state (#131)). The logical Array length floor now uses an exact u32 in ObjectColdState only when it exceeds dense element count; packed dense arrays derive length from elements and allocate no cold sidecar. Object payload drops 168 -> 160 bytes and raw GC allocation 200 -> 192, while ObjectColdState remains exactly 224 payload / 256 raw. Added guards that a real three-property churn literal stays sidecar-free and both slab classes remain fixed. README.md and docs/benchmarks.md carry the new exact layout without changing accepted scores. Validation: 773/773 unit tests; array-length, structured-clone wire, and sidecar-focused checks; arrays/push-resize-multithread.js 1/1 under no-GIL TSan (132489 ms).

9 days ago

Landed 14cbce1 (refactor(object): centralize element slice access (#131)). All 121 direct element-slice consumers in GC tracing, structured clone, interpreter, VM, and Object helpers now flow through Object.elementsItems(). This is layout-neutral and compile-verified; it isolates the next lazy element-storage representation change behind one slice view while retaining existing element-lock discipline and the 160-byte Object / 224-byte cold-state proofs.

9 days ago

Landed 0c8d9a1 (perf(object): move elements into lazy side state (#131)). The 24-byte inline ArrayListUnmanaged is now an 8-byte atomically published ObjectElementsState pointer, allocated only for arrays and internal tuples that actually store elements. Existing element-lock semantics remain direct and unchanged. Object payload drops 160 -> 144 bytes and raw GC allocation 192 -> 176; ObjectColdState remains 224 / 256 raw. The real three-property churn literal is explicitly proven to have neither cold nor element side state. Finalization tracks the state and its buffer independently for exact OOM rollback and once-only release. README.md and docs/benchmarks.md carry the exact layout without changing accepted scores. Validation: 773/773 unit tests, focused layout/finalization/heap-cap/array/Map/Set gates, and arrays/push-resize-multithread.js 1/1 under no-GIL TSan (134340 ms), including concurrent first installation on an empty shared array.

9 days ago

Pushed the next two small main-branch slices: b94da8e centralizes every runtime named-slot read/write behind Object.slotsItems(), and 41bfd83 moves external slot-list metadata into atomically published lazy state. Four-or-fewer named properties now derive their live slice from immutable shape metadata and remain entirely inline; only the fifth property allocates the side state. This reduces Object from 144 payload / 176 raw bytes to 128 payload / 160 raw bytes. Focused layout, migration/OOM rollback, GC graph/finalization, delete/rebuild, real three-property churn sidecar absence, and TSan concurrent fifth-property publication gates pass. README/docs report the exact 128/160 layout while retaining the latest accepted benchmark scores; this still has not crossed the raw 128-byte slab boundary.

9 days ago

Pushed the slab-crossing milestone in 0b82d1c and the explicit churn-literal proof in 12aa217. Object is now exactly 96 payload / 128 raw bytes, so ordinary cells use the 128-byte GC slab. One lazy wrapper replaces the three side-state pointers plus direct allocator/flag bookkeeping; four named values remain inline, and the real three-property churn literal has no wrapper. Validation: all 775 tests executed, with 773 semantic/production passes and zero leaks; the only two failures were fixed-count byte-pressure expectations made stale by the intended cell halving, and both pass after equivalent-byte recalibration. TSan passes simultaneous slot/element first installation plus real GC-backed contended shared-array/shared-object growth. README/docs state the 96/128 layout and retain the latest accepted pre-crossing scores. Next gate is exact-parent object-churn A/B before publishing any new benchmark numbers.

9 days ago

Completed and published in 78a3fc5. The 128-byte ordinary-object slab is validated by the batched semantic/GC/TSan gates, an exact-parent A/B, and the full 1,540-sample JSC matrix. All 30 comparable headline rows remain zig-js wins. Remaining shared-realm scaling work is accurately carried by #97.

9 days ago

Follow-up regression hardening landed in #316 (4a391cd8): WebAssembly Instance trace metadata had widened ObjectColdState to 240/272 bytes. It now lives behind one arena pointer, restoring the intended 224/256-byte cold allocation class while keeping Object at 96/128.

Sign in to comment on this issue.