[JSC] Iterate Strings without allocating a JSStringIterator
Component: JSC bytecode / LLInt, Baseline, DFG | 5330149
JSTests/stress/for-of-fast-string-and-iterator-with-known-structure.js
ES iteration normally calls iterable[Symbol.iterator]() to get an iterator object, then calls its next method each step and its return method on early exit. JSC has fast modes (FastArray, FastString) in which the iterator object is unobservable as long as the built-in iterator prototypes are untouched, and watchpoint sets such as stringIteratorProtocolWatchpointSet track whether that assumption still holds. FastArray already keeps its state in the bytecode registers: iterator holds a sentinel value, next holds the index and iterable holds the array. This commit applies the same trick to strings.
For-of and destructuring over a String in FastString mode no longer allocate a JSStringIterator in LLInt, Baseline or DFG. The iterator register holds a string-specific sentinel, next holds the current index as an Int32, and iterable still holds the string. op_iterator_next and op_iterator_close_check compare iterator against the Array sentinel first, then the String sentinel. On close, if stringIteratorProtocolWatchpointSet has been invalidated, a real JSStringIterator is created so return can be looked up.
Before (FastString): After (FastString):
iterator_open iterator_open
└─► alloc JSStringIterator ├─► iterator = fastStringSentinel
iterator = JSStringIterator ├─► next = Int32(0)
next = cached next fn └─► iterable = JSString
iterator_next iterator_next
└─► call/inline next(iterator) ├─► iterator == arraySentinel? -> array path
(reads iterator's string+index) ├─► iterator == stringSentinel? -> index++ (no alloc)
iterator_close_check └─► else -> generic call
└─► return absent (watchpoint valid) iterator_close_check
└─► iterator == stringSentinel?
├─► watchpoint valid -> skip close
└─► watchpoint invalid -> materialize
JSStringIterator, run generic close
Significance
This removes one heap allocation per string loop and yields 1.06x-1.29x speedups on string iteration microbenchmarks. It extends the FastArray design to a second iterable type, so more bytecode and JIT paths now depend on sentinel and register-state invariants. Registers that normally hold a callable and an iterator object now hold a sentinel and a raw integer, so the interpreter, both JIT tiers, the DFG parser, the GC and OSR entry/exit must all agree on that encoding.
Audit directions
This reworks register semantics across LLInt (LowLevelInterpreter64.asm), Baseline (JITCall.cpp) and DFG (DFGByteCodeParser.cpp) at once, in a security-critical type-assumption area.
- Consumers of the
nextanditeratorregisters. Look for places that read them and assume a callee or object: OSR exit/entry reconstruction, DFG value profiling, and anything that callsnextwhen the sentinel comparison falls through. A raw Int32 or sentinel reaching a call or aJSCellcast would be type confusion. - Lazy
JSStringIteratormaterialization. Inop_iterator_close_checkwhen the watchpoint has fired mid-loop, the recreated iterator's index and string must match the register state, including after OSR between tiers. Theosr-late-closeandfresh-realmtests show where trouble is expected. - Sentinel lifetime and GC visiting. Check
VM::visitAggregateImpl(fastStringSentinelreplacingfastStringValuesSentinel), and whether the sentinel can be mistaken for the Array sentinel or a real value in a different realm. - Index handling. An Int32 index against long or rope strings (resolution during iteration), surrogate-pair stepping, and wide-operand bytecode variants.
- DFG multi-kind sites. Check conversion to inlined generator iteration alongside String handling when one site sees multiple iterable kinds (
handleIteratorOpen/handleIteratorNext), and whether speculation and watchpoint assumptions stay consistent after invalidation.