[JSC] Inline allocation for `Promise.resolve()` of a non-thenable object
Component: JSC | b1a6241
DFGConstantFoldingPhase는 condition-set watchpoint를 이용해 컴파일 타임에 속성을 증명합니다. 여기서는 객체의 prototype chain에 then 속성이 없다는 사실을 증명하여, 해당 객체가 non-thenable임을 확정합니다. Clobberize pass는 각 IR node가 어떤 heap location을 읽고 쓰는지를 판단하며, 이 정보는 alias analysis와 store elimination의 입력이 됩니다. clobberTop()은 최대한 보수적인 모델로 어떤 heap location이든 변경될 수 있다고 가정하는 반면, HeapObjectCount는 allocation이라는 side effect 하나만을 나타냅니다.
JSTests/stress/dfg-promise-resolve-fulfilled-object.js
이 commit은 313220@main에서 유입된 DFG/FTL regression을 수정합니다. NewResolvedPromise node에 isResolvedValueKnownNonThenable flag가 추가되었습니다. 이를 통해 constant-folding으로 non-thenable임이 증명된 object 인자는, clobberTop()을 동반하는 operationNewResolvedPromise C call로 fallback하는 대신 inline promise allocation 경로를 타게 됩니다. 결과적으로 throughput이 약 1.53배 회복됩니다.
Before (313220@main regression):
ConstantFoldingPhase
└─ obj proven non-thenable via watchpoint
└─► NewResolvedPromise(obj)
└─► object arg detected
└─► operationNewResolvedPromise() ← C call
clobberTop() ← every heap loc may change
After (this commit):
ConstantFoldingPhase
└─ obj proven non-thenable via watchpoint
└─► NewResolvedPromise(obj, isKnownNonThenable=true)
└─► flag set
└─► inline allocate fulfilled promise ← no C call
HeapObjectCount ← only alloc side effect
Significance
Heap을 clobber하던 C call 경로가 순수한 inline allocation으로 승격되고, 이 node에 대해 모델링된 side effect는 clobberTop()에서 HeapObjectCount로 좁혀집니다. Alias analysis, store elimination, LICM, GVN 모두 이제 이 좁혀진 모델을 기준으로 이 node를 판단하게 됩니다.
Audit directions
좁게 보면, 이 flag는 watchpoint로 보장된 가정을 근거로 컴파일된 node에 그대로 박혀 들어갑니다. ConstantFoldingPhase가 flag를 설정한 이후, deoptimization이 완전히 적용되기 전에 prototype chain mutation이 watchpoint를 발동시키는 경우를 생각할 수 있습니다. 이 경우 inline allocation 경로는 thenable 여부를 다시 검증하지 않은 채로 실행됩니다. 전형적인 deopt-boundary race에 해당하는 지점입니다. Stress test의 resolveProto 케이스가 정확히 이 표면 위에 있으며, 이는 V8과 SpiderMonkey에서 과거 exploitable JIT bug를 낳았던 표면과 동일합니다.
넓게 보면, clobberize 완화 자체가 재사용 가능한 audit target입니다. Node의 effect model을 clobberTop()에서 특정 값으로 좁히는 모든 commit은, 해당 node가 도달 가능한 모든 경로에서 오직 그 좁힌 모델이 기술하는 동작만 수행한다고 주장하는 셈입니다. 만약 inline allocation 경로가 allocation 이상의 동작을 할 수 있다면, 예를 들어 finalizer를 실행하거나 weak reference를 건드리거나 GC callback을 유발할 수 있다면, 이 모델링은 틀린 것이고 그 위에 구축된 모든 하위 pass가 이 오류를 그대로 물려받습니다. 최근 clobberize.h diff들을 살펴, effect model이 좁혀진 케이스를 찾아 그 node가 취할 수 있는 모든 경로, 특히 slow-path로의 전환까지 포함해 검증할 필요가 있습니다. 여기서 눈여겨봐야 할 징후는 clobberTop()을 제거하면서 명시적인 escape나 slow-path guard를 함께 추가하지 않은 diff입니다. 셋째로, abstract interpreter의 변경은 하위로 다른 inferred type을 전파하게 되는데, 여기서 전파가 잘못되면 그래프 다른 부분의 speculation까지 unsound해질 수 있습니다.