← All reports

[JSC] Add global priority queue inliner

Component: JSC DFG JIT | bdb7246

Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp

+bool ByteCodeParser::planPermitsInlining() const
+{
+ if (!m_inliningPlan.isBuilt())
+ return true;
+ const InliningPlan::Site* site = m_inliningPlan.siteFor(m_inlineStackTop->m_planSite, m_currentIndex.offset());
+ if (!site)
+ return true;
+ return site->admitted;
+}
...
+void ByteCodeParser::planInlining()
+{
+ if (!Options::useGlobalInliningPlanner())
+ return;
+ if (m_graph.m_plan.isUnlinked())
+ return;
+ m_inliningPlan.build(m_profiledBlock, m_graph.m_plan.jitType());
+}

Source/JavaScriptCore/runtime/OptionsList.h

+ v(Bool, useGlobalInliningPlanner, false, Normal, ...)
+ v(Unsigned, globalInliningPlanBudgetForDFG, 2500, Normal, ...)
+ v(Unsigned, globalInliningPlanBudgetForFTL, 12000, Normal, ...)

DFG는 JSC의 optimizing JIT tier로, bytecode parsing 과정에서 어떤 함수 호출을 caller의 IR에 직접 inline할지, 아니면 call을 그대로 emit할지 결정합니다. 기존에는 이 판단이 greedy 방식이었습니다. 즉 하나의 parse pass 동안 각 call site를 마주칠 때마다 즉석에서 결정이 이루어졌고, 그 결과 먼저 나온 call site가 실제 이득과 무관하게 inlining budget을 먼저 소진해버릴 수 있었습니다.

새로 추가된 InliningPlan은 profiled call site들을 미리 한 번 훑어보는 pre-pass를 수행합니다. 각 site에 점수를 매겨 순위를 매긴 뒤, 승인된 후보들의 tree를 구성하고, parser는 planPermitsInlining()을 통해 이 결과를 참조합니다. 이 메커니즘은 순수하게 veto 역할만 수행합니다. survey의 admitted list에서 제외된 call site를 거부할 수는 있지만, 기존 handleCallVariant의 cost check와 site별 heuristic은 이후에도 그대로 실행됩니다. 또한 InlineAttribute::Always가 붙은 callee는 이 plan을 아예 거치지 않습니다. survey가 미처 예측하지 못한 site — 예를 들어 나중에 intrinsic이나 DOM call, varargs frame으로 밝혀지는 경우 — 는 기존 heuristic으로 그대로 fallback됩니다. 이 기능은 useGlobalInliningPlanner 옵션 뒤에 게이트되어 있고 기본값은 false이며, budget 역시 충분히 크게 잡혀 있어 관찰되는 동작 자체는 변하지 않습니다.

이 기능이 활성화되고 튜닝되고 나면, 코드베이스 전반에서 어떤 함수가 inline되는지가 달라지게 됩니다. 그리고 이는 곧 보안 연구자가 JIT 동작을 분석할 때 마주하는 machine code의 형태 자체가 달라진다는 의미이기도 합니다. 다만 현재로서는 opt-in 상태의 infrastructure에 머물러 있습니다. 약 260줄 규모의 새로운 subsystem이 DFG bytecode-parsing 경로 한복판에 자리잡았는데, 이 경로는 역사적으로 JSC에서 버그가 자주 발견되어온 영역입니다.

코드 주석에는 이 방식과 기존 heuristic 사이의 divergence가 "decision quality에는 영향을 주지만 correctness는 절대 해치지 않는다"는 주장이 담겨 있습니다. 다만 이 주장은 veto-only라는 속성이 유지되는 동안에만 성립합니다. 좁게 보면, 옵션이 활성화된 이후에도 이 invariant가 그대로 유지되는지 확인할 필요가 있습니다. 구체적으로는, plan이 site별 heuristic이라면 거부했을 site를 오히려 승인하는 경로가 존재하는지, 아니면 heuristic이 승인했을 site를 거부하는 방향으로만 작동하는지를 살펴봐야 합니다. 이때 단서가 되는 지점은 site->admitted를 소비하는 코드 중 이후의 cost check를 추가로 수행하는 대신 아예 건너뛰는 곳입니다. 조금 더 넓게 보면, 새로 추가된 자료구조 자체도 memory-safety 관점에서 살펴볼 지점을 안고 있습니다. SegmentedVector<Site, 16> 내부를 가리키는 Site* 형태의 parent/child pointer가 있는데, reallocation이 발생할 때 이 pointer들이 invalidate되지는 않는지 확인이 필요합니다. 또한 identity 비교에만 쓰이고 dereference는 되지 않는 calleeIdentity pointer도 있는데, 해제된 뒤 재할당된 객체가 같은 주소를 다시 갖게 되는 상황을 무엇이 막아주는지 명확하지 않습니다. interior pointer가 growable container를 가리키는 이 형태는 DFG의 다른 side table들에서도 동일하게 나타나는지 검색해볼 가치가 있습니다. 가장 넓게 보면, survey pass와 parse pass가 서로 다른 코드로 동일한 predicate("이 call을 inline해야 하는가")를 각각 계산하는 구조 자체가 위의 PNG decoder fix에서 다뤘던 divergence class를 JIT 영역으로 옮겨온 형태에 해당합니다. "inline해야 하는가"를 판단하는 두 개의 서로 다른 구현은 시간이 지나면서 서서히 어긋나기 마련이며, survey가 갖는 call site 모델은 parser가 갖는 모델보다 필연적으로 더 거칠 수밖에 없습니다.