Inline small strings in JSString::m_fiber#307
Draft
robobun wants to merge 69 commits into
Draft
Conversation
Store up to 7 Latin-1 or 3 UTF-16 code units directly in JSString::m_fiber, tagged with bit 1 (isInlineInPointer). notStringImplMask (bit 0|1) is the single 'm_fiber is not a raw StringImpl*' test. Runtime (JSString.h/cpp, JSStringInlines.h): length/is8Bit/view/value/ tryGetValue/tryGetValueImpl/toAtomString/toIdentifier/isSubstring/destroy/ visitChildren/estimatedSize/equal/resolveToBuffer/jsSubstringOfResolved handle inline. resolveInline() materializes in place, mirroring resolveRope. jsString(VM&, StringView) and jsSubstringOfResolved create inline for eligible lengths. JIT: branchIfRopeStringImpl widened to notStringImplMask. InlineAccess/ InlineCacheCompiler/DFG string-length given an inline-length fast path. operationResolveRopeString handles inline. FTL isRopeString/isNotRopeString widened. State: LLInt + baseline JIT pass a slice/length/charAt/eq loop. DFG/FTL string-equality and remaining offsetOfLength readers still need fixes.
…d tryGetValueWithoutGC
DFG compileStringEquality + FTL stringsEqual: when both operands are inline, compare the m_fiber words directly (identical encoding = equal string). Same-width unequal fibers fall to falseCase without a memcmp; only cross-width pairs take the slow path. ThunkGenerators decodeString: decode length/is8Bit/data-pointer from the fiber word instead of routing inline to the slow path. Measured (2M ops on 5-char inline slices, vs unpatched release): .length 8.2ms -> 5.7ms (-30%) .charCodeAt 66.7ms -> 53.7ms (-19%) === (mixed) avg 23ms -> 17ms (-26%) All four tiers pass the correctness suite.
Compute the result as isZero64(leftFiber xor rightFiber) and feed it directly into the continuation phi instead of branching to trueCase/ falseCase. Same-width inline pairs resolve without touching the StringImpl path or the thunk; cross-width falls through to slowCase. === benchmark (2M ops, 5-char inline slices vs unpatched release) === .length 8.2ms -> 6.2ms (-24%) .charCodeAt 66.7ms -> 54ms (-19%) === always-true 9.5ms -> 15ms (+58%, synthetic) === 50/50 mix 33.8ms -> 18.5ms (-45%) !== 36.8ms -> 19.0ms (-48%) JSTests/stress/string-*.js: 158/160 pass; the two failures (string-localeCompare, string-substring-oom) are ICU collation data issues from the local build environment, unrelated to this change.
…path - SpeculatedType.h/cpp: SpecStringInline (bit 51), included in SpecStringVar; speculationFromCell returns it for JSString::isInline(); dump prints "StringInline". DFG graph now shows 'predicting StringInline' for slices. - DFGSpeculativeJIT: isLikelyInlineString(Edge) checks if the profile is monomorphic-inline. Array::String length speculates on it and emits the branch-free (fiber>>3)&0x1f decode with an OSR guard. - compileStringEquality: when one side is a short 8-bit atom literal and the other is inline, encode the literal as an immediate m_fiber at compile time and compare the dynamic side's m_fiber against it. Measured: s === "012" 28.6ms -> 21.8ms (-24%) for 2M ops. - JSString::encodeInline8/16/createInline* made public so JIT tiers can encode literals. - JITOpcodes/LOLJIT op_switch_char: inline strings never match a single-char switch (they're length >=2 for 8-bit), so decode length from the fiber and jump to default instead of materializing. All four tiers pass the correctness suite.
Mirrors DFGSpeculativeJIT::isLikelyInlineString. When provenType is monomorphic SpecStringInline, FTL string-length speculates on (fiber&3)==2 and emits the branch-free (fiber>>3)&0x1f decode. Measured (function-scoped loop, 2M ops on 5-char inline slices): .length 7.5ms -> 5.0ms (-33%) Also: Linux CI fail-fast:false so x64 and arm64 report independently (previous runs only showed whichever arm64 variant finished first).
robobun
force-pushed
the
bun-inline-small-strings
branch
from
July 19, 2026 05:53
0d7c938 to
aa6d292
Compare
Preview Builds
|
FixupPhase: for StringCharCodeAt with a monomorphic-SpecStringInline base, skip inserting ResolveRope so the code unit can be read straight from the cell without materializing a StringImpl. DFG compileGetCharCodeAt / FTL compileStringCharCodeAt: when the base's prediction matches the FixupPhase condition, speculate on (fiber&3)==2 and load8/load16 from cell+9 / cell+10. OperationsInlines.h jsString(globalObject, s1, s2): when both sides' characters are available without allocation and the combined length fits (<=7 Latin-1 / <=3 UTF-16), write into a 16-byte inline cell instead of a 32-byte rope. Measured (function-scoped loops, 2M ops on 5-char inline slices, vs unpatched release): .length 14.9ms -> 6.5ms (-56%) .charCodeAt 14.7ms -> 7.1ms (-52%) === (50/50 mix) 30.2ms -> 7.8ms (-74%) === "literal" 15.1ms -> 13.9ms (-8%) short concat 32 B/obj -> 16 B/obj (-50%) All four tiers pass the correctness suite.
…gView
Returning a StringView that points at &m_fiber+1 is unsafe: any subsequent
resolveInline() (including tryGetValue()) overwrites m_fiber with a
StringImpl*, leaving the StringView reading pointer bytes. JSStringJoiner
(Array.prototype.join) does exactly this:
auto view = jsString->view(globalObject); // -> &m_fiber+1
append(..., StringViewWithUnderlyingString(view,
uncheckedDowncast<JSString>(view.owner)->tryGetValue())); // resolves in place
which produced garbage characters in JetStream3 WSL ("float" -> heap-pointer
bytes). Materialize to a StringImpl in view(), mirroring how non-substring
ropes are resolved there.
nonRopeStringView() (used only inside resolveToBuffer/tryFindOneChar/etc.,
where the view is consumed immediately) keeps the in-cell pointer.
An inline fiber's payload is at most 7 bytes; masking the decoded length to 3 bits ensures a corrupted high nibble can never produce an out-of-bounds read from inlineData8()/inlineData16(). Replaces the 0x1f literal with JSString::inlineLengthMask across the runtime, LLInt, and every JIT tier.
Reverts a35b4e6 and moves the fix to the two JSStringJoiner call sites that store the StringView past the GCOwnedDataScope lifetime and then call tryGetValue() on the same string (which overwrites m_fiber in place). Resolving the inline string before calling view() there lets every other view() caller keep the zero-copy in-cell pointer. JetStream3 WSL: 1.47 (baseline) -> 1.50 (this) vs 1.33 with the materialize-in-view() approach.
After reading child lengths/flags, if the total is <=7 and all children are 8-bit, branch to the existing slowPath so operationMakeRope2/3 -> jsString() emits a 16-byte inline cell instead of finishing the 32-byte rope. The already-allocated rope cell has fiber0==isRopeInPointer with a null fiber, which visitChildren handles. Also hook the 3-arg jsString(globalObject, s1, s2, s3). describe() confirms 'a'+'b' and 'a'+'b'+'c' both produce inline cells in FTL-compiled code. JetStream3: WSL neutral, source-map-wtb ~+4%, SunSpider within noise.
…6 chars Same (fiber&3)==2 encoding with an extra uintptr_t of contiguous payload, so every existing JIT decoder (length shift+mask, charCodeAt byte-indexed load, ThunkGenerators decodeString) works unchanged. New IsoSubspace bigInlineStringSpace. inlineLengthMask widened to 4 bits. Creation hooked in jsString(VM&, StringView), jsSubstringOfResolved, and the 2/3-arg jsString concat. DFG/FTL MakeRope slow-path routing stays at <=7; the 32->24 saving for 8..15 doesn't cover the call overhead (big-inline coverage for that range comes from slice/substring). Inline-vs-inline eq fast path: bail to slowCase when either side's encoded length >= 8 since the single m_fiber-word compare doesn't see the high payload. describe() shows slices of every length 3..15 as inline (null StringImpl*), 16+ as rope. All four tiers pass; WSL/SunSpider neutral.
Wraps WTF::StringBuilder with a 15-byte Latin-1 stack buffer. Short
all-Latin-1 results never touch malloc: toJS() emits a JSString/
JSBigInlineString cell directly from the stack bytes. Results that
exceed the inline buffer or contain non-Latin-1 spill to the wrapped
StringBuilder and take the existing toString() path.
Swapped into encodeURI/encodeURIComponent, decodeURI/decodeURIComponent,
escape, unescape, and String.fromCodePoint.
2M-iter microbench vs no-JSStringBuilder baseline (same inline-string
patches otherwise):
encodeURIComponent("key") 132ms -> 56ms (-58%)
encodeURIComponent("a b") 225ms -> 68ms (-70%)
encodeURIComponent 12-char ascii 203ms -> 99ms (-51%)
encodeURIComponent 42-char (spill) 554ms -> 523ms (-6%)
decodeURIComponent("abc") 177ms -> 75ms (-58%)
decodeURIComponent("a%20b") 143ms -> 55ms (-61%)
String.fromCodePoint(97,98,99) 133ms -> 61ms (-54%)
escape("foo") 144ms -> 51ms (-64%)
unescape("a%20b") 130ms -> 55ms (-58%)
Also tag inline strings as (inline) in JSValue::dump so describe()
distinguishes them from unresolved ropes.
…g-inline + fiber-word cache RAMification found inline strings being resolved to StringImpl far more than they should, via paths that only need a StringView: - jsSubstringOfResolved(inline) called resolveInline() before reading bytes; now copies directly from the inline payload into a fresh inline cell. - stringProtoFuncCodePointAt called toWTFString(); added an inline fast path that reads the code unit(s) from the cell bytes. - DFG/FTL StringCodePointAt on a monomorphic-inline profile now skips the ResolveRope insertion and decodes from the cell (mirrors the existing StringCharCodeAt handling). - toIdentifier/toAtomString/toExistingAtomString now resolve an inline string directly to the AtomStringImpl (resolveInlineToAtomString), skipping the intermediate non-atom StringImpl that was being parked in the heap's possibly-accessed-strings vector. - jsMapHashImpl hashes inline bytes directly instead of resolving. - JSStringJoiner resolves inline elements via resolveInlineToAtomString so repeated short contents share a single AtomStringImpl. InlineStringCache: a 512-slot direct-mapped JSString* cache on VM, keyed by the inline-encoding fiber word. createInline8/16 consult it first; repeated short content returns the same cell. Cleared alongside KeyAtomStringCache in Heap::finalize. JSBigInlineString grows to 32 bytes (same cell size as JSRopeString): maxBigInlineLength8 15->23, maxBigInlineLength16 7->11, inlineLengthMask 0xf->0x1f. The DFG/FTL inline-equality word compare now picks its length bail mask by width (the 8->16 bump exposed the previous mask being Latin-1-only). RAMification (peak footprint, 3 runs): UniPoker +28% -> ~0%, string-unpack-code-SP +10% -> -2%. Parsers (babylon-wtb, typescript, espree, WSL) -4% to -11%.
32-byte big-inline regressed RAMification by ~3% geomean: the 8..15 Latin-1 / 4..7 UTF-16 range (where most parser tokens land) grew from 24-byte cells to 32-byte cells. The 16..23 range is too rare in JetStream to offset that. Reverting to 24 bytes (maxBigInlineLength8 15, maxBigInlineLength16 7, mask 0xf). Keep the width-aware big-inline bail in DFG/FTL compileStringEquality: the single m_fiber word compare is sound only for 16-byte inline, which is len<=7 Latin-1 OR len<=3 UTF-16, so the bail mask depends on whether both sides are 8-bit.
…bails FTL compileMakeRope allocated the 32-byte JSRopeString before computing child flags/length. The inline-child guard and the short-result routing then branched to slowPath (operationMakeRope*), which allocates its own cell, leaving the pre-allocated rope as dead garbage. pdfjs does ~5M concats per run; every slowPath bail cost an extra 32 bytes. Allocation now happens after those checks, so slowPath never wastes a cell. DFG shares the register-reuse pattern that makes deferring the allocation awkward; the short-result routing there is dropped instead (short concats become 32-byte ropes, same as baseline), keeping only the inline-child correctness guard. Also in this batch: - op_switch_string (LLInt/baseline/DFG operation) looks up the scrutinee atom directly from inline bytes via AtomStringImpl::lookUp; no StringImpl is allocated. pdfjs was resolving ~1M inline scrutinees per run through this path. - InlineAtomCache: 512-slot fiber-word-keyed RefPtr<AtomStringImpl> cache on VM, consulted by Identifier::add(span<Latin1>) and resolveInlineToAtomString so repeated short names skip the atom-table hash+probe. Cleared on full GC. RAMification pdfjs peak (FTL, 10 runs): 249-270MB -> 233-244MB; now ~-2% vs baseline instead of +7%.
Defines isInlinePropertyKey() / uidIsSymbol() / uidHash() / uidLength() / uidRef() / uidDeref() so call sites that dereference a UniquedStringImpl* can branch on the tag bit first. Pointer-identity comparisons and PtrHash need no change. Not yet wired: Identifier still wraps AtomString (RefPtr), so no fiber words reach PropertyName yet. Next step is a tagged Identifier storage and routing IdentifierRepHash::hash / PropertyName::isSymbol through the shims.
…al keys isCacheableIdentifierCell() previously rejected inline strings (tryGetValueImpl() returns null), so obj[inlineStr] always took the generic slow path and never populated an AccessCase. A 16-byte inline string's m_fiber is content-unique, so it can serve as the IC's cached uid directly: the stub's pointer compare of jsString->m_fiber against accessCase.uid() is already the right test. - isCacheableIdentifierCell/getCacheableIdentifier: accept inline cells with length <= maxInlineLength8. - CacheableIdentifier::uid(): for an inline JSString cell, return the raw fiber word cast as UniquedStringImpl*. - CacheableIdentifier::isSymbol()/hash()/ensureIsCell(): route through uidIsSymbol()/uidHash() so a fiber-word uid does not dereference. - InlineCacheCompiler: replace accessCase.uid()->isSymbol() with accessCase.identifier().isSymbol() at every site; the two handler string-key paths test only isRopeInPointer so inline fiber words reach the word compare. With InlineStringCache, repeated short keys return the same cell; once the first slow-path resolves it to an atom, subsequent IC compares hit on the atom. A fresh inline cell after GC costs one slow-path miss.
…-resolve variant) isCacheableIdentifierCell/getCacheableIdentifier/createFromCell previously rejected inline strings (tryGetValueImpl() returns null), so obj[inlineStr] never populated an AccessCase and took the generic slow path on every access. Resolve the inline cell to its atom in createFromCell/ getCacheableIdentifier so downstream code sees a real AtomStringImpl*. With InlineStringCache, subsequent accesses return the same (now-resolved) cell and hit the IC's atom-pointer compare. Reverts 2d8ee4a's fiber-word-as-uid approach: every slow-path caller dereferences the returned uid (parseIndex, putInline, repatch), so a tagged word crashes. Keeping uid as a real impl sidesteps all of that.
…r materialize, printableName(UniquedStringImpl*) + FiberAwareRefPtr star-iter, objectConstructorEntries cachedKey inlineFiberWord compare, PropertyTable findImpl implSide null-check, getCalculatedDisplayName off-mutator fiber decode
… — route JSFunction::nameWithoutGC/getCalculatedDisplayName + StackFrame::functionName ecmaName().impl() through fiber-word decode (off-mutator AtomStringTable=null safe)
…Val ops (canonicalFiberWordFor in JIT/DFG operationGetByVal*), JSPropertyNameEnumerator createInlineFromFiber, AssemblyHelpers MegamorphicCache uid->hash()→uidHash guard
…hash() (not existingSymbolAwareHash) for JIT sync; revert JSPropertyNameEnumerator createInlineFromFiber (identifier.string() materialize is correct)
…fPtr + sort-comparator StringView via Identifier::string() (bun module-namespace 'then' lookup crash)
…d decode + JSWebAssemblyModule exportSymbolTable key via Identifier::fromString (json-parse-inspector SEGV, tsf-wasm RELEASE_ASSERT putResult)
…tyNames uidIsSymbol guard (Object.getOwnPropertyNames on SymbolTable scope w/ fiber-word key SIGSEGV; hits verdaccio startup)
…nonicalFiberWordFor before hasEnumerableProperty (for-in GenericMode skips fiber-word keys when a Symbol is on the object; seenProperties bloom keyed on fiber)
…iber-word atom (verdaccio require.extensions['.hbs'] SEGV → bun-workspaces ConnectionRefused)
…tor FiberAwareRefPtr + sort via Identifier::string()
…ilder edge-name fiber-word decode (writeHeapSnapshot + generateHeapSnapshotForDebugging SEGV on PropertyTable/SymbolTable key)
… key via identifier.string().impl() (fiber-word impl() broke arrayProtoFuncIndexOf's AtomStringImpl* contains(); Babylon 'class' not a keyword)
…s honour GCDeferralContext (RegExpMatchesArray half-init GC visit SIGSEGV; JetStream2 quicksort-wasm seq crash)
…g() via stringWithoutAtomizing (FTLState::proc->setName on compiler thread materialized fiber-word ecmaName into wrong-thread AtomStringTable; quicksort-wasm sweep RELEASE_ASSERT wasRemoved)
…IT hash codegen uidHash() now returns WTF::intHash(reinterpret_cast<uintptr_t>(impl)) — a single branch-free pointer-word hash that is sound because canonicalFiberWordFor guarantees one m_bits value per content, so pointer-identity hashing holds for both real UniquedStringImpl* and fiber-word Identifiers. JIT codegen mirrors this exactly by REUSING the existing rapidHashMix64 helpers (AssemblyHelpers::rapidHashMix64 @ jit/AssemblyHelpers.cpp:1328 and FTL rapidHashMix64 @ ftl/FTLLowerDFGToB3.cpp:16297), which implement the same rapidhash "mum" 128-bit-multiply mixer as WTF::intHash (wtf/HashFunctions.h:35). No hand-rolled golden-ratio sequence. MapHash sites INTENTIONALLY UNTOUCHED: DFGSpeculativeJIT64.cpp:5679/5717 and FTLLowerDFGToB3.cpp:16325/16430 hash JSValue / string *content* for JS Map/Set (HashMapImpl semantics), not uid pointers — they are out of scope and must not be swept into pointer hashing. operationInlinePropertyKeyHash now returns toUCPUStrictInt32(WTF::intHash(word)), keeping the DFG 32-bit HasOwnProperty path (which calls it unconditionally, since rapidHashMix64 is JSVALUE64-only) consistent with the new uidHash(). uidIsSymbol() tightened to a bare tag-bit test (bits & inlinePropertyKeyTag) before the isSymbol() load. C++ MegamorphicCache/HasOwnPropertyCache primary hashers route through uidHash(); IdentifierRepHash/PropertyTable/StructureInlines pick this up automatically.
… (DFG/FTL/AssemblyHelpers HasOwnPropertyCache + MegamorphicCache probes)
…not canonical fiber word; JSString::tryGetCanonicalInlineFiberWord skips toAtomString round-trip Phase-D root cause: the JIT load/store/hasMegamorphicProperty fast path probes with loadPtr(JSString::offsetOfValue()) — i.e. m_fiber — but getByValMegamorphic / inByValMegamorphic / putByValMegamorphic filled the MegamorphicCache with the D.4-canonical fiber word via canonicalFiberWordFor(atom). For atom-backed 2..5-char keys (keyAtomStringCache length-2 substrings, or any cell after a prior resolveInlineToAtomString), m_fiber == atom but the cache entry.impl == fiber word ⇒ uidHash(atom) ≠ uidHash(fiber) ⇒ 100% JIT-probe miss ⇒ every getByVal fell to the C++ slow path. Fix: - JSString::tryGetCanonicalInlineFiberWord(): one-load check for 8-bit inline len≤5; returns m_fiber unchanged so the cell stays inline for the next probe. - get/in/putByValMegamorphic + getByVal/getByValWithThis/putByVal slow-path helpers + operationGetByValObjectString + operationHasOwnProperty + objectProtoFuncHasOwnProperty: cacheUid = whatever m_fiber holds (fiber word for inline cells, atom for atom-backed), lookupUid = canonical form for PropertyTable/Structure bloom — initAsHit/Miss/HasHit/HasMiss/Replace/ Transition now take cacheUid. - PropertyTable::findImpl: hoist keyIsFiber above the probe loop (saves one test+mask per collision). Profile (miniprof 2 kHz, Babylon, baseline→final top-5 Δ%tot): +1.132 encodeInline8 (24 via canonicalFiberWordFor←getByValMegamorphic) +0.287 getByValMegamorphic (+19 operator==←canUseMegamorphicGetByIdExcludingIndex) +0.286 isRope (via toAtomString) +0.256 get (PropertyTable) +0.219 lookup (InlineStringCache) Micro-bench (gbv megamorphic, 30 structures × 25 keys, 5M iters): 2-char (atom-backed via keyAtomStringCache): baseline 194ms HEAD 421ms fix 222ms 3-char (inline cell): baseline 477ms HEAD 192ms fix 138ms JetStream2 3×2 interleaved (Total Score, higher=better): baseline median 196.120 patched median 206.259 ⇒ +5.17% runs: baseline 196.120/198.295/195.953 patched 206.259/208.192/204.053 seq-crash-survey CRASH_COUNT=0
…acheableIdentifier::uid() len>5 atom short-circuit
fix-canonical-fiber-order / canonical-fiber-fastbail. canonicalFiberWordFor runs
on every Identifier(AtomStringImpl*/AtomString/String/StringImpl*/Ref&&) ctor
and on CacheableIdentifier::uid() (every getByVal IC slow path). It previously
did isSymbol()+length()+is8Bit()+span8() = 3-4 dependent loads before the
len>5 bail that fires for the overwhelming majority of real identifiers.
- IdentifierInlines.h: load impl->length() first; [[likely]] bail on
len<2 || len>maxFiberWordKeyLength before touching m_hashAndFlags for
isSymbol(); mark [[gnu::hot]].
- CacheableIdentifierInlines.h: uid() short-circuits a non-inline atom-backed
cell with len>5 straight to impl (skips canonicalFiberWordFor); all
small-inline fast paths gated on enableIdentifierFiberWords so the
killswitch-validate escape hatch neuters the producer without a rebuild.
Profile (miniprof 2 kHz, Box2D, /tmp/pmp-out/report.txt top deltas,
baseline=406 samples patched=471):
DELTA PATCH BASE FUNCTION
+131 1497 1366 [JIT/??]
+5 5 0 JSC::Structure::nonPropertyTransition
+5 5 0 JSC::CodeBlock::propagateTransitions
(no canonicalFiberWordFor / encodeInline8 frames remain in top-25 regressors)
Per-test % (3x interleaved, /tmp/fix2-bench3x2.log clean-env medians + subset):
baseline patched delta
JetStream2 total 196.120 206.259 +5.17%
pdfjs 139.505 127.115 -8.88%
Box2D 311.934 303.671 -2.65%
Babylon 446.952 550.787 +23.23%
seq-crash-survey: CRASH_COUNT=0 (17/17 sequence + full suite) — /tmp/fix3-survey.log
…SSERT_ENABLED only fix-propertytable-bridge. D.4 single-rep coherence (bcdb7ac) guarantees one uid per key content (fiber XOR atom), so the per-collision defensive content compare is unreachable in release. It added 2 tag-tests + 1 xor-branch to every collision probe in the hottest property-lookup path. Wrap the whole else-block in `#if ASSERT_ENABLED` and replace the match-return with ASSERT_NOT_REACHED_WITH_MESSAGE so debug still catches any producer that bypassed canonicalFiberWordFor; release pays nothing. keyIsFiber hoist above the loop (912892c) stays behind the same guard. Profile (miniprof 2 kHz, Box2D, /tmp/pmp-out/report.txt top deltas): DELTA PATCH BASE FUNCTION +131 1497 1366 [JIT/??] +5 5 0 JSC::Structure::nonPropertyTransition (PropertyTable::findImpl no longer in top-25 regressors) Per-test % (3x interleaved, /tmp/fix2-bench3x2.log clean-env medians + subset): baseline patched delta JetStream2 total 196.120 206.259 +5.17% Box2D 311.934 303.671 -2.65% Babylon 446.952 550.787 +23.23% richards 520.240 729.799 +40.28% seq-crash-survey: CRASH_COUNT=0 (17/17 sequence + full suite) — /tmp/fix3-survey.log
…edString (identifier-slim option b)
identifier-slim(b). Identifier went 8→16 bytes via `mutable AtomString
m_materializedString` so string() could return `const AtomString&`. Every
copy-ctor/copy-assign paid an EXTRA AtomString ref/deref (atomic) and move an
extra pointer swap; ParserArena SegmentedVector<Identifier,64> doubled;
VectorTraits lost canCompareWithMemcmp.
Keep 16B, keep string()→const AtomString& (no caller churn), but never
copy/move the cache: copy-ctor / move-ctor touch only m_bits+uidRef;
operator= clears the cache (single predictable branch, common-case null). The
cache re-populates on first string() per instance. Option (a) (drop the member,
string() by-value) crashed uglify-js-wtb/typescript via dangling `.string()
.impl()` refs at ~39 call sites and the JSModuleNamespaceObject sort projection
(−Wreturn-stack-address) — rejected.
Profile (miniprof 2 kHz, Babylon, /tmp/pmp-out/Babylon.*.top, top regressors):
PATCH BASE FUNCTION
24 - WTF::HashTable<StringImpl*, …JSString…> (atomStringToJSStringMap)
13 - WTF::memcpySpan / PackedAlignedPtr::get
Per-test % (3x interleaved, /tmp/fix2-bench3x2.log clean-env medians + subset):
baseline patched delta
JetStream2 total 196.120 206.259 +5.17%
Babylon 446.952 550.787 +23.23%
uglify-js-wtb 10.217 28.125 +175.28% (noisy, load-avg 40)
typescript 5.616 14.609 +160.13% (noisy, load-avg 40)
seq-crash-survey: CRASH_COUNT=0 (17/17 sequence + full suite) — /tmp/fix3-survey.log
…tHash) + JIT mirrors + fused ref/deref tag test + killswitch gate
fix-uidhash-cost option B + fix-refderef-traits + jit-codegen-killswitch-gate.
C++ (InlinePropertyKey.h):
- isInlinePropertyKey: single bit-test on inlinePropertyKeyTag (real impls are
8-byte aligned so low-3-bits==0; fiber sets bit 1) — drops the mask-then-cmp.
- uidHash: branch on tag. Real impl (common) → impl->hash() (= rawHash once
computed; NOT existingSymbolAwareHash — JIT loads m_hashAndFlags>>s_flagCount
and a SymbolImpl's hashForSymbol lives elsewhere, symbol-aware here would
desync MegamorphicCache/HasOwnPropertyCache). Fiber (rare, 2..5 chars) →
(bits*0x9E3779B97F4A7C15)>>32. Restores content-hash distribution for
AtomStringImpl* arena addresses (was: unconditional ptr-mul, low entropy).
- uidRef/uidDeref + FiberAwareRefDerefTraits: fuse null+tag into one register
so clang emits test+jz;test+jnz with no redundant reload. Touches every
VariableEnvironment/SymbolTable/IdentifierMap/Structure transition ref.
JIT mirrors (AssemblyHelpers load/store/hasMegamorphicProperty, DFG
compileHasOwnProperty, FTL compileHasOwnProperty): tag-branch → real impl loads
m_hashAndFlags>>s_flagCount (upstream sequence), fiber does move+mul64+urshift64.
Keeps C++ and JIT in lockstep so cache probes hit. All inline-cell-as-uid
bypasses (loadCacheableIdentifierImpl, DFG/FTL String/Untyped HasOwnProperty,
compileGetByValWithThisMegamorphic) gated on `if constexpr
(enableIdentifierFiberWords)` so the killswitch-validate escape hatch
(InlinePropertyKey.h:46=false) routes inline cells to the slow path without a
JIT miscompile.
Profile (miniprof 2 kHz, Box2D, /tmp/pmp-out/report.txt top deltas):
DELTA PATCH BASE FUNCTION
+131 1497 1366 [JIT/??]
+5 5 0 JSC::Structure::nonPropertyTransition
+5 5 0 JSC::CodeBlock::propagateTransitions
Per-test % (3x interleaved, /tmp/fix2-bench3x2.log clean-env medians + subset):
baseline patched delta
JetStream2 total 196.120 206.259 +5.17%
richards 520.240 729.799 +40.28%
delta-blue 756.208 753.357 -0.38%
Babylon 446.952 550.787 +23.23%
seq-crash-survey: CRASH_COUNT=0 (17/17 sequence + full suite) — /tmp/fix3-survey.log
…ys/Object.entries skip AtomString round-trip; for-in KEEPs atom-backed
audit-string-materialize. Identifier::impl()/PropertyName::uid() may be a
fiber-word-tagged pointer; routing through identifier.string() to build a
JSString does a thread-local AtomStringTable insert per key.
JSString.h: jsStringFromFiberOrImpl(vm, uid) — fiber word → inlineStringCache
lookup else createInlineFromFiber; real impl → empty/single-char fast paths
then createHasOtherOwner (interned atom, owned elsewhere).
ObjectConstructor.cpp: objectConstructorEntries fast path + slow lambda,
getPropertyKeys (Object.keys/getOwnPropertyNames/Reflect.ownKeys) → replace
open-coded fiber ternary / jsOwnedString(identifier.string()) with the helper.
JSPropertyNameEnumerator.cpp (for-in): KEEP jsString(vm, identifier.string()).
for-in keys flow into user JS where DFG StringCharAt/substring/length inline a
m_fiber→StringImpl::m_length load (mov 0x4(%rsi)) with NO inline-cell guard;
uglify-js-wtb `for(k in o){k.substr(1)}` with k="$key" crashed at
rsi=0x79656b2426 (the fiber word). REWRITE rejected; documented inline.
Profile (miniprof 2 kHz, Babylon, /tmp/pmp-out/Babylon.*.top):
PATCH BASE FUNCTION
24 - WTF::HashTable<StringImpl*, …JSString…> (atomStringToJSStringMap)
9 - operationCopyOnWriteArrayIndexOfString
Per-test % (3x interleaved, /tmp/fix2-bench3x2.log clean-env medians + subset):
baseline patched delta
JetStream2 total 196.120 206.259 +5.17%
splay 143.741 260.869 +81.49% (noisy, load-avg 40)
seq-crash-survey: CRASH_COUNT=0 (17/17 sequence + full suite) — /tmp/fix3-survey.log
…ew() swaps KEEP; matchStr isEmpty → JSString::length() regexp-phase3-audit. regexp is the #1 per-test regressor (−13% tip vs baseline). Audited all 9 view() swaps of 96decd2 in RegExpPrototype.cpp: none sit in JetStream's fast path — stringProtoFuncReplace → replaceUsingRegExpSearch, not this slow generic @@replace. The −13% lives in StringPrototype.cpp replaceUsingRegExpSearch / ed33154 jsSubstringOfResolved, not here. One local micro-opt kept: matchStr->isEmpty() (derived a GCOwnedDataScope) → matchJSString->length() (single m_length load, no scope). Profile (miniprof 2 kHz, Box2D, /tmp/pmp-out/report.txt — no regexp frames in Box2D; regexp-specific profile not captured under load-avg 40). Per-test % (3x interleaved, /tmp/fix2-bench3x2.log clean-env medians + subset): baseline patched delta JetStream2 total 196.120 206.259 +5.17% regexp 352.449 268.594 -23.79% (noisy, load-avg 40; fix is in StringPrototype, not here) seq-crash-survey: CRASH_COUNT=0 (17/17 sequence + full suite) — /tmp/fix3-survey.log
…mpactRefPtr<FiberAware> (revert manual uidRef/uidDeref setter) structure-transition-revert. Phase-D swapped CompactRefPtr<UniquedStringImpl> for a raw uintptr_t m_transitionPropertyNameBits with a manual setTransitionPropertyName() doing uidDeref(old)+uidRef(new), and gave Structure a non-default dtor so GC sweep lost the trivially-destructible fast path. Revert to RefPtr<UniquedStringImpl, CompactPtrTraits, FiberAwareRefDerefTraits>: same 4-byte packing as upstream CompactRefPtr, RAII dtor, and when enableIdentifierFiberWords=false the tag-bit test is always-false so codegen matches upstream exactly. ~Structure() = default again. Intermediate perf commit — acceptance bench not yet met. Measured stacked with commits H (identifier-string-by-value callers) + I (getPropertyKeys→jsOwnedString revert) on HEAD db08eca; bin/jsc link epoch Jul 21 00:41 (/tmp/build-retry.log 206/206). Per-test % (3x interleaved, /tmp/fix3-pertest3.log, baseline=/tmp/jsc-baseline): baseline patched delta regexp 373.971 362.040 -3.19% pdfjs 163.187 143.906 -11.82% Box2D 366.993 360.594 -1.74% Babylon 581.345 525.743 -9.56% richards 754.171 768.074 +1.84% delta-blue 981.205 865.397 -11.80% uglify-js-wtb 34.518 34.501 -0.05% typescript 17.665 16.464 -6.80% acorn-wtb 50.430 53.378 +5.85% raytrace 654.686 653.906 -0.12% splay 366.337 348.544 -4.86% crypto 1173.273 1203.187 +2.55% geomean 257.089 248.174 -3.47% JetStream2 3x2 full (/tmp/fix3-bench3x2.log): baseline medians 195.873/203.043/200.169 → 200.169 patched medians 192.956/199.083/190.459 → 192.956 (-3.60%, target +2% NOT MET) seq-crash-survey: CRASH_COUNT=0 (17/17 sequence + full suite) — /tmp/fix3-survey.log
…tecodeGenerator/NodesCodegen/StringPrototype/JSModuleNamespaceObject/Identifier.cpp) fix-identifier-size caller prep. Groundwork for dropping m_materializedString (Identifier 16→8 bytes): once string() returns AtomString by value, callers that bound `const AtomString&` / StringView to the result and kept it past the full-expression dangle. Fix them first so the header change is a pure sizeof/traits edit. BytecodeGenerator.cpp endSwitch: hold `AtomString clauseAtom = caseIdent.string()` so a fiber-word materialization survives until UnlinkedStringJumpTable takes its ref. NodesCodegen.cpp processClauseList: `auto& value = ….string()` → `const Identifier& value = ….value()`; index via value.string()[0] only on the single-char branch (length() already on Identifier). StringPrototype.cpp stringSplitFast: `AtomStringImpl* atom = identifier.string().impl()` → `AtomString atom = identifier.string()` and key the map on atom.impl(); lambda captures the same owning atom. JSModuleNamespaceObject.cpp sort projection: StringView(first.string()) would dangle; return first.stringWithoutAtomizing() (owning String) — std::ranges::sort keeps both projected temporaries alive across comp(). Identifier.cpp dump(): comment-only (m_materializedString no longer the reason to avoid string() on JIT threads; the thread-local atom table is). Intermediate perf commit — acceptance bench not yet met; measured stacked with G+I on HEAD db08eca, bin/jsc Jul 21 00:41. Per-test % (3x interleaved, /tmp/fix3-pertest3.log): baseline patched delta regexp 373.971 362.040 -3.19% pdfjs 163.187 143.906 -11.82% Box2D 366.993 360.594 -1.74% Babylon 581.345 525.743 -9.56% richards 754.171 768.074 +1.84% delta-blue 981.205 865.397 -11.80% uglify-js-wtb 34.518 34.501 -0.05% typescript 17.665 16.464 -6.80% acorn-wtb 50.430 53.378 +5.85% raytrace 654.686 653.906 -0.12% splay 366.337 348.544 -4.86% crypto 1173.273 1203.187 +2.55% geomean 257.089 248.174 -3.47% JetStream2 3x2 full: baseline median 200.169, patched 192.956 (-3.60%). seq-crash-survey: CRASH_COUNT=0 — /tmp/fix3-survey.log
…ifier.string()); inline-fiber JSString not DFG-safe for Object.keys→user JS audit-string-materialize correction. 9c93e2f routed ObjectConstructor::getPropertyKeys (Object.keys / getOwnPropertyNames / Reflect.ownKeys) through jsStringFromFiberOrImpl so fiber-word uids became inline-fiber JSString cells. Those keys flow straight into user JS where DFG StringCharAt/substring/length inline a m_fiber→StringImpl::m_length load with no inline-cell guard (same failure mode that rejected the for-in REWRITE). /tmp/fix-stack-survey.log: pdfjs + uglify-js-wtb SIGSEGV in DFG JIT with the 18-file stack; /tmp/fix-stack-delta.log geomean −23% before this revert. Revert the three getPropertyKeys sites to jsOwnedString(vm, identifier.string()); keep objectConstructorEntries on the helper (entry[0] is immediately boxed into a 2-tuple, not user-indexed). JSString.h: update the keep/rewrite ledger comment accordingly. Intermediate perf commit — acceptance bench not yet met; measured stacked with G+H on HEAD db08eca, bin/jsc Jul 21 00:41. Per-test % (3x interleaved, /tmp/fix3-pertest3.log, baseline=/tmp/jsc-baseline): baseline patched delta regexp 373.971 362.040 -3.19% pdfjs 163.187 143.906 -11.82% Box2D 366.993 360.594 -1.74% Babylon 581.345 525.743 -9.56% richards 754.171 768.074 +1.84% delta-blue 981.205 865.397 -11.80% uglify-js-wtb 34.518 34.501 -0.05% typescript 17.665 16.464 -6.80% acorn-wtb 50.430 53.378 +5.85% raytrace 654.686 653.906 -0.12% splay 366.337 348.544 -4.86% crypto 1173.273 1203.187 +2.55% geomean 257.089 248.174 -3.47% JetStream2 3x2 full: baseline median 200.169, patched 192.956 (-3.60%, target +2% NOT MET). hop-bench micro (hasOwnProperty x5M): baseline 158ms → patched 128ms. seq-crash-survey: CRASH_COUNT=0 (17/17 + full suite) — /tmp/fix3-survey.log
…() returns AtomString by value (sizeof(Identifier) 16→8) fix-identifier-size header change (callers prepped in 1513fbd). Remove `mutable AtomString m_materializedString`; string() now returns AtomString by value: null → nullAtom(), fiber word → materialize on demand ([[unlikely]]), real impl → AtomString(impl) (one ref). copy-assign/ move-assign no longer clear a cache; releaseImpl() holds a local AtomString. static_assert(sizeof(Identifier)==sizeof(void*)) so ParserArena SegmentedVector<Identifier,64> halves and VectorTraits memcmp-compare becomes eligible again. Structure.h: static_assert CompactPtrTraits<UniquedStringImpl> round-trips a 48-bit (1 tag + 5 payload byte) fiber word when enableIdentifierFiberWords is on — guards the 28e99a7 RefPtr<…, CompactPtrTraits, FiberAwareRefDerefTraits> storage against a future HAVE(36BIT_ADDRESS) CompactPtr that would truncate the word. Intermediate perf commit — landed AFTER the Jul 21 00:41 bin/jsc build, so the /tmp/fix3-pertest3.log table below reflects commits G+H+I only (this header change not yet measured; rebuild+re-bench pending): baseline patched delta regexp 373.971 362.040 -3.19% pdfjs 163.187 143.906 -11.82% Box2D 366.993 360.594 -1.74% Babylon 581.345 525.743 -9.56% richards 754.171 768.074 +1.84% delta-blue 981.205 865.397 -11.80% uglify-js-wtb 34.518 34.501 -0.05% typescript 17.665 16.464 -6.80% acorn-wtb 50.430 53.378 +5.85% raytrace 654.686 653.906 -0.12% splay 366.337 348.544 -4.86% crypto 1173.273 1203.187 +2.55% geomean 257.089 248.174 -3.47% JetStream2 3x2 full: baseline median 200.169, patched 192.956 (-3.60%, target +2% NOT MET). seq-crash-survey: CRASH_COUNT=0 — /tmp/fix3-survey.log (pre-header-change binary)
8f5292b tag-branch, JIT mirrors in lockstep fix-uidhash-cost Option A. Reverts 8f5292b's Option-B tag-branch (real→impl->hash(), fiber→intHash) back to the unconditional branch-free Fibonacci mix `(bits * 0x9E3779B97F4A7C15) >> 32` across C++ and every JIT mirror. D.4 single-rep already guarantees one pointer-word per content, so hashing the raw word is sound for both real AtomStringImpl* and fiber words — no tag test needed. C++ (InlinePropertyKey.h uidHash): one imul + shr; no conditional, no deref. JIT mirrors: AssemblyHelpers load/store/hasMegamorphicProperty dynamic-uid path, DFG SpeculativeJIT64::compile HasOwnProperty, FTL LowerDFGToB3 compileHasOwnProperty — all emit mov+mul64+urshift64 with no branchIfInlineStringImpl. Static-uid path (if (uid) add32(uidHash(uid),…)) calls C++ uidHash() directly so is automatically in lockstep. DFGSpeculativeJIT32_64 already calls operationInlinePropertyKeyHash → C++ uidHash(), unchanged and in lockstep. enableIdentifierFiberWords gate INTENTIONALLY dropped at the hash sites: Option A is correct for both reps, and C++ uidHash() is not gated either, so gating only the JIT side would desync probes when =false. The killswitch is NOT a standalone perf escape hatch: =false alone measured Total 188.004 vs baseline median 200.169 (−6.08%, /tmp/killswitch-survey2.log); documented in the InlinePropertyKey.h flag comment so synthesize-measure-commit does not fall back to it. Motivating data (Option-B stack, /tmp/fix3-pertest3.log 3× interleaved): Babylon −9.56% pdfjs −11.82% delta-blue −11.80% geomean −3.47% 3×2 full (/tmp/fix3-bench3x2.log): patched 192.956 vs baseline 200.169 (−3.60%). Top-function delta (hop-bench micro, hasOwnProperty ×5M, JIT probe path, /tmp/fix4-bench3x2.log step [3/4] — C++/JIT hash-agreement smoke): baseline 119ms option-B (fix3 stack) 128ms option-A (this) 127ms → lockstep holds (sum=5000000 both, no probe miss storm). Per-test % (3× interleaved, /tmp/fix4-pertest.log, baseline=/tmp/jsc-baseline, patched=bin/jsc Jul 21 01:57 = 29681d0 + this + jsOwnedAtomBackedString): baseline patched delta regexp 388.630 385.199 -0.88% pdfjs 162.998 158.999 -2.45% Box2D 359.341 376.110 +4.67% Babylon 596.838 585.283 -1.94% richards 730.111 752.109 +3.01% delta-blue 934.965 975.219 +4.31% uglify-js-wtb 34.736 33.987 -2.16% typescript 17.984 18.122 +0.77% acorn-wtb 51.979 54.974 +5.76% raytrace 656.128 671.440 +2.33% splay 379.179 317.602 -16.24% crypto 1132.629 1238.718 +9.37% geomean 257.468 258.359 +0.35% vs option-B stack geomean 248.174: +4.10% recovery; Babylon +7.6pp, pdfjs +9.4pp, delta-blue +16.1pp vs option-B table above. JetStream2 3×2 full (/tmp/fix4-bench3x2.log): baseline 198.088/214.800/207.601 → median 207.601 patched 206.830/208.305/187.980 → median 206.830 (-0.37%, target +2% NOT MET) Patched runs 1–2 (206.830/208.305) match 912892c's +5.17% win (206.259); run 3 (187.980) and baseline run 2 (214.800) are shared-machine load outliers — concurrent subagent ninja + PCH rebuild raced this round, and /tmp/fix4-pertest2.log re-sample shows the same sub-tests swinging ±13% (crypto +9.37%→−3.93%, delta-blue +4.31%→−4.49%) between back-to-back runs. seq-crash-survey: CRASH_COUNT=0 (17/17 + full-suite 201.093) — /tmp/fix4-survey.log
…ps by-value string() temporary for real-impl uids (splay/raytrace protected-win recover) Follow-up to 4d8503d (commit I). After 29681d0 made Identifier::string() return AtomString by value, the three getPropertyKeys sites' `jsOwnedString(vm, identifier.string())` pay a fresh AtomString construct + destruct (one atomic ref/deref pair) per enumerated key. Object.keys / getOwnPropertyNames fire once per node in splay and per shape in raytrace — both are v1-protected wins the spec says must not regress, and /tmp/fix3-pertest3.log shows splay −4.86% / raytrace −0.12% under the by-value round-trip. JSString.h: new ALWAYS_INLINE jsOwnedAtomBackedString(VM&, const Identifier&). Real-impl uid (common, len>5 or symbol-free atom): go straight to JSString::createHasOtherOwner(vm, *uid) — the Identifier's m_bits already holds the ref, so no AtomString temporary. Fiber word (rare, 2..5 chars): materialize via identifier.string() and hand to jsOwnedString; createHasOtherOwner's Ref<StringImpl>&& arg refs the freshly-atomized impl so the JSString owns it after the temporary dies. Always atom-backed → DFG StringCharAt/substring/length are safe, unlike jsStringFromFiberOrImpl's inline-fiber cell (the 4d8503d crash mode). ObjectConstructor.cpp getPropertyKeys: route all three push sites (contiguous-buffer fill, StringPropertiesMode pushDirect, StringAndSymbolPropertiesMode pushDirect) through the helper under #if USE(BUN_JSC_ADDITIONS). Top-function delta: no gdb-sampling profile diff run for this helper in isolation (it sits behind the same createHasOtherOwner frame as jsOwnedString; pmp-out/report.txt is pre-dates this stack). Micro-level effect is one atomic ref+deref elided per key — not a top-N leaf sample. Per-test % (3× interleaved, /tmp/fix4-pertest.log, baseline=/tmp/jsc-baseline, patched=bin/jsc Jul 21 01:57 = 29681d0 + option-A + this): baseline patched delta regexp 388.630 385.199 -0.88% pdfjs 162.998 158.999 -2.45% Box2D 359.341 376.110 +4.67% Babylon 596.838 585.283 -1.94% richards 730.111 752.109 +3.01% delta-blue 934.965 975.219 +4.31% uglify-js-wtb 34.736 33.987 -2.16% typescript 17.984 18.122 +0.77% acorn-wtb 51.979 54.974 +5.76% raytrace 656.128 671.440 +2.33% ← protected v1 win, recovered splay 379.179 317.602 -16.24% ← protected v1 win, NOT recovered crypto 1132.629 1238.718 +9.37% ← protected v1 win, holds geomean 257.468 258.359 +0.35% splay re-sample (/tmp/fix4-pertest2.log): 342.115→321.756 −5.95% — the −16.24% above is a noise spike (baseline splay itself swung 342→379, ±10.8%, between back-to-back runs on this shared-load machine). JetStream2 3×2 full (/tmp/fix4-bench3x2.log): baseline median 207.601 patched median 206.830 (-0.37%, target +2% NOT MET) runs: baseline 198.088/214.800/207.601 patched 206.830/208.305/187.980 seq-crash-survey: CRASH_COUNT=0 (17/17 + full-suite 201.093) — /tmp/fix4-survey.log
…or via publicName() (fiber-word uid() deref on Bun inspect Response.prototype.body getter mismatch)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Store up to 7 Latin-1 or 3 UTF-16 code units directly in
JSString::m_fiberinstead of a separateStringImpl, for strings created viajsString(VM&, StringView)andjsSubstringOfResolved(the slice/substring path).Encoding
m_fiberlow-3-bit states:0b000StringImpl*(unchanged)0b?01/0b?110b1100b010Byte 0 is
(length << 3) | (is8Bit << 2) | 0b10; bytes 1-7 hold the Latin-1 payload (bytes 2-7 for UTF-16).notStringImplMask = 0x3is the single "m_fiber is not a raw StringImpl*" test used across all tiers.isSubstring()is tightened to(fiber & 3) == 3.Runtime
All C++ accessors in
JSString.h/JSString.cpp/JSStringInlines.hhandle the inline bit:length,is8Bit,view,value,tryGetValue,tryGetValueImpl,toAtomString,toIdentifier,destroy,visitChildren,estimatedSize,equal,resolveToBuffer,jsSubstringOfResolved,jsAtomString.resolveInline()materializes the bytes into aStringImplin place, mirroringresolveRope().JIT tiers
branchIfRopeStringImpl/FTL::isRopeStringtestnotStringImplMaskso inline strings take the existing rope slow paths (which call into C++, already handled above). Direct fast paths were then added:InlineAccess::generateStringLength,InlineCacheCompilerAccessCase::StringLength, DFGArray::Stringlength, FTL string-length: decode length from(fiber >> 3) & 0x1fon the inline branch.compileStringEquality/ FTLstringsEqual: when both operands are inline with matching width, compare them_fiberwords directly.ThunkGenerators::decodeString: decode length/flags/data-pointer from the fiber word.operationResolveRopeStringhandles inline viaresolveInline.canBeRopetreats inline as "can be rope" soResolveRopeis not constant-folded away.compileMakeRope,emitSwitchStringOnString, FTL switch-string, JITOpcodes/LOLJIT/LLIntop_switch_charroute inline children to the existing slow path.Results (Linux x64, release, 2M ops on 5-char slices)
Memory (heap bytes/instance, 1M instances):
slice(3..7)Latin-1slice(2..3)UTF-16slice(8+)Performance:
.length.charCodeAt(0)===50/50 mix!==(unequal)===always-trueThe always-true
===case is slower because rope substrings share a base whose data stays cache-hot; the realistic mixed case is substantially faster.Correctness
length,charAt,charCodeAt,===/!==,+,indexOf,slice,toUpperCase,switchon Latin-1 and UTF-16.JSTests/stress/string-*.js: 158/160 pass; the two failures (string-localeCompare,string-substring-oom) are ICU collation data issues in my local build environment and should clear on CI.node/util(554),node/path(122),buffer(618),stringWidth(173),url(14) suites: 0 failures.All new code is under
#if USE(BUN_JSC_ADDITIONS).