자바스크립트와 WASM에서 ECS, OOP, Sweep & Prune, 공간 트리의 성능을 비교한 물리 시뮬레이션 벤치마크와 메모리 지역성에 대한 분석.
자주 접근하는 데이터의 메모리 지역성이 높을 때 많은 알고리즘, 예를 들어 정렬 같은 것들이 상당히 개선된다는 이야기를 들어왔습니다. 예를 들어 CPU가 처리에 필요한 관련 정보를 RAM을 치는 대신 자신의 L1/L2 캐시에 모두 올려둘 수 있는 경우입니다. 이를 다루는 일반적인 방법 중 하나가 Entity Component System (ECS) 소프트웨어 아키텍처를 사용하는 것입니다. 하지만 인터프리터 언어나 OOP 언어(Java, Python, Javscript)에서는 이런 메모리 배치를 달성하기가 어려울 수 있습니다. 이런 언어들은 대개 개발자에게 객체의 메모리 위치에 대한 제어권을 그만큼 많이 주지 않기 때문입니다. 제 질문은 이것입니다.
Javascript에서 ECS 스타일 아키텍처를 사용하는 것이 가능할까요? 그리고 해당되는 연산에서 실제로 객체 + V8의 가비지 컬렉션보다 더 나을까요?
이를 테스트하기 위해, 저는 충분히 복잡한 벤치마크를 만들었습니다. 바로… “상자 안에서 튀어다니는 공들”입니다! 이것은 표준적인 2차원 물리 엔진 기법을 따릅니다.
그리고 물론, 저는 너무 신이 나서 아래에 엄청나게 많은 벤치마크 종류를 만들고 말았습니다. 한번 해보세요!
게임 개발을 조금이라도 해봤다면, 아마 Entity Component System (ECS) 을 들어본 적이 있을 것입니다. OOP가 행 기반 데이터베이스(각 객체가 모든 속성을 하나의 메모리 덩어리에 저장하는 구조)라면, ECS는 열 기반 데이터베이스로, 각 속성(또는 속성들의 집합)을 별도의 열 배열에 저장합니다.
다르게 말하면(그리고 ECS에 대한 더 좋은 설명은 다른 곳에서 훨씬 잘 찾을 수 있겠지만)… MovingObject를 상속받고 그것이 다시 PhysicalObject를 상속받는 GameEntity 같은 복잡한 클래스 계층을 만드는 대신, ECS는 모든 것을 세 가지 구별된 개념으로 분해합니다.
Position, Velocity, Color). 로직은 포함하지 않습니다. 각 엔티티는 0개부터 전부까지 동적인 컴포넌트 집합을 가질 수 있습니다.저는 너무 과하게 다섯 가지 차원으로 테스트했습니다.
Array.sort()를 테스트했습니다.아무튼, 어떻게 동작하는지 봅시다! 직접 이것저것 만져보고 싶다면 GitHub 에 호스팅되어 있습니다.
이 모든 구성을 제 Apple M4 Pro chip 에서 로컬로 실행하면(전원 연결 상태, 200 프레임) 숫자가 엄청 많이 나옵니다. 전체 6회 실행의 전체 데이터셋이 보고 싶다면 아래의 Appendix: Full Benchmark Dataset 를 확인해보세요.
정보 과부하를 줄이기 위해, 세 가지 주요 가설에 집중해봅시다.
15,000개의 wandering 엔티티에서, 평면 배열 기반 Sweep & Prune (WASM ECS S&P Quick) 는 1.81 ms 로 실행되며(기준선 대비 9.02배 향상), 평면 배열 기반 공간 트리(ECS Tree) 는 9.97 ms 로 실행됩니다(단지 1.64배 향상).
| Broad-phase System | Architecture | Avg Frame Time | 99th Percentile | Speedup vs OOP Tree |
|---|---|---|---|---|
| WASM ECS S&P (Quick) | Flat 1D Array | 1.809 ms | 2.031 ms | 9.02x |
| ECS (Custom SoA) S&P (Quick) | Flat 1D Array | 4.668 ms | 6.111 ms | 3.50x |
| ECS Tree | 2D Spatial BVH | 9.970 ms | 43.666 ms | 1.64x |
| OOP Tree | 2D Spatial BVH | 16.321 ms | 40.756 ms | 1.00x |
평면 배열을 사용하고 JavaScript 힙 객체도 피하는데도, 왜 트리는 이렇게 더 느릴까요?
posX[indices[i]]). 하지만 AABB 트리는 중첩된 간접 참조를 요구합니다. 순회 중 CPU는 자식 인덱스를 조회해야 하고(treeLeft[idx]), 그다음 그 자식을 사용해 노드의 경계를 다시 조회해야 합니다(treeMinX[leftChild]). 배열들 사이를 이런 비선형 방식으로 점프하면 CPU의 하드웨어 프리페처가 다음 메모리 주소를 예측할 수 없습니다.indices 는 거의 연속된 상태를 유지합니다. 덕분에 인덱스 조회가 캐시 안에서 매우 국소적으로 이뤄집니다. 반면 트리 순회는 언제나 멀리 떨어진 노드들 사이를 점프합니다.순수 JavaScript에서 OOP(객체 배열)에서 ECS(평면 TypedArrays)로 단순히 옮기는 것만으로도 공간 응집성에 따라 1.58배에서 24.9배까지의 속도 향상이 나옵니다.
| Runtime & Memory Layout | Contender | Avg Frame Time | Speedup vs OOP |
|---|---|---|---|
| WebAssembly (SoA) | WASM ECS S&P (Quick) | 2.141 ms | 3.91x |
| JavaScript (SoA) | ECS (Custom SoA) S&P (Quick) | 5.318 ms | 1.58x |
| JavaScript (AoS) | OOP S&P (Quick) | 8.380 ms | 1.00x |
AssemblyScript WASM으로 옮기면 추가로 약 2.5배의 속도 향상이 제공됩니다(2.11 ms까지 감소). 이 향상의 주된 원인은 unchecked() 연산자를 사용할 수 있다는 점입니다. 이것은 컴파일러에게 배열 경계 검사를 건너뛰라고 알려주며, 컴파일러가 고도로 최적화된 벡터 명령을 생성하고 CPU 명령 처리량을 최대화할 수 있게 해줍니다.
이걸 만들면서 꽤 흥미로운 것들을 많이 발견했습니다!
Wandering (높은 응집성)에서는 배열이 거의 정렬된 상태를 유지하므로 Insertion Sort가 매우 빠릅니다(4.21 ms). 하지만 Erratic (낮은 응집성) 작업 부하(예: 폭발이나 순간이동)에서는 배열이 완전히 정렬되지 않은 상태가 됩니다. 그러면 Insertion Sort는 이차 시간 최악 사례($O \left(\right. N^{2} \left.\right)$)로 무너지고, 프레임 시간이 36.39 ms 까지 불어나며 심각한 끊김을 유발합니다.
이를 해결하기 위해, 우리는 Hybrid Sort 를 구현했습니다(Quicksort 또는 Mergesort가 하위 배열이 12개 요소 미만이 되면 Insertion Sort로 전환). 이렇게 하면 작은 분할에서 재귀 스택 오버헤드를 없애면서도, 혼란스러운 움직임 아래에서 $O \left(\right. N log N \left.\right)$ 성능을 보장합니다(5.60 ms).
왜 하위 배열이 12개 요소 미만이 되면 insertion sort로 전환할까요? 아주 작은 배열에서는 분할 정복 재귀의 상대적으로 큰 호출 스택 및 버퍼 오버헤드를 제거할 수 있기 때문입니다. 이런 경우 insertion sort는 준비 비용이 거의 0에 가까워 매우 빠릅니다. 실제로 이렇게 바꾸니 성능이 좋아졌습니다! (다만 얼마나였는지는 잊었습니다)
15,000개의 erratic 엔티티에서 bitECS S&P (Quick) 는 9.56 ms 로 실행되며, 우리의 커스텀 ECS는 5.60 ms 입니다. 약 2배 느리지만, 여전히 OOP 기준선(131.6 ms) 대비 엄청난 14배 속도 향상을 제공합니다.
왜 bitECS가 더 느릴까요?
y, w, h 를 하나의 posYwh 배열에 패킹합니다(posYwh[i * 3 + 0]). 즉 세 값 모두 하나의 CPU 캐시 라인에 로드됩니다. 기본적으로 bitECS 는 컴포넌트의 각 속성마다 별도의 TypedArray 를 할당합니다(y, w, h 가 각각 별도 배열). 그래서 CPU는 세 개의 분리된 메모리 스트림에서 가져와야 합니다.하지만 실제 게임에서는 bitECS 같은 라이브러리의 사용성, API 안전성, 동적 엔티티 관리가 몇 밀리초의 미세 최적화보다 훨씬 더 가치 있습니다.
게다가 아래의 성능 최적화 팁 중 하나를 적용하고, 물리 데이터를 연산 전에 커스텀 구조로 추출하면 이런 차이는 전부 사라질 수도 있습니다.
15,000개 엔티티에서, 우리 ECS (Custom SoA)의 Float32Array 구현은 Float64Array 버전보다 약 5% 느렸습니다 (5.60 ms 대 5.32 ms).
왜 그럴까요? JavaScript 숫자는 항상 64비트 배정밀도입니다. V8이 Float32Array 에서 값을 읽을 때는, 수학 연산을 수행하기 전에 런타임에서 32비트 부동소수점을 64비트 배정밀도로 변환해야 합니다. 다시 쓸 때는 반대로 다시 변환해야 합니다. 이런 런타임 변환 오버헤드가 L1/L2 캐시 밀도 향상 이득을 상쇄해버립니다.
네이티브 환경(WASM, C++, Rust 등)에서는 f32 가 변환 없이 FPU 레지스터에서 직접 처리되므로, 순수한 성능 향상이 됩니다.
트리와 정렬 기반 스윕은 서로 다른 순서로 접촉을 순회하고 해결하므로, 반올림 오차가 다르게 누적됩니다. 이 때문에 시뮬레이션 경로는 거의 즉시(2프레임쯤부터) 갈라집니다. 서로 다른 패러다임 간 궤적을 정확히 일치시키는 것보다, 통계적 정확성과 국소적 안정성에 집중하는 편이 낫습니다.
벤치마크의 99번째 백분위수 숫자를 보면, 눈에 띄는 패턴이 나타납니다. OOP 구현에서는 99번째 백분위수가 평균 프레임 시간보다 종종 2배에서 3배 더 높습니다. 반면 ECS (Custom SoA)와 WASM 구현에서는 99번째 백분위수가 평균과 거의 동일합니다.
OOP에서는 엔티티들이 힙 전체에 흩어져 있습니다. 이들이 움직이고 상호작용하면서 JavaScript 엔진의 가비지 컬렉터가 계속 작동하고, CPU는 포인터 조회를 기다리며 자주 멈춥니다. 이로 인해 간헐적인 프레임 드롭(미세한 끊김)이 발생합니다. ECS는 미리 할당된 평면 TypedArrays를 사용하므로 메모리 접근이 100% 예측 가능하고 GC 오버헤드가 0이어서, 완벽하게 매끄러운 프레임 전달을 보장합니다.
질문: 우리가 생각해낼 수 있는, 합리적으로 설계된 코드 중 가장 빠른 것은 무엇일까요?
판정: 최상위 공간 트리들(WASM Tree, JS ECS Tree)과 Sweep & Prune 챔피언들(WASM Quick S&P, JS Quick S&P)을 15,000개의 혼란스럽게 충돌하는 물체에 대해 맞붙여보면:
| Contender | Paradigm | Architecture | Runtime (Erratic) | Verdict |
|---|---|---|---|---|
| WASM Quick S&P | ECS | 1D Flat Array | 2.14 ms | 🏆 Winner (~7.9x faster than WASM Tree) |
| JS Quick S&P | ECS | 1D Flat Array | 5.32 ms | Best pure Javascript engine |
| WASM Tree | ECS | 2D Spatial BVH | 16.98 ms | Peak spatial tree performance |
| JS ECS Tree | ECS | 2D Spatial BVH | 16.64 ms | JS spatial tree baseline |
bitECS 같은 실전용 ECS 라이브러리를 사용해도 OOP 대비 엄청난 14배 속도 향상을 얻으면서 깔끔하고 확장 가능한 API를 제공받을 수 있습니다. 제 생각에는, 애플리케이션의 99%에서는 라이브러리를 사용하는 것이 올바른 엔지니어링 선택입니다.이 섹션은 상용급 게임 엔진을 위해 S&P broadphase를 더 최적화하는 방법을 설명합니다.
우리의 단순한 ECS S&P에서는 스윕 단계가 좌표를 간접적으로 읽어야 합니다: posX[indices[i]]. 이를 연속적으로 만들기 위해 모든 컴포넌트 배열을 실제로 정렬할 수도 있겠지만, 실제 게임에서는 아키타입 단편화와 물리와 무관한 컴포넌트(예: 인벤토리나 AI 상태)까지 재배열해야 하는 낭비 때문에 매우 비실용적입니다.
대신 Extract-Transform-Load (ETL) Physics Buffer 패턴을 사용할 수 있습니다.
x, y, w, h, id)만 전용의 평면 PhysicsBuffer 로 복사합니다.indices 를 정렬한 다음, 이 가벼운 PhysicsBuffer 만 실제로 재배열합니다.PhysicsBuffer 를 순차적으로 읽을 수 있습니다(posX[indices[i]] 대신 posX[i]).왜 이것이 매우 실용적인가:
.set() 또는 단순 루프 사용)는 매우 빠릅니다. 현대 CPU는 순차 메모리 복사에 매우 최적화되어 있어서, 프리페처가 잘 작동하는 가운데 메모리 대역폭을 쉽게 가득 채웁니다.우리는 Sweep & Prune를 Dynamic AABB Tree와 비교했지만, 또 다른 매우 캐시 친화적인 대안은 Spatial Hash Grid 입니다.
우리의 단순한 Sweep & Prune는 X축만 기준으로 정렬합니다. 많은 엔티티가 수직으로 정렬되면(예: 좁은 통로를 따라 아래로 떨어지는 경우), 이들은 모두 X 투영에서 겹치게 되어 스윕 단계가 $O \left(\right. N^{2} \left.\right)$ 에 가까워집니다.
PhysicsBuffer 가 좌표를 연속적으로 저장하고 있으므로, 한 번에 네 개 엔티티의 경계를 128비트 SIMD 레지스터에 로드할 수 있습니다. 그러면 CPU는 단일 명령으로 AABB 겹침 검사를 병렬 수행할 수 있어, 스윕 단계 처리량을 잠재적으로 네 배까지 늘릴 수 있습니다.
제가 무엇을 놓치고 있을까요? 어떤 다른 구성을 추가해야 할까요? 제가 뭔가 잘못한 걸까요?
unchecked() 를 사용해 포인터 별칭화 보호를 쉽게 비활성화할 수 있었는데, C++와 clang에서는 이게 조금 더 어려웠습니다. 물론 불가능한 것은 아니었습니다.통계 매니아와 성능 순수주의자를 위해, 여기 200 프레임 동안 등록된 18개 시뮬레이터 구성 전체에 걸친, 가감 없는 전체 데이터셋을 제공합니다.
1. Static 동작 (절대적 응집성 - 5,000 Entities)
| System | Avg Frame Time | 99th Percentile | Speedup | |
|---|---|---|---|---|
| WASM ECS S&P (Insertion) | 0.148 ms | 0.178 ms | 4.43x | |
| WASM ECS S&P (Merge) | 0.182 ms | 0.226 ms | 3.60x | |
| WASM ECS S&P (Quick) | 0.202 ms | 0.243 ms | 3.24x | |
| ECS (Custom SoA) S&P (Insertion) | 0.434 ms | 0.484 ms | 1.51x | |
| ECS (Custom SoA) S&P (Quick) | 0.445 ms | 0.525 ms | 1.47x | |
| ECS (Custom SoA) S&P (Merge) | 0.445 ms | 0.507 ms | 1.47x | |
| ECS (Custom SoA) S&P (Native) | 0.601 ms | 0.807 ms | 1.09x | |
| OOP S&P (Native) | 0.604 ms | 0.821 ms | 1.08x | |
| OOP S&P (Insertion) | 0.654 ms | 0.912 ms | 1.00x | |
| OOP S&P (Quick) | 0.706 ms | 1.067 ms | 0.93x | |
| OOP S&P (Merge) | 0.720 ms | 1.044 ms | 0.91x | |
| WASM Tree | 0.742 ms | 0.859 ms | 0.88x | |
| ECS Tree | 0.887 ms | 1.052 ms | 0.74x | |
| bitECS S&P (Insertion) | 0.888 ms | 0.967 ms | 0.74x | |
| bitECS S&P (Quick) | 0.957 ms | 1.067 ms | 0.68x | |
| bitECS S&P (Merge) | 1.066 ms | 2.337 ms | 0.61x | |
| OOP Tree | 1.104 ms | 1.782 ms | 0.59x | |
| bitECS S&P (Native) | 1.351 ms | 3.841 ms | 0.48x |
2. Wandering 동작 (높은 응집성 - 5,000 Entities)
| System | Avg Frame Time | 99th Percentile | Speedup | |
|---|---|---|---|---|
| WASM ECS S&P (Insertion) | 0.318 ms | 0.355 ms | 3.54x | |
| WASM ECS S&P (Merge) | 0.343 ms | 0.483 ms | 3.28x | |
| WASM ECS S&P (Quick) | 0.374 ms | 0.472 ms | 3.01x | |
| ECS (Custom SoA) S&P (Insertion) | 0.845 ms | 0.963 ms | 1.33x | |
| ECS (Custom SoA) S&P (Merge) | 0.856 ms | 1.090 ms | 1.31x | |
| ECS (Custom SoA) S&P (Quick) | 0.858 ms | 0.994 ms | 1.31x | |
| ECS (Custom SoA) S&P (Native) | 1.059 ms | 1.723 ms | 1.06x | |
| OOP S&P (Insertion) | 1.124 ms | 1.676 ms | 1.00x | |
| OOP S&P (Quick) | 1.201 ms | 1.769 ms | 0.94x | |
| OOP S&P (Merge) | 1.325 ms | 1.885 ms | 0.85x | |
| OOP S&P (Native) | 1.379 ms | 2.104 ms | 0.81x | |
| bitECS S&P (Insertion) | 1.588 ms | 2.603 ms | 0.71x | |
| bitECS S&P (Quick) | 1.662 ms | 2.384 ms | 0.68x | |
| bitECS S&P (Merge) | 1.773 ms | 2.556 ms | 0.63x | |
| bitECS S&P (Native) | 1.829 ms | 2.673 ms | 0.61x | |
| WASM Tree | 2.233 ms | 2.614 ms | 0.50x | |
| ECS Tree | 2.353 ms | 2.941 ms | 0.48x | |
| OOP Tree | 2.938 ms | 11.642 ms | 0.38x |
3. Erratic 동작 (낮은 응집성 - 5,000 Entities)
| System | Avg Frame Time | 99th Percentile | Speedup | |
|---|---|---|---|---|
| WASM ECS S&P (Quick) | 0.462 ms | 0.719 ms | 28.71x | |
| WASM ECS S&P (Merge) | 0.475 ms | 0.740 ms | 27.91x | |
| ECS (Custom SoA) S&P (Quick) | 1.070 ms | 1.605 ms | 12.39x | |
| ECS (Custom SoA) S&P (Merge) | 1.125 ms | 1.637 ms | 11.79x | |
| ECS (Custom SoA) S&P (Native) | 1.487 ms | 2.149 ms | 8.92x | |
| OOP S&P (Quick) | 1.549 ms | 1.884 ms | 8.56x | |
| OOP S&P (Merge) | 1.649 ms | 2.101 ms | 8.04x | |
| bitECS S&P (Quick) | 1.675 ms | 2.174 ms | 7.92x | |
| bitECS S&P (Merge) | 1.743 ms | 2.373 ms | 7.61x | |
| bitECS S&P (Native) | 1.966 ms | 2.413 ms | 6.74x | |
| OOP S&P (Native) | 2.052 ms | 2.765 ms | 6.46x | |
| WASM ECS S&P (Insertion) | 3.433 ms | 4.153 ms | 3.86x | |
| ECS Tree | 4.422 ms | 5.360 ms | 3.00x | |
| WASM Tree | 4.485 ms | 4.973 ms | 2.96x | |
| ECS (Custom SoA) S&P (Insertion) | 4.511 ms | 6.598 ms | 2.94x | |
| OOP Tree | 5.509 ms | 12.983 ms | 2.41x | |
| bitECS S&P (Insertion) | 11.583 ms | 19.861 ms | 1.14x | |
| OOP S&P (Insertion) | 13.258 ms | 18.408 ms | 1.00x |
4. Static 동작 (절대적 응집성 - 15,000 Entities)
| System | Avg Frame Time | 99th Percentile | Speedup | |
|---|---|---|---|---|
| WASM ECS S&P (Insertion) | 0.959 ms | 1.085 ms | 5.43x | |
| WASM ECS S&P (Merge) | 1.112 ms | 1.349 ms | 4.69x | |
| WASM ECS S&P (Quick) | 1.170 ms | 1.449 ms | 4.45x | |
| ECS (Custom SoA) S&P (Insertion) | 3.162 ms | 3.653 ms | 1.65x | |
| ECS (Custom SoA) S&P (Quick) | 3.293 ms | 3.769 ms | 1.58x | |
| ECS (Custom SoA) S&P (Merge) | 3.395 ms | 3.954 ms | 1.53x | |
| WASM Tree | 3.587 ms | 5.116 ms | 1.45x | |
| ECS (Custom SoA) S&P (Native) | 3.902 ms | 4.741 ms | 1.34x | |
| ECS Tree | 4.320 ms | 5.095 ms | 1.21x | |
| OOP S&P (Insertion) | 5.210 ms | 6.260 ms | 1.00x | |
| OOP S&P (Native) | 5.423 ms | 7.281 ms | 0.96x | |
| OOP S&P (Quick) | 5.614 ms | 6.560 ms | 0.93x | |
| OOP S&P (Merge) | 5.995 ms | 9.221 ms | 0.87x | |
| OOP Tree | 6.746 ms | 9.319 ms | 0.77x | |
| bitECS S&P (Quick) | 7.456 ms | 10.520 ms | 0.70x | |
| bitECS S&P (Merge) | 7.447 ms | 9.227 ms | 0.70x | |
| bitECS S&P (Native) | 7.712 ms | 9.912 ms | 0.68x | |
| bitECS S&P (Insertion) | 8.448 ms | 14.122 ms | 0.62x |
5. Wandering 동작 (높은 응집성 - 15,000 Entities)
| System | Avg Frame Time | 99th Percentile | Speedup | |
|---|---|---|---|---|
| WASM ECS S&P (Insertion) | 1.557 ms | 1.859 ms | 4.71x | |
| WASM ECS S&P (Merge) | 1.767 ms | 2.186 ms | 4.15x | |
| WASM ECS S&P (Quick) | 1.809 ms | 2.031 ms | 4.05x | |
| ECS (Custom SoA) S&P (Insertion) | 4.489 ms | 6.483 ms | 1.63x | |
| ECS (Custom SoA) S&P (Quick) | 4.668 ms | 6.111 ms | 1.57x | |
| ECS (Custom SoA) S&P (Merge) | 4.781 ms | 5.670 ms | 1.53x | |
| ECS (Custom SoA) S&P (Native) | 5.658 ms | 8.731 ms | 1.30x | |
| OOP S&P (Insertion) | 7.335 ms | 9.044 ms | 1.00x | |
| OOP S&P (Merge) | 7.540 ms | 9.290 ms | 0.97x | |
| OOP S&P (Quick) | 7.645 ms | 10.421 ms | 0.96x | |
| OOP S&P (Native) | 8.036 ms | 11.292 ms | 0.91x | |
| bitECS S&P (Quick) | 9.077 ms | 10.931 ms | 0.81x | |
| WASM Tree | 9.083 ms | 10.938 ms | 0.81x | |
| bitECS S&P (Merge) | 9.215 ms | 11.554 ms | 0.80x | |
| bitECS S&P (Native) | 9.674 ms | 11.345 ms | 0.76x | |
| ECS Tree | 9.970 ms | 43.666 ms | 0.74x | |
| bitECS S&P (Insertion) | 10.730 ms | 37.820 ms | 0.68x | |
| OOP Tree | 16.321 ms | 40.756 ms | 0.45x |
6. Erratic 동작 (낮은 응집성 - 15,000 Entities)
| System | Avg Frame Time | 99th Percentile | Speedup | |
|---|---|---|---|---|
| WASM ECS S&P (Quick) | 2.141 ms | 2.588 ms | 61.92x | |
| WASM ECS S&P (Merge) | 2.226 ms | 2.548 ms | 59.56x | |
| ECS (Custom SoA) S&P (Quick) | 5.318 ms | 5.765 ms | 24.93x | |
| ECS (Custom SoA) S&P (Merge) | 5.668 ms | 6.334 ms | 23.39x | |
| ECS (Custom SoA) S&P (Native) | 6.991 ms | 8.146 ms | 18.97x | |
| OOP S&P (Quick) | 8.380 ms | 9.820 ms | 15.82x | |
| OOP S&P (Merge) | 8.826 ms | 12.094 ms | 15.02x | |
| bitECS S&P (Quick) | 9.692 ms | 11.258 ms | 13.68x | |
| bitECS S&P (Merge) | 10.037 ms | 12.722 ms | 13.21x | |
| OOP S&P (Native) | 10.645 ms | 14.415 ms | 12.45x | |
| bitECS S&P (Native) | 10.877 ms | 12.864 ms | 12.19x | |
| ECS Tree | 16.639 ms | 19.107 ms | 7.97x | |
| WASM Tree | 16.976 ms | 19.827 ms | 7.81x | |
| OOP Tree | 29.261 ms | 74.175 ms | 4.53x | |
| WASM ECS S&P (Insertion) | 32.518 ms | 35.499 ms | 4.08x | |
| ECS (Custom SoA) S&P (Insertion) | 36.896 ms | 41.801 ms | 3.59x | |
| bitECS S&P (Insertion) | 101.341 ms | 122.182 ms | 1.31x | |
| OOP S&P (Insertion) | 132.579 ms | 140.137 ms | 1.00x |