본문 바로가기

Array of Structure (AoS)

@iamrain2025. 12. 2. 20:27

1. Array of Structure (AoS)

1.1. 개요

Array of Structure (AoS)는 여러 필드(멤버)를 포함하는 구조체(또는 클래스) 자체를 배열의 요소로 저장하는, 가장 전통적이고 직관적인 데이터 레이아웃 방식입니다. 객체 지향 프로그래밍에서 객체의 컬렉션을 다룰 때 자연스럽게 사용되는 구조입니다.

예를 들어, 3차원 공간의 점(Position) 100개를 저장해야 한다고 가정해 보겠습니다. 각 점은 x, y, z 좌표를 가집니다.

  • AoS (Array of Structures) 방식에서는 Position 구조체를 정의하고, 이 구조체 타입의 배열 Position positions[100]을 선언하여 100개의 점 객체를 저장합니다.

메모리 상에서는 한 객체의 모든 데이터가 묶여서 연속적으로 나열됩니다.
[x1, y1, z1, x2, y2, z2, x3, y3, z3, ...]

이러한 구조는 개별 객체 단위로 데이터를 처리하고 접근하는 데 매우 효율적이며, 코드의 가독성과 유지보수성을 높여줍니다.


2. 내부 구조 및 구현 방식

2.1. 메모리 레이아웃

AoS의 핵심은 객체 단위의 데이터 지역성(Locality of Reference)을 보장하는 것입니다.

  • AoS 메모리 레이아웃: [x1, y1, z1, x2, y2, z2, ...]
    • positions[0]x, y, z가 메모리 상에 인접해 있고, 그 바로 뒤에 positions[1]x, y, z가 이어집니다.
  • SoA 메모리 레이아웃: [x1, x2, x3, ...], [y1, y2, y3, ...], [z1, z2, z3, ...]
    • 동일 필드(x)의 데이터들이 메모리 상에 인접해 있습니다.

positions[i]라는 한 객체의 모든 필드에 접근하는 연산을 생각해보겠습니다.

  • AoS에서는 positions[i]의 주소에 접근하는 순간, 해당 객체의 모든 필드(x, y, z)가 동일한 캐시 라인에 존재하거나 매우 인접한 캐시 라인에 존재할 확률이 높습니다. 따라서 positions[i].x 접근 후 positions[i].y에 접근할 때 캐시 히트(Cache Hit)가 발생하여 매우 빠릅니다.
  • SoA에서는 x[i], y[i], z[i]가 서로 다른 메모리 영역에 흩어져 있으므로, 한 객체의 모든 필드를 가져오려면 여러 메모리 주소에 접근해야 하고 이는 캐시 미스를 유발할 수 있습니다.

2.2. 구현 방식

C++에서 AoS는 매우 자연스러운 형태로 구현됩니다.

#include <vector>
#include <string>

// 학생 정보를 저장하는 구조체
struct Student {
    int id;
    std::string name;
    float gpa;
};

int main() {
    std::vector<Student> students;
    students.push_back({20230001, "Alice", 3.8f});
    students.push_back({20230002, "Bob", 4.2f});

    // 한 학생의 모든 정보를 출력하는 연산
    // 이 작업은 AoS 구조에서 매우 효율적입니다.
    for (const auto& student : students) {
        // student 객체의 id, name, gpa 필드는 메모리 상에 모여있습니다.
        std::cout << "ID: " << student.id
                  << ", Name: " << student.name
                  << ", GPA: " << student.gpa << std::endl;
    }

    return 0;
}

3. 동작 계층 구조

AoS가 어떻게 사용자 코드부터 하드웨어까지 영향을 미치는지 계층별로 살펴보겠습니다.

  1. 사용자 영역 (Application Layer)
    • 개발자는 student.name, player.health와 같이 객체 중심적인 코드를 작성합니다. 이는 객체 지향 프로그래밍(OOP) 패러다임과 완벽하게 일치하여 코드의 직관성을 높입니다.
    • 로직 자체가 "한 객체의 여러 속성을 조합하여" 동작하는 경우가 많습니다. (e.g., 플레이어의 위치, 속도, 상태를 모두 고려하여 다음 행동을 결정)
  2. 컴파일러 / 런타임 (Compiler / Runtime Layer)
    • 컴파일러는 students[i]의 시작 주소를 계산하고, 각 필드(id, name, gpa)에 접근하기 위해 해당 주소로부터의 고정된 오프셋(offset)을 더하는 명령어를 생성합니다.
    • sizeof(Student)가 구조체의 크기가 되며, students[i]의 주소는 students_base_address + i * sizeof(Student)로 계산됩니다.
  3. 운영체제 (OS Layer)
    • students 벡터(배열)는 연속된 가상 메모리 공간에 할당됩니다.
    • 특정 student 객체에 접근하면, 해당 객체가 포함된 메모리 페이지가 물리 메모리로 로드됩니다. 이 페이지에는 다른 여러 student 객체들도 함께 포함되어 있을 수 있습니다.
  4. 하드웨어 (Hardware Layer - CPU)
    • CPU 캐시: AoS는 객체 단위 접근에 강점을 보입니다. students[i]에 접근하면, 해당 객체의 모든 필드가 하나의 캐시 라인(또는 인접한 캐시 라인)에 로드될 가능성이 높습니다. 이는 공간적 지역성(Spatial Locality) 원리에 부합하며, 해당 객체의 다른 필드에 연달아 접근할 때 캐시 히트를 보장합니다.
    • 하지만, 모든 학생의 gpa만 순회하는 경우, CPU는 gpa 뿐만 아니라 불필요한 idname 데이터까지 캐시 라인에 로드하게 됩니다. 이는 귀중한 캐시 공간을 낭비하는 캐시 오염(Cache Pollution)을 일으키고, 결과적으로 캐시 미스율을 높여 성능 저하를 유발합니다.
    • SIMD: AoS 구조에서 특정 필드(e.g., gpa)는 메모리 상에 연속적이지 않고 일정 간격(stride)을 두고 떨어져 있습니다. 이 때문에 컴파일러가 자동으로 SIMD 명령어로 변환하기 매우 어렵습니다. 이를 처리하려면 데이터를 재정렬하거나, 메모리에서 흩어진 데이터를 모으는 gather 같은 비싼 SIMD 명령어를 사용해야 합니다.

