← All reports

`JSString` gains a per-cell atom bit for concurrent profiling

Component: JSC value profiling | 24f0a33

Source/JavaScriptCore/bytecode/SpeculatedType.h

+inline SpeculatedType speculationFromValueForProfiling(JSValue value)
+{
+ return speculationFromValueImpl<true>(value);
+}

JSC의 DFG/FTL compiler thread는 mutator와 동시에 실행되면서, ValueProfile bucket에 기록된 값을 살펴 runtime type을 추측합니다. JIT가 어떤 fast-path type check를 내보낼지는 이 정보를 근거로 결정됩니다. 그런데 JSString이 atom인지 판별하려면, 기존에는 내부의 StringImpl*을 따라가 StringImpl::isAtom()을 확인해야 했습니다. 여기서 atom은 property key로 사용되는 interned/hash-consed 문자열을 말합니다. 문제는 이 pointer를 기록하는 store가, cell을 compiler thread에 노출시키는 ValueProfile write에 대해 fence로 보호되지 않는다는 점입니다.

이번 commit에서는 JSString에 per-cell isDefinitelyAtom 비트가 추가되었습니다. 문자열이 atomize될 때마다 이 비트가 설정되며, swapToAtomStringJSRopeString::convertToNonRope에서 발생하는 in-place upgrade도 여기에 포함됩니다. 비트의 의미는 의도적으로 한쪽 방향으로만 정의되어 있습니다. 설정되어 있으면 atom이고, 비어 있으면 알 수 없다는 뜻입니다. 덕분에 cell 바깥으로 pointer를 따라가지 않고도 race 상황에서 그대로 읽을 수 있으며, 대신 가끔 과소 예측하는 비용을 감수하게 됩니다. speculationFromCell/speculationFromValue는 template으로 정리되었지만 기존의 dereference 동작은 그대로 유지합니다. 여기에 per-cell 비트만 읽는 speculationFromValueForProfiling이 새로 추가되었고, ValueProfileBase::computeUpdatedPrediction/computeUpdatedPredictionForExtraValue가 이를 사용하도록 변경되었습니다:

Before:                                            After:
Mutator thread                Compiler thread          Mutator thread                Compiler thread
  create JSString                                         create JSString
  store StringImpl* into cell                              store StringImpl* into cell
  write value into                                          write value into
  ValueProfile bucket                                       ValueProfile bucket
  (no fence vs. above store)  ── races with ──►             (no fence vs. above store)
                                 speculationFromValue()                                    speculationFromValueForProfiling()
                                   tryGetValueImpl()                                          reads cell->isDefinitelyAtom()
                                   dereferences StringImpl*                                    (per-cell bit, no ptr chase)
                                 [possible stale/                                            [worst case: stale 'false',
                                  unpublished ptr deref]                                       never a bad dereference]

JSString을 초기화하는 store들과 ValueProfile bucket에 대한 write 사이에는 순서 보장이 존재하지 않습니다. 그래서 compiler thread가 StringImpl*이 게시되기 이전 상태의 cell을 관찰하고, 그 pointer를 dereference하는 상황이 이론적으로 가능합니다. profiling 경로를 cell 내부 비트만 읽도록 제한하면 이 dereference 자체가 사라집니다. 예측이 false negative로 빠질 여지를 감수하는 대신, crash 가능성을 제거하는 맞교환인 셈입니다.

이번 변경은 JSC의 concurrent JIT type-profiling 경로에 해당합니다. 이 경로에서의 memory ordering 실수는 compiler thread에서의 type confusion이나 wild pointer dereference로 이어집니다. 좁게 보면, markAsAtom의 평범한 setPerCellBit(true)swapToAtomString/convertToNonRope에 선행하는 storeStoreFence 조합이 지원 대상 아키텍처 전부에서 충분한지 확인해 볼 만합니다. 이때 눈여겨볼 신호는 publication 패턴입니다. fence가 두 store의 순서만 잡아주고, 정작 consumer 쪽의 flag load에는 대응되는 acquire가 없는 형태를 찾으면 됩니다. 그다음으로는 avoidStringDereference=false로 호출되는 지점, 즉 speculationFromCell/speculationFromValue가 실제로 동일한 profiling race로부터 안전한 문맥에만 한정되어 있는지 점검해야 합니다. 범위를 넓히면, 재사용 가능한 패턴은 이렇습니다. fence 없이 publish된 객체에 도달한 concurrent reader가 그 객체 바깥으로 pointer를 따라가는 경우입니다. 다른 ValueProfile/ArrayProfile consumer 중에서도, 이번 패치가 atom 여부에 한정해 제공한 publication 보장 없이 cell 내부 pointer를 여전히 dereference하는 곳이 남아 있는지 살펴볼 필요가 있습니다. 일반화해서 들고 다닐 질문은 이것입니다. compiler thread가 mutator에서 생성된 cell로부터 어떤 필드를 읽을 때, 그 필드의 초기화와 cell을 보이게 만든 store 사이의 순서는 무엇이 보장하는가?