← All reports

[JSC] Add Array#concat DFG nodes

Component: JSC | 94e35cf

Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp

+ case ArrayConcatIntrinsic: {
+ if (argumentCountIncludingThis != 2)
+ return CallOptimizationResult::DidNothing;
+
+ if (m_inlineStackTop->m_exitProfile.hasExitSite(m_currentIndex, BadConstantCache)
+ || m_inlineStackTop->m_exitProfile.hasExitSite(m_currentIndex, BadCache)
+ || m_inlineStackTop->m_exitProfile.hasExitSite(m_currentIndex, BadType)
+ || m_inlineStackTop->m_exitProfile.hasExitSite(m_currentIndex, ExoticObjectMode))
+ return CallOptimizationResult::DidNothing;
+
+ ArrayMode arrayMode = getArrayMode(Array::Read);
+ if (!arrayMode.isJSArray())
+ return CallOptimizationResult::DidNothing;
+
+ if (!arrayMode.isJSArrayWithOriginalStructure())
+ return CallOptimizationResult::DidNothing;

Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h

+ case ArrayConcatArray:
+ case ArrayConcatAppendOne:
+ setTypeForNode(node, SpecArray);
+ break;

DFG는 JSC의 speculative mid-tier compiler입니다. Bytecode를 typed node로 구성된 IR로 낮추고, type prediction을 기반으로 하는 여러 최적화 단계(parsing, fixup, abstract interpretation, codegen)를 거친 뒤 machine code를 생성합니다. 이때 가정이 깨지면 OSR exit를 통해 interpreter로 되돌아갑니다. Watchpoint는 이런 가정을 뒷받침하는 invalidation 신호 역할을 합니다. 이번 commit은 ArrayConcatArrayArrayConcatAppendOne이라는 두 개의 새로운 DFG/FTL intrinsic node를 추가하여 Array.prototype.concat을 JIT compile할 수 있도록 합니다. Fixup phase에서는 type prediction을 이용해 일반적인 append node를 array 전용 variant로 특수화합니다. Fast path는 arrayIsConcatSpreadableWatchpointSet과 original-structure check(isJSArrayWithOriginalStructure)로 보호됩니다. 런타임에 복잡한 side effect가 발견되면 해당 operation은 nullptr을 반환하고, JIT는 ExoticObjectMode를 통해 OSR exit합니다. concat은 fast-path 적용이 유난히 까다로운 함수인데, ES spec상 임의의 object가 [Symbol.isConcatSpreadable]을 통해 spreading 여부를 opt-in/opt-out할 수 있기 때문입니다. Watchpoint는 바로 이 hook의 가능성을 배제하는 역할을 합니다. 여기에 COW(copy-on-write) butterfly가 최적화의 나머지 절반을 담당합니다. Array literal의 backing store는 write가 강제로 copy를 유발하기 전까지 공유되고 read-only 상태를 유지하는데, 덕분에 빈 array와 COW source를 concat하면 같은 butterfly를 재사용하는 array를 반환할 수 있습니다.

Bytecode: array.concat(arg)
         │
         ▼
  DFGByteCodeParser
    └─► emit ArrayConcatAppendOne(array, arg)
              │
              ▼
      DFGFixupPhase (type prediction check)
        ├─[arg predicted Array]──► ArrayConcatArray    (COW-reuse fast path)
        └─[arg unknown]──────────► ArrayConcatAppendOne (generic path)
              │
              ▼
      Runtime operation (DFGOperations.cpp)
        ├─[watchpoints valid, no isConcatSpreadable]──► fast path, may reuse COW butterfly
        └─[side effects / exotic object detected]──────► return nullptr
              │
              ▼
      DFG/FTL checks nullptr
        └─[nullptr]──► OSR exit (ExoticObjectMode) ──► Interpreter

가장 빈번하게 호출되는 Array builtin 중 하나에 새로운 고도로 최적화된 경로가 생겼습니다. COW butterfly 재사용, watchpoint 기반 speculation, 그리고 multi-phase type-driven node 변환이 한데 결합된 형태입니다. 이 세 가지 요소는 각각 독립적으로도 JIT correctness 버그와 type confusion의 역사적인 원인이었는데, 여기서는 이들이 함께 조합됩니다.

Regression test 자체에 tryConcatOneArgFast에서 존재했던 crash 하나가 문서화되어 있습니다. Array가 아닌 object argument가 JSArray로의 uncheckedDowncast에 도달해, 잘못된 butterfly pointer를 dereference하는 문제였습니다. 이 fix는 최근에 적용된 것이며, 인접한 로직에도 같은 가정이 남아 있을 가능성이 있으므로 주변 downcast 지점부터 먼저 살펴볼 필요가 있습니다.

두 번째로 살펴볼 지점은 COW butterfly 재사용입니다. 빈 array와 COW source를 concat한 결과가 같은 butterfly를 공유하는 array로 반환되는 경우, 이 동작의 정확성은 abstract interpreter, fixup, codegen 단계 전반에서 COW 추적이 일관되게 유지되는지에 달려 있습니다. COW 관련 테스트에서는 DFG abstract result structure set이 CopyOnWrite variant를 누락하면 이후 speculation이 unsound해진다는 점을 명시적으로 지적하고 있는데, 이는 곧 abstract interpreter와 실제 runtime 상태가 어긋나는 구간이 최소 한 번은 존재했다는 의미입니다. Result structure set을 수동으로 작성하는 다른 node가 있다면 동일한 점검이 필요합니다.

세 번째는 watchpoint invalidation 경로입니다. Watchpoint 등록과 JIT-compile된 호출 사이의 시점에 [Symbol.isConcatSpreadable]이 설치되는 경우, invalidation과 실행 중인 코드 사이의 순서 관계가 중요해집니다. 네 번째로, fixup phase는 type prediction을 근거로 ArrayConcatAppendOneArrayConcatArray로 변환하는데, fixup 판단 이후 타입이 바뀌도록 입력을 조작하는 것, 예를 들어 prototype 조작을 통한 방식은 전형적인 JIT type confusion 벡터에 해당합니다. 이 문제는 이 node에 국한되지 않고 fixup phase의 모든 prediction 기반 node 변환에 동일하게 적용됩니다. 다섯 번째로, nullptr 반환 시 ExoticObjectMode를 통한 OSR exit는 codegen 단계에서 두 갈래 경로를 만들어냅니다. 두 경로 모두에서 object identity, array length, index bounds가 올바르게 유지되는지 확인이 필요합니다.