const points = [];for (let i = 0; i < N; i++) { points.push({ x: Math.random(), y: Math.random(), z: Math.random() });}let sum = 0;for (let i = 0; i < N; i++) { sum += points[i].x + points[i].y + points[i].z;}const points = { x: new Float64Array(N), y: new Float64Array(N), z: new Float64Array(N)};for (let i = 0; i < N; i++) { points.x[i] = Math.random(); points.y[i] = Math.random(); points.z[i] = Math.random();}let sum = 0;for (let i = 0; i < N; i++) { sum += points.x[i] + points.y[i] + points.z[i];}접근 방식 | 시간 |
|---|---|
객체 배열 (AoS) | ~42ms |
TypedArray의 객체 (SoA) | ~10ms |
Array 또한 연속된 메모리 블록을 제공할 것이라고 자연스럽게 가정할 수 있습니다.
arr[500000] = "보세요, 숫자 배열에 문자열이 들어왔어요";arr[999999] = { look: "ma", an:"object", in:"numeric array" };arr.length = 5; // 보세요, 다 날려버렸어요delete arr[42]; // 보세요, 구멍(hole)을 만들었어요const arr = new Array(1000000);for (let i = 0; i < arr.length; i++) { arr[i] = i * 1.5;}
PACKED_DOUBLE_ELEMENTS를 사용합니다. 이는 힙 포인터가 아닌, 박싱되지 않은(Unboxed) 64비트 부동 소수점의 연속된 배열입니다. C 배열과 똑같이 메모리에 나란히 놓인 순수 IEEE 754 double 값들입니다.const arr = [];for (let i = 0; i < 10; i++){ arr.push(i * 1.5); if(i === 5){ arr[i] = "보세요, 숫자 배열에 문자열이 들어왔어요"; } if(i === 7){ arr[i] = { look: "ma", an:"object", in:"numeric array" }; }}
const N = 1_000_000;const regular = [];for (let i = 0; i < N; i++) regular.push(Math.random());const typed = new Float64Array(N);for (let i = 0; i < N; i++) typed[i] = Math.random();function sumRegular() { let sum = 0; for (let i = 0; i < regular.length; i++) sum += regular[i]; return sum;}function sumTyped() { let sum = 0; for (let i = 0; i < typed.length; i++) sum += typed[i]; return sum;}
point.x는 속성 조회가 필요하지만, arr[i]는 직접 인덱싱입니다.points[i].x는 다음을 요구합니다:points.x[i]는:첫 번째 AoS 방식은 배열을 생성하고 루프의 각 반복마다 요소를 push합니다. 반면 SoA는 초기화 시점에 필요한 크기의 배열을 생성한 다음 배열 인덱스를 사용하여 효율적으로 기록합니다. 이것이 당신이 보고 있는 속도 향상의 대부분을 차지할 것입니다. 이 테스트는 조작된 문제이고, 어리석은 전제이며, 잘못된 테스트 케이스이고, 솔직히 무지한 게 아니라면 부정직한 것입니다.
push() 대 사전 할당(Pre-allocation)이 실제로 읽기(READ) 성능에 영향을 미치는지 테스트해 보겠습니다. push()와 사전 할당으로 생성된 메모리 레이아웃이 다를 수 있기 때문입니다. (생성 루프가 아닌 합산 루프의 시간만 측정하므로 읽기 성능만 측정합니다.)const N = 1_000_000;const ITERATIONS = 100;const aosPush = [];for (let i = 0; i < N; i++) { aosPush.push({ x: Math.random(), y: Math.random(), z: Math.random() });}const aosPrealloc = new Array(N);for (let i = 0; i < N; i++) { aosPrealloc[i] = { x: Math.random(), y: Math.random(), z: Math.random() };}function Point(x, y, z) { this.x = x; this.y = y; this.z = z; }const aosConstructor = new Array(N);for (let i = 0; i < N; i++) { aosConstructor[i] = new Point(Math.random(), Math.random(), Math.random());}const soaTyped = { x: new Float64Array(N), y: new Float64Array(N), z: new Float64Array(N)};for (let i = 0; i < N; i++) { soaTyped.x[i] = Math.random(); soaTyped.y[i] = Math.random(); soaTyped.z[i] = Math.random();}const soaRegular = { x: new Array(N), y: new Array(N), z: new Array(N)};for (let i = 0; i < N; i++) { soaRegular.x[i] = Math.random(); soaRegular.y[i] = Math.random(); soaRegular.z[i] = Math.random();}function benchmark(name, fn) { for (let i = 0; i < 10; i++) fn(); const times = []; for (let i = 0; i < ITERATIONS; i++) { const start = performance.now(); fn(); times.push(performance.now() - start); } times.sort((a, b) => a - b); console.log(`${name}: ${times[Math.floor(times.length / 2)].toFixed(3)}ms`);}console.log('AoS 변형 (모두 객체 읽기):');benchmark(' AoS (push)', () => { let sum = 0; for (let i = 0; i < N; i++) sum += aosPush[i].x + aosPush[i].y + aosPush[i].z; return sum;});benchmark(' AoS (사전 할당)', () => { let sum = 0; for (let i = 0; i < N; i++) sum += aosPrealloc[i].x + aosPrealloc[i].y + aosPrealloc[i].z; return sum;});benchmark(' AoS (생성자)', () => { let sum = 0; for (let i = 0; i < N; i++) sum += aosConstructor[i].x + aosConstructor[i].y + aosConstructor[i].z; return sum;});console.log('\nSoA 변형 (객체 없음):');benchmark(' SoA (TypedArray)', () => { let sum = 0; for (let i = 0; i < N; i++) sum += soaTyped.x[i] + soaTyped.y[i] + soaTyped.z[i]; return sum;});benchmark(' SoA (일반 Array)', () => { let sum = 0; for (let i = 0; i < N; i++) sum += soaRegular.x[i] + soaRegular.y[i] + soaRegular.z[i]; return sum;});
push() 대 사전 할당은 거의 차이가 없습니다.arr[i]를 읽을 때, CPU는 해당 주소를 포함하는 전체 라인을 가져옵니다. 데이터가 연속적이라면, 그 한 번의 가져오기로 8개의 float 값을 공짜로 얻는 셈입니다. 데이터가 힙 객체로 흩어져 있다면, 캐시 라인의 대부분은 필요 없는 쓰레기 데이터가 됩니다.sum += points.x[i] + points.y[i] + points.z[i];// 인터리브 방식: x0, y0, z0, x1, y1, z1, ...const points = new Float64Array(N * 3);const N = 1_000_000;const ITERATIONS = 100;const soA = { x: new Float64Array(N), y: new Float64Array(N), z: new Float64Array(N)};for (let i = 0; i < N; i++) { soA.x[i] = Math.random(); soA.y[i] = Math.random(); soA.z[i] = Math.random();}const interleaved = new Float64Array(N * 3);for (let i = 0; i < N; i++) { interleaved[i * 3] = Math.random(); interleaved[i * 3 + 1] = Math.random(); interleaved[i * 3 + 2] = Math.random();}function benchmark(name, fn) { for (let i = 0; i < 10; i++) fn(); const times = []; for (let i = 0; i < ITERATIONS; i++) { const start = performance.now(); fn(); times.push(performance.now() - start); } times.sort((a, b) => a - b); console.log(`${name}: ${times[Math.floor(times.length / 2)].toFixed(3)}ms`); return times[Math.floor(times.length / 2)];}const soaTime = benchmark('SoA (3개의 TypedArray)', () => { let sum = 0; for (let i = 0; i < N; i++) { sum += soA.x[i] + soA.y[i] + soA.z[i]; } return sum;});const interleavedTime = benchmark('인터리브 TypedArray', () => { let sum = 0; for (let i = 0; i < N * 3; i += 3) { sum += interleaved[i] + interleaved[i + 1] + interleaved[i + 2]; } return sum;});
const N = 1_000_000;const data = new Float64Array(N * 3);for (let i = 0; i < N * 3; i++) data[i] = Math.random();// 반복당 3번의 먼 거리 접근function threeDistantAccesses() { let sum = 0; for (let i = 0; i < N; i++) { sum += data[i] + data[i + N] + data[i + N * 2]; } return sum;}// 반복당 1번의 순수 순차 접근function pureSequential() { let sum = 0; for (let i = 0; i < N * 3; i++) { sum += data[i]; } return sum;}// 반복당 3개씩 그룹화된 순차 접근function groupedSequential() { let sum = 0; for (let i = 0; i < N * 3; i += 3) { sum += data[i] + data[i + 1] + data[i + 2]; } return sum;}
// 3,000,000번 반복, 각 1번의 덧셈 — 루프 오버헤드를 300만 번 지불for (let i = 0; i < N * 3; i++) { sum += data[i];}// 1,000,000번 반복, 각 3번의 덧셈 — 루프 오버헤드를 100만 번 지불for (let i = 0; i < N * 3; i += 3) { sum += data[i] + data[i+1] + data[i+2];}a + b + c는 CPU 파이프라인에서 부분적으로 겹쳐서 실행될 수 있지만, 이는 동일한 반복문 안에 있을 때만 가능합니다.points.x, points.y, points.z 해석을 루프 밖으로 이동시킵니다. 루프 내부에서는 인덱스 접근만 남습니다.i * 3 곱셈 연산이 필요 없습니다.i * 3 산술 오버헤드와 속성 조회를 끌어올리는 이점을 동일하게 누리지 못하기 때문입니다.function main() { const ARRAY_SIZE = 50_000_000; const aos = []; for (let i = 0; i < ARRAY_SIZE; i++) { aos.push({ x: i, y: i * 2, z: i * 3 }); } const soa = { x: new Float64Array(ARRAY_SIZE), y: new Float64Array(ARRAY_SIZE), z: new Float64Array(ARRAY_SIZE) }; for (let i = 0; i < ARRAY_SIZE; i++) { soa.x[i] = i; soa.y[i] = i * 2; soa.z[i] = i * 3; } const interleaved = new Float64Array(ARRAY_SIZE * 3); for (let i = 0; i < ARRAY_SIZE; i++) { const base = i * 3; interleaved[base] = i; interleaved[base + 1] = i * 2; interleaved[base + 2] = i * 3; } let sumAoS = 0; const startAoS = performance.now(); for (let iter = 0; iter < 10; iter++) { for (let i = 0; i < ARRAY_SIZE; i++) { sumAoS += aos[i].x + aos[i].y + aos[i].z; } } const timeAoS = performance.now() - startAoS; let sumSoA = 0; const startSoA = performance.now(); for (let iter = 0; iter < 10; iter++) { for (let i = 0; i < ARRAY_SIZE; i++) { sumSoA += soa.x[i] + soa.y[i] + soa.z[i]; } } const timeSoA = performance.now() - startSoA; let sumInterleaved = 0; const startInterleaved = performance.now(); for (let iter = 0; iter < 10; iter++) { for (let i = 0; i < ARRAY_SIZE; i++) { const base = i * 3; sumInterleaved += interleaved[base] + interleaved[base + 1] + interleaved[base + 2]; } } const timeInterleaved = performance.now() - startInterleaved; console.log(`AoS: ${timeAoS.toFixed(2)}ms`); console.log(`SoA: ${timeSoA.toFixed(2)}ms`); console.log(`Interleaved: ${timeInterleaved.toFixed(2)}ms`);}main();
i * 3)을 유발하고, SoA와 같은 수준의 끌어올리기 및 깔끔한 벡터화를 방해하여 공간 지역성이 좋음에도 불구하고 성능을 약간 떨어뜨리는 복합적인 오버헤드를 만듭니다.아직 댓글이 없습니다.
첫 번째 댓글을 작성해보세요!

TypeScript 성능 문제 해결: 사례 연구
Inkyu Oh • Front-End

시그널(Signals) vs 쿼리 기반 컴파일러(Query-Based Compilers)
Inkyu Oh • SW Engineering

AI 에이전트를 위한 좋은 스펙 작성법
Inkyu Oh • AI & ML-ops

GraphQL 에러 처리 가이드
Inkyu Oh • 라이브러리, 프레임워크