← All issues

Butterfly-less JSObject for Wasm GC

55783b9

Source/JavaScriptCore/runtime/JSObject.h

- WriteBarrier<Unknown> m_butterfly;
+ // m_butterfly moved to JSObjectWithButterfly
 
Butterfly* butterfly()
{
+ auto* b = *std::bit_cast<Butterfly**>(std::bit_cast<char*>(this) + butterflyOffset());
+ if (type() == WebAssemblyGCObjectType) [[unlikely]]
+ b = nullptr;
+ return b;
}

Source/JavaScriptCore/jit/AssemblyHelpers.cpp

- static_cast<int32_t>(sizeof(JSObject)) -
+ static_cast<int32_t>(JSObject::offsetOfInlineStorage()) -

Source/JavaScriptCore/llint/LowLevelInterpreter64.asm

- loadp JSObject::m_butterfly[objectAndStorage], objectAndStorage
+ loadp JSObjectWithButterfly::m_butterfly[objectAndStorage], objectAndStorage
- addp sizeof JSObject - (firstOutOfLineOffset - 2) * 8, objectAndStorage
+ addp sizeof JSObjectWithButterfly - (firstOutOfLineOffset - 2) * 8, objectAndStorage

In JSC, the butterfly is a heap-allocated sidecar structure hanging off JSObject that holds both out-of-line named properties (to the left of the pointer) and indexed storage (to the right). Its offset within JSObject is a load-bearing constant baked into JIT code, LLInt assembly macros, and B3 IR lowering. Wasm GC objects (structs and arrays defined via the GC proposal) have a fixed, typed layout with no JS properties, so their butterfly is structurally impossible and was always nullptr — pure waste of 8 bytes per object.

This commit splits JSObject into two classes: a butterfly-less JSObject base and a new JSObjectWithButterfly subclass that carries m_butterfly. Wasm GC objects inherit directly from JSObject. The butterfly() accessor compensates with a speculative load trick — it unconditionally reads from butterflyOffset() (always valid because HeapCell atoms are ≥16 bytes), then zeroes the result when type() == WebAssemblyGCObjectType, keeping the common path branchless. JIT-generated butterfly loads skip the type check entirely, relying on the invariant that Wasm GC objects can never satisfy the structure/indexing-type guards that precede any butterfly dereference.

Before:                                After:
  JSObject { m_butterfly, inline... }    JSObject { inline... }  ← no m_butterfly
    ├── JSNonFinalObject                   ├── JSObjectWithButterfly { m_butterfly }
    ├── JSFinalObject                      │     ├── JSNonFinalObject
    └── WebAssemblyGCObjectBase            │     └── JSFinalObject
          (m_butterfly always nullptr)     └── WebAssemblyGCObjectBase  ← 8 bytes saved

This restructures one of JSC's most fundamental object hierarchies — every offset calculation, JIT inline cache, LLInt macro, and B3 lowering that touches the butterfly pointer or inline storage had to be audited and updated, and any missed site is a silent memory corruption. The immediate payoff is reduced Wasm GC heap footprint.

🔒

The speculative butterfly load and the JIT bypass invariant both have edge cases in newly introduced code paths worth investigating.

Subscribe to read more