[JSC] Cache result of Symbol.prototype.toString
Component: JSC | 5e57a58
JSC에서 "intrinsic"이란, JIT가 identity로 인식해서 일반 호출 대신 hand-coded IR node로 대체하는 built-in function을 말합니다. Intrinsic을 새로 추가하면 DFGByteCodeParser가 전용 node를 emit하고, DFGFixupPhase가 speculative type guard로 입력을 제약하며, SpeculativeJIT/FTL이 이를 machine code로 lower합니다. 이 과정에서 GC는 새 node가 건드리는 모든 heap pointer를 추적할 수 있어야 합니다.
JSTests/stress/symbol-prototype-to-string-intrinsic.js
Symbol마다 cached string 필드가 추가되어, Symbol.prototype.toString()을 반복 호출할 때 미리 할당된 JSString을 반환하도록 변경되었습니다. 새로 추가된 SymbolToString DFG/FTL intrinsic node는 이 연산을 해당 slot에서 직접 field를 load하는 형태로 컴파일하며, 최초 호출 시에는 slow path가 cache를 할당하고 값을 기록합니다. StringConstructor.cpp도 별도로 수정되어, String(symbol) 경로에서 동일한 cache를 사용하도록 되었습니다.
JS: sym.toString()
│
▼
DFGByteCodeParser
sees SymbolPrototypeToStringIntrinsic ──► emits SymbolToString(sym)
│
▼
DFGFixupPhase
constrains input ──► SymbolUse
│
▼
SpeculativeJIT64 / FTLLowerDFGToB3
load Symbol::m_cachedString
┌── non-null ──► return cached JSString (fast path, no alloc)
└── null ──► slow-path call ──► allocate JSString, write cache, return
Significance
새로 추가된 JIT intrinsic이 Symbol cell 위의 GC-traced 필드를 raw memory load로 직접 읽어오게 되었으므로, type speculation과 GC liveness, slow-path write-back이 동시에 모두 정확하게 맞아떨어져야 합니다. Intrinsic의 type assumption과 실제 runtime 동작 사이에 불일치가 생기면, JSC에서 흔히 나타나는 전형적인 type-confusion vector가 됩니다.
Audit directions
좁게 보면, 핵심은 type-speculation 경계입니다. FixupPhase는 입력을 SymbolUse로 제약하는데, 정작 Symbol.prototype.toString은 Object(Symbol())로 생성되는 boxed wrapper인 SymbolObject를 this로 받아 호출하는 것도 가능합니다. 만약 intrinsic이 SymbolObjectUse를 emit하거나 두 케이스를 모두 처리해야 할 자리에 SymbolUse만 emit한다면, speculation guard 자체가 잘못된 것이고 type confusion이 하위 단계에서 그대로 체크되지 않은 채 통과하게 됩니다. 새로운 intrinsic을 리뷰할 때 눈여겨봐야 할 신호는, spec 상 primitive와 wrapper를 모두 허용하는 prototype method임에도 Use kind가 단 하나만 지정되어 있는 경우입니다.
좀 더 넓게 보면, 새 필드에 대한 GC tracing도 점검 대상입니다. Symbol::visitChildrenImpl이 이제 cached JSString을 추적해야 하는데, field offset이 잘못되었거나 DECLARE_VISIT_CHILDREN expansion이 누락되었거나 compaction 이후 pointer가 stale 상태로 남으면, fast-path load를 통해 도달 가능한 dangling string이 만들어집니다. 이는 위쪽 JSPromise rework에서도 동일하게 지적된 manual-visit 의무 사항과 같은 맥락입니다. 기존 JSC cell type에 cell-pointer member를 추가하는 커밋을 확인할 때마다, 같은 diff 안에 대응하는 visitChildrenImpl 업데이트가 포함되어 있는지 함께 훑어볼 필요가 있습니다.
가장 넓게 보면, 하나의 cache를 서로 다른 곳에서 사용하는 consumer 간의 일관성도 살펴봐야 합니다. m_cachedString을 초기화하는 slow path는 여러 tier의 JIT 코드에서 호출되므로, 해당 store가 concurrent compile 사이에서 제대로 visible한지, 그리고 첫 번째 slow-path entry와 경합하는 두 번째 entry가 partially-written pointer를 노출시킬 가능성은 없는지 확인해야 합니다. StringConstructor.cpp는 String(symbol)을 통해 동일한 cache에 접근하는데, 이때 이 경로의 validity assumption이 intrinsic 쪽과 어긋난다면 그 자체로 consistency bug에 해당합니다. 하나의 cache에 다른 파일에서 두 번째 reader가 추가될 때마다, 두 reader의 precondition을 서로 비교해보는 것이 중요한 점검 포인트입니다.