[JSC] Allocate property storage of arrays without C++ operation call
Component: JSC JIT | fa3737d
JSTests/microbenchmarks/put-by-id-transition-on-array.js
JSC 객체의 butterfly는 음수 offset 쪽에 out-of-line property를 저장합니다. 배열이라면 양수 offset 쪽에 indexing header(publicLength, vectorLength)가 놓이고, 그 뒤에 element vector가 이어집니다. put 연산으로 property를 추가할 때 out-of-line capacity가 이미 가득 차 있으면 butterfly를 다시 할당해야 합니다. 이때 header와 element가 새 영역으로 복사되고, StructureID도 갱신됩니다. 지금까지는 모든 JIT tier가 이 과정을 C++ operation에 맡겨 왔습니다. 즉 크기 계산, 복사, hole 값 처리, GC 대응이 모두 해당 operation 안에서 처리되었습니다.
이번 commit으로 put-transition IC stub, DFG, FTL이 확장된 butterfly를 inline으로 allocate하도록 변경되었습니다. 크기는 vectorLength를 기준으로 계산되며, header와 vector는 JIT이 생성한 loop로 복사됩니다. 대상은 Undecided, Int32, Double, Contiguous 배열입니다. 반면 ArrayStorage와 typed array는 여전히 operation을 호출합니다. 한편 AccessCase::doesCalls()는 더 이상 이러한 transition을 call로 집계하지 않도록 수정되었습니다.
Put transition (array), after:
└─► check structure
├─ Undecided/Int32/Double/Contiguous:
│ ├─ size = f(vectorLength, newOOLCapacity)
│ ├─ inline allocate (slow path on fail)
│ ├─ loop-copy header + vector
│ └─ store butterfly, then StructureID
└─ ArrayStorage / typed arrays: call operation (unchanged)
Significance
새로 생성한 배열에 named property를 추가하는 동작이 1.20-1.25배 빨라졌습니다. dart-sass 같은 dart2js 출력물에서 흔히 나타나는 패턴입니다. 다만 그 대가로, security-critical한 연산의 allocation 및 copy codegen이 세 곳에 각각 직접 작성되었습니다.
Audit directions
- 크기 및 복사 계산.
vectorLength와 새 out-of-line capacity로 크기를 계산하는 과정에서 overflow가 없는지 확인해야 합니다. preCapacity와 unused capacity가 올바르게 처리되는지도 점검이 필요합니다. 네 가지 indexing type 모두에서 C++ operation과 동작이 일치하는지 역시 살펴봐야 합니다. 복사 횟수와 방향은 빈 배열, holey 배열, 큰 배열(1200개 이상), copy-on-write 배열 각각에 대해 확인할 필요가 있습니다. - GC가 관찰하는 내용. hole 표현(빈
JSValue, Double의 경우 PNaN)이 정확해야 합니다. 또한publicLength와vectorLength사이의 초기화되지 않은 tail slot도 GC와 이후의 read 입장에서 유효한 값이어야 합니다. - 순서와 call 집계. 먼저 butterfly store와
StructureIDstore 사이의 순서 및 fence를 검토해야 합니다. allocation slow path에서 register save/restore가 정상적으로 수행되는지, stub 실행 중 GC가 발생하는 경우는 어떤지도 살펴볼 필요가 있습니다.doesCalls()변경 역시 점검 대상입니다. call이 없는 것으로 취급된 stub이 실제로는 slow path call에 도달한다면, register 보존이나 exception, GC 상태가 잘못 처리될 가능성이 있습니다. - Tier 간 불일치. 어느 tier에서든 structure check와 inline allocation 사이에 indexing type이 바뀔 수 있는지 확인해야 합니다. 이와 함께 DFG, FTL, IC lowering을 서로 비교해 볼 필요가 있습니다. 같은 로직이 세 벌로 나뉘어 서로 어긋나는 경우는 전형적인 버그 원인이기 때문입니다.