awesome-everything EN
↑ Обратно к восхождению

Производительность

SIMD и data layout: AoS vs SoA и разница в 4–8x

Суть AVX2 обрабатывает 8 float за один цикл — но только если данные лежат правильно. AoS (array of structs) требует gather, SoA (struct of arrays) грузит 8 элементов за один aligned load. Layout решает, работает ли SIMD.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Hot ML inference loop в 5 раз медленнее reference C++ implementation, несмотря на идентичный алгоритм. Разница: reference хранит xs[], ys[], zs[]. Медленная версия хранит {x, y, z}[]. Изменение layout — не алгоритма — приносит 5x speedup.

SIMD register widths

Современные CPU имеют wide vector registers для параллельной обработки:

ISARegister widthFloat32 per opПлатформа
SSE4.2128 бит4x86 (всё)
AVX2256 бит8x86 (2013+)
AVX-512512 бит16x86 server (Skylake-SP+)
NEON128 бит4ARM (Apple Silicon, Android)
SVE2 / SME128–2048 бит4–64ARM (Apple M4, server)

Одна AVX2 инструкция multiply 8 float за 1 цикл — в 8x больше throughput, чем scalar. Но это работает только если 8 float лежат contiguous в памяти.

AoS vs SoA

Array of Structures (AoS) — интуитивный layout:

struct Point { float x, y, z; };
Point points[N];  // points[0].x, points[0].y, points[0].z, points[1].x, ...

Нужно сложить все x компоненты. AVX2 должен загрузить 8 x значений. Они находятся на адресах 0, 12, 24, 36, 48, 60, 72, 84 (stride 12 байт). Один vector load захватывает x0, y0, z0, x1, y1... — не то, что нужно. Нужен gather — 8 separate loads из несмежных адресов. Gather в 4–8x медленнее, чем aligned load.

Structure of Arrays (SoA):

float xs[N], ys[N], zs[N];  // xs[0], xs[1], xs[2], ... (contiguous)

Сложить все x: один AVX2 vmovups грузит xs[0..7] за один цикл. 8 float, один load. Prefetcher тоже счастлив — sequential access.

// AoS: 8 separate loads из случайных stride адресов
// SoA: один aligned load
__m256 vx = _mm256_loadu_ps(&xs[i]);  // грузит xs[i..i+7]
__m256 vy = _mm256_loadu_ps(&ys[i]);  // грузит ys[i..i+7]
__m256 result = _mm256_add_ps(vx, vy);  // 8 сложений за 1 цикл

Auto-vectorisation

Компиляторы auto-vectorise простые циклы в SIMD instructions при -O2/-O3, когда:

  • Нет pointer aliasing (используй restrict в C, Rust handles this automatically).
  • Loop trip count известен или предсказуем.
  • Нет data-dependent inner branches.
  • Data layout allows contiguous access.

Проверить, что компилятор emit-ил SIMD: -fopt-info-vec (GCC) или -Rpass=loop-vectorize (Clang).

Когда auto-vec fails: manual SIMD intrinsics (AVX2, NEON) или portable libraries (Highway, simd-everywhere).

Memory bandwidth как отдельный constraint

Cache hit rate — одна ось. Memory bandwidth — другая. Workload, streaming через 100 GB данных, bound RAM bandwidth (~50–100 GB/s на DDR5) независимо от cache locality.

Bandwidth-bound фикс: не locality, а снижение data volume:

  • Более компактные типы (float16 вместо float32 если precision позволяет).
  • On-the-fly computation вместо materialised tables.
  • Compression.

perf stat cache-misses vs mem-loads-retired.l3-miss разделяет: первый показывает locality-bound проблему, второй — bandwidth-bound.

NUMA

Servers с 2+ CPU sockets — Non-Uniform Memory Access: каждый socket имеет local RAM на ~70 нс, remote (other socket) RAM на ~120–150 нс.

Thread, аллоцирующий на socket 0, но работающий на socket 1, платит remote-access tax на каждой load. Митигации: pin threads на sockets (taskset, hwloc), аллоцируй на local NUMA node (numactl). Mis-configured 2-socket box может работать медленнее 1-socket для memory-bound workloads.

SIMD и memory bandwidth числа
AVX2 float throughput
8 float / цикл
AVX-512 float throughput
16 float / цикл
Gather vs aligned load
4–8x медленнее
DDR5-6000 bandwidth
~50 GB/s per channel
NUMA local vs remote RAM
70 нс vs 120–150 нс
AVX2 alignment
32-byte aligned load быстрее
Почему это работает

SIMD-friendly data alignment важен: AVX-512 требует 64-byte alignment для самых быстрых load инструкций; AVX2 — 32-byte; NEON — 16-byte. Misaligned access работает, но медленнее (split-load penalty). Для structs нужны атрибуты: alignas(64) в C++, #[repr(align(64))] в Rust. Для allocation: posix_memalign, std::aligned_alloc. Mismatch alignment — частая причина «SIMD не дал ожидаемого speedup». Verify alignment через (uintptr_t)ptr % 64 == 0 assertion перед SIMD load.

Викторина

Hot ML inference loop хранит activations как array of (x,y,z,w) структур (AoS). Reference хранит xs[], ys[], zs[], ws[] (SoA). Почему SoA в 4–8x быстрее для vector operations?

Викторина

Компания переходит с DDR4 на DDR5 (вдвое больше bandwidth) но streaming workload ускорился лишь на 15%, не на 100%. Почему?

Вспомните перед уходом
  1. 01
    Почему AoS layout defeat-ит SIMD, а SoA layout его enables?
  2. 02
    Как отличить cache-bound workload от bandwidth-bound?
Итог

SIMD registers (AVX2: 256 бит = 8 float) требуют contiguous однотипные данные. AoS интерлив-ит поля — SIMD вынужден gather (4–8x медленнее aligned load). SoA хранит каждое поле в dense array — один load, полный SIMD lane. Auto-vectorisation компилятором требует правильного layout, отсутствия aliasing, predictable trip count. Memory bandwidth и cache locality — разные constraints с разными фиксами: bandwidth-bound снижает data volume; cache-bound улучшает locality. NUMA добавляет latency 70% к remote socket: pin threads и аллоцируй на local node. SIMD alignment критичен: misaligned access работает, но с split-load penalty.

Связанные уроки
встречается в167
Продолжить восхождение ↑Hardware prefetcher, TLB и memory-level parallelism
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.