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

  • SoA(Structure of Arrays)
  • AoS(Array of Structures)
  • AoSoA(Array of Structures of Arrays)

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

SoA (Structure of Arrays)

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

const N: usize = 100;

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

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

  • SIMD 연산이 필요할 때 (벡터 집약 연산이 필요할 떄)
  • 모든 데이터를 업데이트할 때 (CPU 캐시 히트율이 올라감)

단점도 있다.

  • 구조체 단위 접근 시 불리 (캐시 히트율이 떨어짐)
  • 가독성 저하 (코드 관리 및 데이터 관리가 어려움)

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

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 순으로 성능이 좋다. 벤치마크 코드는 블로그 깃헙에서 확인할 수 있다.

참조