← All reports

[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

+ function* first() { yield "a"; yield "b"; yield "c"; }
+ function* second() { yield "a"; yield "b"; yield "c"; }
+ const withGenerator = { [Symbol.iterator]: first };
+
+ function collect(iterable) { let seen = []; for (let x of iterable) seen.push(x); return seen.join(); }
+ function breakAtTwo(iterable) { let seen = []; for (let x of iterable) { seen.push(x); if (seen.length === 2) break; } return seen.join(); }
+ function firstTwo(iterable) { let [a, b] = iterable; return a + "," + b; }
...
+ for (let iterable of ["abc", withGenerator]) {
+ shouldBe(collect(iterable), "a,b,c");
+ shouldBe(breakAtTwo(iterable), "a,b");
+ shouldBe(firstTwo(iterable), "a,b");
+ }

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

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.

This reworks register semantics across LLInt (LowLevelInterpreter64.asm), Baseline (JITCall.cpp) and DFG (DFGByteCodeParser.cpp) at once, in a security-critical type-assumption area.