SOA와 AOS

2분 읽기 •


근래에 LLM이 떠오르며 SIMD 연산이 중요해지고 그에 따라 자연스럽게 데이터를 다루는 레이아웃을 고민하게 되었다. 여기엔 세가지가 있는데

각각의 레이아웃 장단점이 뭘까?

SoA (Structure of Arrays)

SoA는 구조체 안에 배열을 담는 방식이다. 예를 들어, 다음과 같은 구조체가 있다고 하자.

const N: usize = 100;

struct Person {
    age: [i32; N],
    height: [f32; N],
}

이 방식은 다음과 같은 상황일 때 이점이 크다.

단점도 있다.

특수한 유즈케이스가 아니면 잘 쓰지 않는다.

AoS (Array of Structures)

AoS는 구조체를 배열로 만드는 레이아웃 방법이다. 흔히 일반적인 개발자라면 많이 볼 수 있는 방법이다.

struct Person {
    age: i32,
    height: f32,
}

let people: Vec<Person> = vec![
    Person { age: 20, height: 170.0 },
    Person { age: 21, height: 175.0 },
    // ...
];

이 방식은 각각의 구조체에 접근할 때 효율이 좋다.

다만, 한번에 대량의 데이터에 접근하려면 모든 구조체에 접근해야 하기 떄문에 비효율적이다.

AoSoA (Array of Structures of Arrays)

이건 위 두 방식의 장점을 합쳐 SIMD 연산에 특화된 설계 방식이다.

간단한데, SoA 방식을 사용하고 그걸 다시 AoS 방식으로 묶는 방법이다.

struct PersonBlock {
    age: [i32; 8],
    height: [f32; 8],
}

let people: Vec<PersonBlock> = vec![
    PersonBlock {
        age: [20, 21, 22, 23, 24, 25, 26, 27],
        height: [170.0, 175.0, 180.0, 185.0, 190.0, 195.0, 200.0, 205.0],
    },
    PersonBlock {
        age: [28, 29, 30, 31, 32, 33, 34, 35],
        height: [210.0, 215.0, 220.0, 225.0, 230.0, 235.0, 240.0, 245.0],
    },
    // ...
];

이렇게 하면 한번에 8개(256비트, SIMD 연산에 최적화됨)의 데이터에 접근할 수 있고, 모든 데이터를 업데이트할 때도 효율적이다.

또한 SoA 로 설계된 데이터에선 SIMD 연산을 할 때 직접 오프셋과 끝을 정해줘야 했는데, AoSoA 방식에서는 블록 단위로 자연스럽게 SIMD 연산이 가능하다는 장점이 있다.

이제 대충 물리 시뮬레이터를 만들어서 벤치마킹을 해보자.

# AoS 모든 위치 업데이트 벤치마크
AoS update positions    time:   [2.8440 ms 2.8596 ms 2.8775 ms]
                        change: [−2.5705% −1.7786% −0.9646%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 10 outliers among 100 measurements (10.00%)
  6 (6.00%) high mild
  4 (4.00%) high severe

Benchmarking SoA update positions: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 6.8s, enable flat sampling, or reduce sample count to 60.

# SoA 모든 위치 업데이트 벤치마크
SoA update positions    time:   [1.3333 ms 1.3451 ms 1.3598 ms]
                        change: [−2.3746% −1.1937% −0.0467%] (p = 0.05 > 0.05)
                        No change in performance detected.
Found 12 outliers among 100 measurements (12.00%)
  5 (5.00%) high mild
  7 (7.00%) high severe

# AoSoA 모든 위치 업데이트 벤치마크
AoSoA update positions  time:   [2.7560 ms 2.7724 ms 2.7909 ms]
                        change: [−1.3765% −0.5173% +0.4083%] (p = 0.26 > 0.05)
                        No change in performance detected.
Found 14 outliers among 100 measurements (14.00%)
  8 (8.00%) high mild
  6 (6.00%) high severe

# ------------------------------------------------------------

# AoS 운동 에너지 계산 벤치마크
AoS kinetic energy      time:   [2.3260 ms 2.3430 ms 2.3620 ms]
                        change: [−3.2449% −2.0336% −0.9182%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 11 outliers among 100 measurements (11.00%)
  6 (6.00%) high mild
  5 (5.00%) high severe

Benchmarking SoA kinetic energy: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 6.6s, enable flat sampling, or reduce sample count to 60.

# SoA 운동 에너지 계산 벤치마크
SoA kinetic energy      time:   [1.3045 ms 1.3221 ms 1.3428 ms]
                        change: [−1.5365% −0.4294% +0.7775%] (p = 0.46 > 0.05)
                        No change in performance detected.
Found 6 outliers among 100 measurements (6.00%)
  4 (4.00%) high mild
  2 (2.00%) high severe

# AoSoA 운동 에너지 계산 벤치마크
AoSoA kinetic energy    time:   [2.3083 ms 2.3286 ms 2.3521 ms]
                        change: [−2.5771% −1.3791% −0.1421%] (p = 0.02 < 0.05)
                        Change within noise threshold.
Found 11 outliers among 100 measurements (11.00%)
  3 (3.00%) high mild
  8 (8.00%) high severe

Benchmarking AoS sum x coordinates: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 9.1s, enable flat sampling, or reduce sample count to 50.

# AoS x 좌표 합 벤치마크
AoS sum x coordinates   time:   [1.8064 ms 1.8162 ms 1.8274 ms]
                        change: [−1.9885% −1.1965% −0.3979%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 11 outliers among 100 measurements (11.00%)
  7 (7.00%) high mild
  4 (4.00%) high severe

# SoA x 좌표 합 벤치마크
SoA sum x coordinates   time:   [928.80 µs 937.06 µs 946.70 µs]
                        change: [−2.9895% −1.8426% −0.6016%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 5 outliers among 100 measurements (5.00%)
  5 (5.00%) high mild

Benchmarking AoSoA sum x coordinates: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 8.8s, enable flat sampling, or reduce sample count to 50.

# AoSoA x 좌표 합 벤치마크
AoSoA sum x coordinates time:   [1.7468 ms 1.7510 ms 1.7559 ms]
                        change: [−1.6810% −0.9152% −0.1729%] (p = 0.02 < 0.05)
                        Change within noise threshold.
Found 12 outliers among 100 measurements (12.00%)
  5 (5.00%) high mild
  7 (7.00%) high severe

결과를 보면 전반적으로 SoA -> AoSoA -> AoS 순으로 성능이 좋다. 벤치마크 코드는 블로그 깃헙에서 확인할 수 있다.

참조