4. AoS vs. SoA 비교

특징 Array of Structures (AoS) Structure of Arrays (SoA)
메모리 구조 객체 단위로 데이터가 연속적 [x1,y1,z1, x2,y2,z2] 동일 필드 데이터가 연속적 [x1,x2,x3], [y1,y2,y3]
장점 - 직관적인 코드: 객체 지향 개념과 일치하여 가독성 및 유지보수 용이.
- 객체 단위 접근 용이: 한 객체의 모든 필드 접근 시 캐시 효율 좋음.
- 데이터 관리 용이: 객체 단위 추가/삭제가 간편.
- 캐시 효율성 극대화: 특정 필드 순회 시 캐시 미스 최소화.
- SIMD 최적화: 데이터가 연속적이어서 자동 벡터화에 매우 유리.
- 메모리 대역폭 절약: 필요한 필드 데이터만 로드.
단점 - 특정 필드 순회 시 비효율: 불필요한 데이터까지 캐시에 로드되어 캐시 오염 및 미스 발생.
- SIMD 최적화 어려움: 데이터가 흩어져 있어 벡터화가 힘듦 (Gather 연산 필요).
- 객체 단위 접근 비효율: 한 객체의 모든 필드 접근 시 여러 배열을 인덱싱해야 하므로 캐시 미스 유발 가능.
- 코드 복잡성 증가: 데이터 구조가 직관적이지 않을 수 있음.
- 객체 단위 관리 복잡: 객체 추가/삭제 시 모든 배열을 동기화해야 함.
주 사용처 - 일반적인 응용 프로그램
- UI, 게임 로직 등 객체 단위 작업이 많은 곳
- 코드 가독성과 유지보수가 중요할 때
- 고성능 컴퓨팅 (과학 시뮬레이션)
- 게임 엔진 (파티클, 렌더링)
- 데이터 분석, Column-oriented DB

언제 무엇을 사용해야 하는가?

  • AoS: "소수의 객체""많은 필드"를 동시에 접근하거나, 객체 단위의 로직이 복잡한 경우. (e.g., player.updateAll();)
  • SoA: "많은 객체""소수의 필드"를 반복적으로 처리하는 경우. (e.g., for (p in particles) p.pos += p.vel;)

성능이 매우 중요한 시스템이 아니라면, 대부분의 경우 AoS가 제공하는 코드의 직관성과 유지보수의 용이성이 더 큰 장점이 될 수 있습니다. 성능 최적화는 프로파일링을 통해 병목 지점을 명확히 식별한 후에, 해당 부분에만 SoA나 다른 데이터 구조를 적용하는 것이 현명한 접근 방식입니다.


5. 요약

Array of Structure, 줄여서 AoS는 우리가 프로그래밍할 때 가장 보편적으로 사용하는 데이터 구조입니다. 이름, 학번, 학점 등 여러 정보를 담고 있는 '학생'이라는 객체(구조체)를 만들고, 이 객체들을 배열에 순서대로 담는 방식입니다.

이 구조의 가장 큰 장점은 한 객체에 대한 모든 정보가 메모리 상에 모여 있다는 것입니다. 그래서 특정 학생 한 명의 정보를 조회하거나 수정할 때, 관련된 데이터들이 CPU 캐시에 한 번에 로드될 확률이 높아 매우 효율적입니다. 또한, 객체 지향 프로그래밍의 개념과 잘 맞아 코드가 직관적이고 이해하기 쉬우며 유지보수하기 편리합니다.

하지만 단점도 명확합니다. 만약 모든 학생의 학점 평균을 구하는 것처럼 특정 정보 하나만 전체 학생에 대해 순회해야 할 경우, 학점 외에 이름이나 학번 같은 불필요한 데이터까지 메모리에서 읽어와 CPU 캐시를 낭비하게 됩니다. 이는 캐시 효율을 떨어뜨리고 성능 저하의 원인이 될 수 있습니다.

'Computer Science' 카테고리의 다른 글

고아 프로세스와 좀비 프로세스  (0) 2025.12.11
단위 벡터  (0) 2025.12.09
Structure of Array (SoA)  (0) 2025.12.02
해시 충돌 Hash Collision  (0) 2025.11.25
뮤텍스 Mutex  (1) 2025.11.25
iamrain
@iamrain :: Annals of Unreal

iamrain 님의 블로그 입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차