1. Structure of Array
1.1. 개요
Structure of Array (SoA)는 여러 객체의 데이터를 저장할 때, 각 객체의 동일한 멤버(필드)들을 분리하여 각각의 독립된 배열에 연속적으로 저장하는 데이터 레이아웃 방식입니다.
예를 들어, 3차원 공간의 점(Position) 100개를 저장해야 한다고 가정해 보겠습니다. 각 점은 x, y, z 좌표를 가집니다.
- AoS (Array of Structures) 방식이라면:
Position[100]배열 하나에Position객체 100개를 저장합니다. - SoA (Structure of Arrays) 방식이라면:
float x[100],float y[100],float z[100]와 같이 각 좌표 성분별로 3개의 배열을 만들어 데이터를 저장합니다.
메모리 상에서는 다음과 같이 표현될 수 있습니다.[x1, x2, ..., xn], [y1, y2, ..., yn], [z1, z2, ..., zn]
이러한 구조는 특정 필드의 데이터들만 묶어서 순차적으로 처리해야 할 때 엄청난 성능적 이점을 제공하며, 특히 고성능 컴퓨팅, 게임 개발, 데이터 분석 분야에서 각광받는 기술입니다.
2. 내부 구조 및 구현 방식
2.1. 메모리 레이아웃
SoA의 핵심은 데이터의 지역성(Locality of Reference)을 특정 필드에 집중시키는 것입니다.
- AoS 메모리 레이아웃:
[x1, y1, z1, x2, y2, z2, x3, y3, z3, ...]- 한 객체의 모든 필드(
x, y, z)가 인접해 있습니다.
- 한 객체의 모든 필드(
- SoA 메모리 레이아웃:
[x1, x2, x3, ...], [y1, y2, y3, ...], [z1, z2, z3, ...]- 동일한 필드의 모든 데이터(
x좌표들)가 인접해 있습니다.
- 동일한 필드의 모든 데이터(
모든 객체의 x 좌표를 순회하는 연산을 생각해보겠습니다.
- AoS에서는
x1에 접근한 후, 다음x2에 접근하기 위해y1,z1을 건너뛰어야 합니다. 이 과정에서 불필요한y1,z1데이터가 CPU 캐시에 로드되어 캐시 공간을 낭비하고(Cache Pollution), 메모리 대역폭을 소모합니다. - SoA에서는
x좌표 배열이 메모리에 연속적으로 배치되어 있으므로,x1다음x2,x3... 순으로 접근할 때 CPU가 데이터를 매우 효율적으로 캐시 라인에 로드(Prefetching)할 수 있습니다. 이는 캐시 미스(Cache Miss)를 획기적으로 줄여줍니다.
2.2. 구현 방식
구조체나 클래스를 사용하여 SoA 레이아웃을 쉽게 구현할 수 있습니다.
#include <vector>
// 100개의 파티클 데이터를 저장한다고 가정
const size_t NUM_PARTICLES = 100;
// SoA 방식의 파티클 데이터 구조
struct Particles_SoA {
std::vector<float> posX, posY, posZ;
std::vector<float> velX, velY, velZ;
std::vector<float> lifetime;
Particles_SoA(size_t size) {
posX.resize(size);
posY.resize(size);
posZ.resize(size);
velX.resize(size);
velY.resize(size);
velZ.resize(size);
lifetime.resize(size);
}
};
int main() {
Particles_SoA particles(NUM_PARTICLES);
// 모든 파티클의 위치를 업데이트하는 연산
// 이 루프는 매우 캐시 친화적이며 SIMD 최적화에 유리합니다.
for (size_t i = 0; i < NUM_PARTICLES; ++i) {
particles.posX[i] += particles.velX[i];
particles.posY[i] += particles.velY[i];
particles.posZ[i] += particles.velZ[i];
}
return 0;
}
3. 동작 계층 구조
SoA가 어떻게 사용자 코드부터 하드웨어까지 영향을 미치는지 계층별로 살펴보겠습니다.
- 사용자 영역 (Application Layer)
- 개발자는
particles.posX[i]와 같이 특정 데이터 필드에 직접 접근하는 코드를 작성합니다. - 알고리즘 자체가 "모든 객체의 특정 속성"을 일괄 처리하는 형태로 설계됩니다. 예를 들어, "모든 파티클의 수명 감소" 또는 "모든 적 캐릭터의 위치 업데이트"와 같은 연산입니다.
- 개발자는
- 컴파일러 / 런타임 (Compiler / Runtime Layer)
- 컴파일러는 SoA 형태의 데이터 순회 코드를 매우 쉽게 최적화할 수 있습니다. 특히 자동 벡터화(Auto-Vectorization)가 핵심입니다.
for루프에서posX배열을 순회하는 코드는 컴파일러에 의해 SIMD(Single Instruction, Multiple Data) 명령어로 변환될 가능성이 매우 높습니다. SIMD는 하나의 명령어로 여러 개의 데이터(예: 4개의 float)를 동시에 처리하는 CPU 기술입니다.- 예를 들어,
posX[i] += velX[i]연산은 4개의 파티클에 대해 한 번의 SIMD 명령어로 처리될 수 있어, 이론적으로 4배의 성능 향상을 기대할 수 있습니다. AoS 구조에서는 데이터가 흩어져 있어(non-contiguous) 이러한 자동 벡터화가 어렵습니다.
- 운영체제 (OS Layer)
- OS는 가상 메모리를 물리 메모리에 매핑하는 역할을 합니다. SoA의 각 배열(
posX,posY등)은 각각 연속된 가상 메모리 공간에 할당됩니다. posX배열에 대한 연산이 발생하면, OS는 해당 배열이 담긴 메모리 페이지만 물리 메모리로 가져옵니다.posY나posZ배열이 담긴 페이지는 필요하지 않다면 로드되지 않으므로, 페이지 폴트(Page Fault) 발생 시에도 더 효율적일 수 있습니다.
- OS는 가상 메모리를 물리 메모리에 매핑하는 역할을 합니다. SoA의 각 배열(
- 하드웨어 (Hardware Layer - CPU)
- CPU 캐시: SoA의 가장 큰 수혜자입니다.
posX배열을 순차적으로 읽을 때, CPU의 프리페처(Prefetcher)는 다음에 사용될 데이터를 예측하여 미리 캐시 라인(e.g., 64 bytes)에 로드합니다. 데이터가 연속적이므로 예측 성공률이 매우 높고, 이는 캐시 미스를 최소화하여 CPU가 데이터 로드를 기다리며 멈추는(stall) 현상을 방지합니다. - SIMD 유닛: 앞서 언급했듯, 연속된 데이터는 CPU 내의 SIMD 연산 유닛(AVX, SSE 등)을 통해 병렬로 처리될 수 있습니다. 이는 데이터 처리량을 극대화합니다.
- CPU 캐시: SoA의 가장 큰 수혜자입니다.
4. SoA vs. AoS 비교
| 특징 | Structure of Arrays (SoA) | Array of Structures (AoS) |
|---|---|---|
| 메모리 구조 | 동일 필드 데이터가 연속적 [x1,x2,x3], [y1,y2,y3] |
객체 단위로 데이터가 연속적 [x1,y1,z1, x2,y2,z2] |
| 장점 | - 캐시 효율성 극대화: 특정 필드 순회 시 캐시 미스 최소화. - SIMD 최적화: 데이터가 연속적이어서 자동 벡터화에 매우 유리. - 메모리 대역폭 절약: 필요한 필드 데이터만 로드. |
- 직관적인 코드: 객체 지향 개념과 일치하여 가독성 및 유지보수 용이. - 객체 단위 접근 용이: 한 객체의 모든 필드 접근 시 캐시 효율 좋음. - 데이터 관리 용이: 객체 단위 추가/삭제가 간편. |
| 단점 | - 객체 단위 접근 비효율: 한 객체의 모든 필드 접근 시 여러 배열을 인덱싱해야 하므로 캐시 미스 유발 가능. - 코드 복잡성 증가: 데이터 구조가 직관적이지 않을 수 있음. - 객체 단위 관리 복잡: 객체 추가/삭제 시 모든 배열을 동기화해야 함. |
- 특정 필드 순회 시 비효율: 불필요한 데이터까지 캐시에 로드되어 캐시 오염 및 미스 발생. - SIMD 최적화 어려움: 데이터가 흩어져 있어 벡터화가 힘듦 (Gather 연산 필요). |
| 주 사용처 | - 고성능 컴퓨팅 (과학 시뮬레이션) - 게임 엔진 (파티클, 렌더링) - 데이터 분석, Column-oriented DB |
- 일반적인 응용 프로그램 - UI, 게임 로직 등 객체 단위 작업이 많은 곳 - 코드 가독성과 유지보수가 중요할 때 |
언제 무엇을 사용해야 하는가?
- SoA: "많은 객체"의 "소수의 필드"를 반복적으로 처리하는 경우. (e.g.,
for (p in particles) p.pos += p.vel;) - AoS: "소수의 객체"의 "많은 필드"를 동시에 접근하거나, 객체 단위의 로직이 복잡한 경우. (e.g.,
player.updateAll();)
현대 게임 엔진이나 데이터베이스는 두 방식의 장점을 모두 취하기 위해 하이브리드 접근법(AoSоA)을 사용하기도 합니다. 즉, 객체들을 작은 청크(chunk)로 나누고, 각 청크 내에서는 SoA 방식으로 데이터를 저장하는 구조입니다.
5. 요약
Structure of Array, 줄여서 SoA는 여러 객체의 데이터를 저장할 때, 이름, 학번, 학점과 같은 동일한 속성(필드)끼리 묶어서 각각 별도의 배열로 관리하는 데이터 구조입니다. 예를 들어 학생 100명의 정보가 있다면, 이름 배열 1개, 학번 배열 1개, 학점 배열 1개를 만드는 방식입니다.
이렇게 구성하면 특정 속성에 대한 일괄 처리에 매우 강력한 성능을 발휘합니다. 가령, 모든 학생의 학점 평균을 계산한다고 할 때, 학점 데이터만 메모리에 연속적으로 모여있기 때문에 CPU가 데이터를 읽어올 때 캐시 미스가 거의 발생하지 않습니다. 또한, CPU는 하나의 명령어로 여러 데이터를 동시에 처리하는 SIMD 기술을 활용할 수 있는데, SoA 구조는 이러한 병렬 처리에 매우 이상적이어서 컴파일러가 코드를 자동으로 최적화하기 쉽습니다.
반면, 한 학생의 모든 정보, 즉 이름, 학번, 학점을 한 번에 가져오려면 여러 배열을 각각 접근해야 해서 번거롭고 오히려 비효율적일 수 있습니다.
'Computer Science' 카테고리의 다른 글
| 단위 벡터 (0) | 2025.12.09 |
|---|---|
| Array of Structure (AoS) (0) | 2025.12.02 |
| 해시 충돌 Hash Collision (0) | 2025.11.25 |
| 뮤텍스 Mutex (1) | 2025.11.25 |
| TCP와 UDP (0) | 2025.11.24 |
