← All issues

[JSC] Inline `NumericStrings` int cache lookup in DFG and FTL `ToString(Int32)`

186e2a9

NumericStrings is a per-VM cache mapping integers to their precomputed JSString representations, with two tiers: a direct-mapped small-int array (indices 0..1023) and a hash-based m_intCache for arbitrary 32-bit values, keyed by WTF::IntHash<int> (the rapidHashMix64 mum mixer). When DFG/FTL inlines a cache lookup, it emits the hash arithmetic, memory loads, and conditional branches as native instructions — no C++ call frame, no runtime guards beyond what is explicitly emitted.

Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp

- slowCases.append(branch32(AboveOrEqual, valueGPR, TrustedImm32(NumericStrings::cacheSize)));
- move(valueGPR, resultGPR);
- lshiftPtr(TrustedImm32(WTF::fastLog2(static_cast<unsigned>(sizeof(NumericStrings::StringWithJSString)))), resultGPR);
- addPtr(TrustedImmPtr(vm().numericStrings.smallIntCache()), resultGPR);
- loadPtr(Address(resultGPR, NumericStrings::StringWithJSString::offsetOfJSString()), resultGPR);
- doneCases.append(branchTestPtr(NonZero, resultGPR));
- callOperation(operationInt32ToStringWithValidRadix, resultGPR, LinkableConstant::globalObject(*this, node), valueGPR, TrustedImm32(10));

Source/JavaScriptCore/b3/B3AbstractHeapRepository.h

+ macro(IntCache, 0, sizeof(NumericStrings::CacheEntryWithJSString<int>)) \

This commit inlines the NumericStrings int-cache hash lookup for radix-10 ToString(Int32) in the 64-bit DFG and FTL backends, covering values outside the existing 0..1023 small-int range. It emits rapidHashMix64 directly as JIT machine code to index into m_intCache, returning the cached JSString on a key match and only falling through to the vmCall on a miss. A new IntCache B3 abstract heap controls how the optimizer may reorder those loads. The 32-bit DFG backend keeps the old small-int-only path since the 128-bit multiply in rapidHashMix64 is not viable there.

The result is a ~2.38x speedup for large-integer-to-string conversions in hot JIT code, at the cost of new JIT-emitted code that directly dereferences GC-managed JSString pointers out of a runtime hash table.

The key-match and jsString-non-null check are two separate loads from the cache slot — if a write to the cache (from another ToString on a colliding key) races between the two loads, the key check can pass on one value while jsString belongs to a different integer. The rapidHashMix64 JIT emission must be bit-for-bit identical to WTF::IntHash<int>; any difference in integer width, sign extension, or multiply truncation would index a different slot than the runtime, returning a JSString for the wrong integer. The new B3 IntCache abstract heap governs reordering of the slot load relative to surrounding stores — if its declared size or offset is wrong, the optimizer could use a stale slot value while believing the heap was unmodified. Finally, the 64/32-bit split creates a permanent maintenance divergence: an asymmetric future change to the cache structure could silently misread entries on 64-bit while 32-bit stays correct.