[JSC] Unify JS Allocators
Component: JavaScriptCore Heap | aa19dc6
Source/JavaScriptCore/heap/BlockDirectory.cpp
Source/JavaScriptCore/heap/AlignedMemoryAllocator.cpp
JSC의 heap은 cell을 타입별로 나누어 subspace(JSObject, Structure, JSString 등)에 배치합니다. 각 subspace에는 16KB 단위의 MarkedBlock을 관리하는 BlockDirectory가 하나씩 붙습니다. 두 directory 사이에서 빈 block을 steal할 수 있는지는 같은 AlignedMemoryAllocator를 공유하는지에 따라 결정됩니다. 하나의 allocator에 묶인 directory끼리의 steal은 Options::stealEmptyBlocksFromOtherAllocators 뒤에 이미 구현되어 있었습니다. 다만 기존에는 IsoSubspace마다 전용 allocator를 따로 소유했기 때문에, IsoSubspace 사이에서는 steal이 성립하지 않았습니다. 이번 commit에서는 IsoSubspace::m_allocator가 제거되었습니다. 대신 subspace를 메모리 영역과 alignment 클래스 기준으로 묶습니다. fastMalloc 기반, primitive gigacage, 그리고 새로 추가된 공용 Structure allocator의 세 갈래입니다. 이와 함께 subspace를 순회하던 기존 bookkeeping(Subspace::findEmptyBlockToSteal, registerDirectory/registerSubspace)도 정리되었습니다. 그 자리는 현재 빈 block을 보유한 directory들을 lock으로 보호되는 linked list로 관리하는 방식이 대신합니다. steal 범위가 넓어진 만큼 안전장치도 함께 들어갔습니다. 기존 경로는 ~inUseBits만 마스킹했는데, 여기에 block 단위 제외 조건 두 가지가 추가되었습니다. 먼저 stealableBits()(emptyBits & ~destructibleBits & ~inUseBits)는 아직 destructor 처리가 남아 있는 block을 건너뜁니다. 또 새 탐색 루프 안에서는 weakSet().head()가 non-null인 block을 건너뛰도록 검사합니다.
Before (private allocator per IsoSubspace):
IsoSubspace A IsoSubspace B
private AlignedMemoryAllocator private AlignedMemoryAllocator
BlockDirectory A BlockDirectory B
(no shared allocator => A and B can never steal from each other)
After (allocators shared by memory region):
Shared AlignedMemoryAllocator (e.g. structureAllocator)
BlockDirectory A <──steal──> BlockDirectory B
still excluded per-block: destructor-owing blocks, blocks with a live WeakSet
Significance
allocator가 통합되면서 빈 MarkedBlock이 이동할 수 있는 cell 타입 subspace의 범위가 이전보다 크게 넓어집니다. JS heap grooming 기법이 주목하는 block 재사용 경로가 그만큼 확장된 셈입니다. 새로 추가된 두 제외 조건이 없었다면, 어느 쪽 block을 넘기든 한 cell 타입의 destructor 비용이나 WeakBlock sweep 비용이 다른 타입의 allocation에 전가되었을 것입니다.
Audit directions
JSC collector에서 heap block 재사용을 지탱하던 핵심 invariant가 다시 배선된 변경입니다. 그만큼 들여다볼 가치가 높은 지점입니다. 좁게 보면, findEmptyBlockToSteal과 noteBlockMayBeStealable을 먼저 점검할 필요가 있습니다. m_isOnEmptyBlocksList에 대한 relaxed atomic fast path 검사와 lock으로 보호되는 리스트 조작 사이에 race가 존재하는지 확인해야 합니다. 또한 검사 시점과 실제 steal 시점 사이에 block의 stealability가 달라질 수 있는지도 살펴볼 지점입니다. WeakSet이 비게 되는 경우나 destructor 플래그가 stale해지는 경우가 여기에 해당합니다. edge case를 하나라도 놓치면 destructor 작업이 남아 있거나 살아 있는 WeakImpl을 붙들고 있는 block이 steal될 수 있습니다. 그 결과 use-after-free나 서로 무관한 cell 타입 사이의 type confusion으로 이어질 가능성이 있습니다. 더 넓게 보면, 어떤 directory들이 allocator를 공유하는지를 전제로 동작하던 코드는 이제 훨씬 큰 equivalence class를 다루게 됩니다. alignedMemoryAllocator()를 사용하는 나머지 호출 지점과 Options::stealEmptyBlocksFromOtherAllocators gate를 다시 확인해 볼 만합니다. allocator를 공유한다는 사실이 곧 연관된 cell 타입임을 뜻한다는 가정이 남아 있는지가 관건입니다. 가장 넓게 보면, 이 변경은 "서로 분리되어 있던 equivalence class를 합치는 pooling 최적화"라는 패턴에 해당합니다. 분리 구조 덕분에 우연히 지켜지던 invariant는 이제 명시적인 제외 검사로 바뀌며, 그 검사는 빠짐없이 완결되어야 합니다. 코드 리뷰에서 눈여겨볼 신호는 이런 형태입니다. 여러 bitvector mask를 조합한 새 *Bits() accessor가 등장하는데, 같은 diff에서 그 mask가 적용되는 대상 범위까지 함께 넓어지는 경우입니다.