[JSC][Wasm] Inline table.get for funcref
Component: JSC WebAssembly JIT | 6a359a0
Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp
Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
WebAssembly Table은 funcref 요소를 담을 수 있는데, 이 요소들은 JS 쪽에 일반 함수 객체로 노출됩니다. 이때 JSC는 각 function reference가 처음 관찰되는 시점에 JS wrapper 객체를 lazy하게 생성한 뒤 캐싱합니다. 패치 이전에는 funcref table에 대한 table.get이 예외 없이 C++ runtime(operationGetWasmTableElement)을 호출했습니다. bounds check, 캐시된 wrapper 조회, 없을 경우의 생성이 모두 이 호출 안에서 처리되었습니다.
이번 commit은 funcref 경로를 BBQ baseline(64-bit path)과 OMG optimizing JIT 양쪽 tier에 inline으로 전개합니다. 먼저 생성된 코드가 인덱스를 table 길이와 비교하는 bounds check를 inline으로 수행하고, 실패하면 곧바로 trap이 발생합니다. 이어서 table의 wrapper 배열에서 캐시된 wrapper를 inline으로 로드하게 됩니다. C++ 호출로 분기하는 경우는 해당 slot이 0으로 읽힐 때뿐입니다. 즉 wrapper가 아직 생성되지 않았거나, 요소가 실제로 null funcref인 상황입니다. 여기에 더해 WasmFuncRefTable_wrappers라는 B3 abstract heap이 새로 추가되었습니다. 컴파일러의 alias analysis가 wrapper 배열에 대한 load와 store를 판단할 수 있도록 하기 위해서입니다.
Significance
funcref table 요소를 읽는 유일한 경로에서 필수였던 C++ runtime 호출이 제거되면서, 두 Wasm JIT tier 모두에서 table.get이 눈에 띄게 빨라집니다. OMG 경로에서는 fast block과 rare로 표시된 slow block 위에 Phi를 명시적으로 구성합니다. 그 결과 slow call이 hot path의 critical dependency chain에서 빠지게 됩니다.
Audit directions
앞으로 눈여겨볼 패턴은, bounds check가 적용된 GC 관리 대상 메모리 읽기가 안전한 C++ runtime 호출에서 빠져나와 두 tier의 손으로 생성한 machine code로 옮겨졌다는 점입니다. Narrow: inline으로 들어간 table length check가 table.grow를 기준으로 stale해질 수 있는지 살펴볼 필요가 있습니다. table.grow는 backing store와 wrapper 배열을 모두 재할당합니다. grow를 사이에 두고 inline load가 stale base pointer나 stale length를 들고 있는 형태가 이 종류의 전형적인 버그이며, 두 tier의 동작도 서로 일치해야 합니다. 또한 inline 경로가 "wrapper가 아직 캐싱되지 않은 상태"와 정상적인 null funcref를 어떻게 구분하는지도 확인해야 합니다. fast path는 로드한 값을 0과 비교하므로, sentinel 선택이 잘못되면 초기화되지 않았거나 쓰레기 값인 JSValue가 함수 객체처럼 script에 노출될 가능성이 있습니다. Wider: 새로 추가된 WasmFuncRefTable_wrappers abstract heap은 컴파일러가 inline load를 table.set이나 table.grow, 혹은 wrapper 캐시를 GC로부터 보호하는 write barrier 너머로 재배치하지 못하게 막는 역할을 합니다. 새로 도입된 heap range를 다룰 때와 동일한 기준으로 clobber 선언을 점검할 필요가 있습니다. 여기에 더해 아직 runtime 호출을 거치는 나머지 Wasm table 및 memory 연산들도 훑어보면서, 같은 inlining이 예정되어 있는지 확인해 볼 만합니다. Widest: runtime 호출을 inline code로 대체하는 JIT 변경은, 해당 runtime 함수가 암묵적으로 강제하던 invariant를 전부 떠안게 됩니다. C++ 함수가 검사하던 항목을 하나씩 열거한 뒤, 각 검사에 대응하는 inline 검사가 있는지 아니면 불필요함이 입증된 근거가 있는지 확인해야 합니다.