SoA vs AoS: Difference between revisions
Created page with "'''Array of Structures (AoS)''' dan '''Structure of Arrays (SoA)''' adalah dua cara menyusun (me-''layout'') data di memori. Keduanya menyimpan informasi yang sama persis, tapi urutan byte-nya di RAM berbeda. Di dunia High Performance Computing, perbedaan "sepele" ini bisa berdampak besar ke performa — sering kali 2x sampai 8x lipat — karena berhubungan langsung dengan cara kerja '''cache''', '''SIMD/vektorisasi''', dan '''memory coalescing''' di GPU.<ref nam..." |
No edit summary |
||
| Line 1: | Line 1: | ||
'''Array of Structures (AoS)''' dan '''Structure of Arrays (SoA)''' adalah dua cara menyusun (me-''layout'') data di memori. Keduanya menyimpan informasi yang sama persis, tapi urutan byte-nya di RAM berbeda. Di dunia [[ | '''Array of Structures (AoS)''' dan '''Structure of Arrays (SoA)''' adalah dua cara menyusun (me-''layout'') data di memori. Keduanya menyimpan informasi yang sama persis, tapi urutan byte-nya di RAM berbeda. Di dunia [[High Performance Computing|HPC]], perbedaan "sepele" ini bisa berdampak besar ke performa — sering kali 2x sampai 8x lipat — karena berhubungan langsung dengan cara kerja '''cache''', '''SIMD/vektorisasi''', dan '''memory coalescing''' di GPU. | ||
Prinsip utamanya satu kalimat: '''susun data mengikuti pola aksesnya, bukan mengikuti "kerapian" konsep OOP'''. Ini adalah ide inti dari ''data-oriented design''. | Prinsip utamanya satu kalimat: '''susun data mengikuti pola aksesnya, bukan mengikuti "kerapian" konsep OOP'''. Ini adalah ide inti dari ''data-oriented design''. | ||
== Analogi Sederhana == | == Analogi Sederhana == | ||
| Line 15: | Line 15: | ||
* Dengan SoA: tinggal ambil '''satu''' buku "Nilai Matematika", baca dari atas ke bawah. Selesai. | * Dengan SoA: tinggal ambil '''satu''' buku "Nilai Matematika", baca dari atas ke bawah. Selesai. | ||
CPU bekerja persis seperti ini. Setiap kali CPU mengambil data dari RAM, dia tidak mengambil 1 byte, melainkan '''satu boks penuh''' yang disebut '''cache line''' (umumnya 64 byte). | CPU bekerja persis seperti ini. Setiap kali CPU mengambil data dari RAM, dia tidak mengambil 1 byte, melainkan '''satu boks penuh''' yang disebut '''cache line''' (umumnya 64 byte). Kalau isi boks itu kebanyakan data yang tidak dipakai, bandwidth memori terbuang percuma. | ||
== Array of Structures (AoS) == | == Array of Structures (AoS) == | ||
| Line 110: | Line 110: | ||
</pre> | </pre> | ||
Karena mayoritas kernel HPC bersifat ''memory-bound'' (bottleneck-nya di bandwidth memori, bukan di ALU), efisiensi cache line ini langsung menentukan performa akhir. | Karena mayoritas kernel HPC bersifat ''memory-bound'' (bottleneck-nya di bandwidth memori, bukan di ALU), efisiensi cache line ini langsung menentukan performa akhir. | ||
=== 2. Vektorisasi (SIMD) === | === 2. Vektorisasi (SIMD) === | ||
Instruksi SIMD (SSE = 4 float, AVX2 = 8 float, AVX-512 = 16 float) paling senang kalau datanya '''berurutan rapat''' di memori — cukup satu instruksi ''load'' untuk mengisi satu register penuh. | Instruksi SIMD (SSE = 4 float, AVX2 = 8 float, AVX-512 = 16 float) paling senang kalau datanya '''berurutan rapat''' di memori — cukup satu instruksi ''load'' untuk mengisi satu register penuh. | ||
<pre> | <pre> | ||
| Line 133: | Line 133: | ||
</pre> | </pre> | ||
Efek praktisnya: compiler (GCC/Clang/ICC) jauh lebih gampang melakukan '''auto-vectorization''' pada loop SoA. Loop AoS sering gagal divektorisasi, atau divektorisasi dengan instruksi ''gather'' yang mahal. | Efek praktisnya: compiler (GCC/Clang/ICC) jauh lebih gampang melakukan '''auto-vectorization''' pada loop SoA. Loop AoS sering gagal divektorisasi, atau divektorisasi dengan instruksi ''gather'' yang mahal. | ||
=== 3. GPU: Coalesced Memory Access === | === 3. GPU: Coalesced Memory Access === | ||
Di GPU (CUDA/OpenCL), 32 thread dalam satu ''warp'' (NVIDIA) atau ''wavefront'' (AMD) mengeksekusi instruksi load secara bersamaan. Kalau alamat yang diakses ke-32 thread itu '''berurutan''', hardware menggabungkannya (''coalesce'') menjadi sedikit transaksi memori saja. | Di GPU (CUDA/OpenCL), 32 thread dalam satu ''warp'' (NVIDIA) atau ''wavefront'' (AMD) mengeksekusi instruksi load secara bersamaan. Kalau alamat yang diakses ke-32 thread itu '''berurutan''', hardware menggabungkannya (''coalesce'') menjadi sedikit transaksi memori saja. | ||
<pre> | <pre> | ||
| Line 200: | Line 200: | ||
</pre> | </pre> | ||
Kelebihan: load SIMD tetap berurutan, tapi field-field satu elemen masih "berdekatan" di memori. Kekurangan: indexing jadi lebih ribet. Layout ini dipakai antara lain oleh compiler [[ISPC]] (tipe <code>soa<N></code>) | Kelebihan: load SIMD tetap berurutan, tapi field-field satu elemen masih "berdekatan" di memori. Kekurangan: indexing jadi lebih ribet. Layout ini dipakai antara lain oleh compiler [[ISPC]] (tipe <code>soa<N></code>) dan banyak kode simulasi partikel/MD. | ||
== Contoh Praktis: Mengukur Sendiri == | == Contoh Praktis: Mengukur Sendiri == | ||
| Line 266: | Line 266: | ||
* Beri tahu compiler bahwa pointer tidak saling tumpang tindih dengan <code>__restrict__</code> — sering jadi penentu berhasil/tidaknya auto-vectorization. | * Beri tahu compiler bahwa pointer tidak saling tumpang tindih dengan <code>__restrict__</code> — sering jadi penentu berhasil/tidaknya auto-vectorization. | ||
* Di CUDA/OpenCL: field yang diakses per-thread hampir selalu lebih cepat dalam bentuk SoA; tipe vektor bawaan seperti <code>float4</code> bisa jadi kompromi karena di-load sebagai satu transaksi 16 byte. | * Di CUDA/OpenCL: field yang diakses per-thread hampir selalu lebih cepat dalam bentuk SoA; tipe vektor bawaan seperti <code>float4</code> bisa jadi kompromi karena di-load sebagai satu transaksi 16 byte. | ||
* Library yang membantu: [[Kokkos]] (<code>Kokkos::View</code> bisa ganti <code>LayoutLeft</code>/<code>LayoutRight</code> tanpa mengubah kode kernel), Intel '''SDLT''' (SIMD Data Layout Templates), dan ISPC dengan tipe <code>soa</code>. | * Library yang membantu: [[Kokkos]] (<code>Kokkos::View</code> bisa ganti <code>LayoutLeft</code>/<code>LayoutRight</code> tanpa mengubah kode kernel), Intel '''SDLT''' (SIMD Data Layout Templates), dan ISPC dengan tipe <code>soa</code>. | ||
* Ingat trade-off-nya: SoA menukar kenyamanan kode dengan bandwidth. Untuk kode non-kritis, AoS yang enak dibaca sering kali pilihan yang benar. | * Ingat trade-off-nya: SoA menukar kenyamanan kode dengan bandwidth. Untuk kode non-kritis, AoS yang enak dibaca sering kali pilihan yang benar. | ||
| Line 280: | Line 280: | ||
== Referensi == | == Referensi == | ||
* Ulrich Drepper, ''What Every Programmer Should Know About Memory'', Red Hat, Inc., 2007 — https://people.freebsd.org/~lstewart/articles/cpumemory.pdf | |||
* NVIDIA, ''CUDA C++ Best Practices Guide'' — bagian "Coalesced Access to Global Memory" — https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/ | |||
* Agner Fog, ''Optimizing Software in C++: An Optimization Guide for Windows, Linux, and Mac Platforms'' — https://www.agner.org/optimize/ | |||
* Intel Corporation, ''Intel 64 and IA-32 Architectures Optimization Reference Manual'' — pembahasan data layout dan vectorization. | |||
* Richard Fabian, ''Data-Oriented Design: Software Engineering for Limited Resources and Short Schedules'', 2018 — https://www.dataorienteddesign.com/dodbook/ | |||
* Mike Acton, "Data-Oriented Design and C++", CppCon 2014 — https://www.youtube.com/watch?v=rX0ItVEVjHc | |||
* Intel, ''ISPC User's Guide'' — "Structure of Array Types" — https://ispc.github.io/ispc.html | |||
* John L. Hennessy & David A. Patterson, ''Computer Architecture: A Quantitative Approach'', 6th Edition, Morgan Kaufmann, 2017 — bab Memory Hierarchy Design. | |||
[[Kategori:HPC]] | [[Kategori:HPC]] | ||
[[Kategori:Optimasi Performa]] | [[Kategori:Optimasi Performa]] | ||
[[Kategori:Arsitektur Komputer]] | [[Kategori:Arsitektur Komputer]] | ||
Revision as of 07:32, 3 July 2026
Array of Structures (AoS) dan Structure of Arrays (SoA) adalah dua cara menyusun (me-layout) data di memori. Keduanya menyimpan informasi yang sama persis, tapi urutan byte-nya di RAM berbeda. Di dunia HPC, perbedaan "sepele" ini bisa berdampak besar ke performa — sering kali 2x sampai 8x lipat — karena berhubungan langsung dengan cara kerja cache, SIMD/vektorisasi, dan memory coalescing di GPU.
Prinsip utamanya satu kalimat: susun data mengikuti pola aksesnya, bukan mengikuti "kerapian" konsep OOP. Ini adalah ide inti dari data-oriented design.
Analogi Sederhana
Bayangkan sekolah dengan 1.000 siswa, dan tiap siswa punya data: nama, nilai Matematika, nilai Fisika, nilai Kimia.
- AoS = satu map arsip per siswa. Map si Budi berisi lengkap: nama Budi, nilai Mat Budi, nilai Fisika Budi, nilai Kimia Budi.
- SoA = satu buku besar per jenis data. Ada buku khusus "Nilai Matematika seluruh siswa", buku "Nilai Fisika seluruh siswa", dst.
Sekarang tugasnya: hitung rata-rata nilai Matematika seluruh sekolah.
- Dengan AoS: kamu harus buka 1.000 map satu per satu, ambil selembar nilai Matematika, dan sisanya (nama, Fisika, Kimia) ikut terangkat dari rak padahal tidak dipakai. Boros tenaga.
- Dengan SoA: tinggal ambil satu buku "Nilai Matematika", baca dari atas ke bawah. Selesai.
CPU bekerja persis seperti ini. Setiap kali CPU mengambil data dari RAM, dia tidak mengambil 1 byte, melainkan satu boks penuh yang disebut cache line (umumnya 64 byte). Kalau isi boks itu kebanyakan data yang tidak dipakai, bandwidth memori terbuang percuma.
Array of Structures (AoS)
Ini gaya yang paling natural kalau kita terbiasa OOP: definisikan satu struct untuk satu objek, lalu buat array dari struct itu.
struct Particle {
float x, y, z; // posisi
float mass; // massa
}; // total: 16 byte per partikel
Particle p[N]; // <-- Array of Structures
// Contoh: jumlahkan semua koordinat x
float sum = 0.0f;
for (int i = 0; i < N; ++i)
sum += p[i].x; // tiap iterasi "loncat" 16 byte
Layout di Memori
Semua field satu partikel berdempetan, lalu disusul partikel berikutnya:
Alamat memori bertambah ke kanan ---> +----+----+----+----+----+----+----+----+----+----+----+----+ | x0 | y0 | z0 | m0 | x1 | y1 | z1 | m1 | x2 | y2 | z2 | m2 | ... +----+----+----+----+----+----+----+----+----+----+----+----+ \_____Partikel 0______/ \_____Partikel 1______/ \__Partikel 2 ...
Structure of Arrays (SoA)
Kebalikannya: satu array terpisah untuk tiap field.
struct Particles {
float x[N];
float y[N];
float z[N];
float mass[N];
}; // <-- Structure of Arrays
Particles p;
// Contoh: jumlahkan semua koordinat x
float sum = 0.0f;
for (int i = 0; i < N; ++i)
sum += p.x[i]; // akses 100% berurutan (contiguous)
Layout di Memori
x[] : +----+----+----+----+----+----+
| x0 | x1 | x2 | x3 | x4 | x5 | ...
+----+----+----+----+----+----+
y[] : +----+----+----+----+----+----+
| y0 | y1 | y2 | y3 | y4 | y5 | ...
+----+----+----+----+----+----+
z[] : | z0 | z1 | z2 | ...
mass[] : | m0 | m1 | m2 | ...
Kenapa Ini Penting di HPC
1. Efisiensi Cache Line
Cache line modern = 64 byte = muat 16 buah float. Perhatikan apa yang terjadi kalau loop kita hanya butuh field x:
Kasus AoS -- 1 cache line (64 byte) berisi 4 partikel utuh: +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ | x0 | y0 | z0 | m0 | x1 | y1 | z1 | m1 | x2 | y2 | z2 | m2 | x3 | y3 | z3 | m3 | +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ ^^ ^^ ^^ ^^ dipakai dipakai dipakai dipakai Hanya 4 dari 16 float yang dipakai --> efisiensi bandwidth 25% (75% data yang diangkut dari RAM dibuang sia-sia) Kasus SoA -- 1 cache line berisi 16 nilai x sekaligus: +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ | x0 | x1 | x2 | x3 | x4 | x5 | x6 | x7 | x8 | x9 |x10 |x11 |x12 |x13 |x14 |x15 | +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ ^^^^ semua 16 float dipakai --> efisiensi bandwidth 100%
Karena mayoritas kernel HPC bersifat memory-bound (bottleneck-nya di bandwidth memori, bukan di ALU), efisiensi cache line ini langsung menentukan performa akhir.
2. Vektorisasi (SIMD)
Instruksi SIMD (SSE = 4 float, AVX2 = 8 float, AVX-512 = 16 float) paling senang kalau datanya berurutan rapat di memori — cukup satu instruksi load untuk mengisi satu register penuh.
SoA + AVX2 (register 256-bit = 8 float):
memori x[] : | x0 | x1 | x2 | x3 | x4 | x5 | x6 | x7 |
| | | | | | | |
v v v v v v v v <- 1 instruksi
register : | x0 | x1 | x2 | x3 | x4 | x5 | x6 | x7 | (vmovups)
AoS + AVX2:
memori : | x0 | y0 | z0 | m0 | x1 | y1 | z1 | m1 | ...
| |
+--- perlu gather / banyak shuffle ---+
(lambat, boros instruksi)
Efek praktisnya: compiler (GCC/Clang/ICC) jauh lebih gampang melakukan auto-vectorization pada loop SoA. Loop AoS sering gagal divektorisasi, atau divektorisasi dengan instruksi gather yang mahal.
3. GPU: Coalesced Memory Access
Di GPU (CUDA/OpenCL), 32 thread dalam satu warp (NVIDIA) atau wavefront (AMD) mengeksekusi instruksi load secara bersamaan. Kalau alamat yang diakses ke-32 thread itu berurutan, hardware menggabungkannya (coalesce) menjadi sedikit transaksi memori saja.
Warp = 32 thread. Thread ke-t membaca posisi x milik elemen ke-t.
SoA:
thread : t0 t1 t2 t3 ... t31
alamat : x[0] x[1] x[2] x[3] ... x[31]
\__________ 128 byte berurutan __________/
=> digabung jadi transaksi memori minimal (coalesced)
AoS (stride 16 byte):
thread : t0 t1 t2 ...
alamat : p[0].x p[1].x p[2].x ...
(loncat-loncat 16 byte antar thread)
=> butuh transaksi memori jauh lebih banyak,
bandwidth efektif anjlok
Karena itu, hampir semua kode GPU yang serius memakai layout SoA untuk data yang diproses per-elemen. Ini juga berlaku persis sama untuk kernel OpenCL.
Kapan AoS Justru Lebih Baik
SoA bukan jawaban universal. AoS unggul kalau pola aksesnya "semua field dari satu elemen sekaligus":
- Akses acak (random access) per objek — misalnya collision detection: begitu ketemu partikel ke-i, kamu butuh x, y, z, dan mass-nya sekaligus. Dengan AoS, satu cache line langsung membawa seluruh objek itu. Dengan SoA, kamu malah kena 4 cache miss di 4 array berbeda.
- Objek sering di-insert/delete/dipindah sebagai satu kesatuan.
- Data di-serialize atau dikirim per-objek (misalnya lewat jaringan atau MPI dengan datatype per-struct).
- Kode lebih mudah dibaca dan di-maintain — jangan remehkan ini untuk kode yang bukan hotspot.
Aturan praktisnya:
Loop menyentuh SATU field dari BANYAK elemen --> SoA Loop menyentuh SEMUA field dari SATU elemen --> AoS
AoSoA: Jalan Tengah
Array of Structures of Arrays (AoSoA), kadang disebut tiled SoA, menggabungkan keduanya: data dipecah jadi blok-blok kecil selebar register SIMD, dan di dalam blok disusun ala SoA.
constexpr int W = 8; // lebar SIMD (AVX2 = 8 float)
struct ParticleBlock {
float x[W], y[W], z[W], mass[W];
};
std::vector<ParticleBlock> blocks(N / W); // <-- AoSoA
// Akses partikel ke-i:
// blocks[i / W].x[i % W]
+-------------------------------+-------------------------------+ | Blok 0 | Blok 1 | | x0..x7 | y0..y7 | z0..z7 | m..| x8..x15| y8..y15| z8..z15| ...| +-------------------------------+-------------------------------+ ^ di dalam blok: SoA (ramah SIMD) ^ antar blok: AoS (lokalitas objek tetap lumayan)
Kelebihan: load SIMD tetap berurutan, tapi field-field satu elemen masih "berdekatan" di memori. Kekurangan: indexing jadi lebih ribet. Layout ini dipakai antara lain oleh compiler ISPC (tipe soa<N>) dan banyak kode simulasi partikel/MD.
Contoh Praktis: Mengukur Sendiri
Benchmark mini untuk membuktikan efeknya (kompilasi dengan -O3 -march=native):
#include <chrono>
#include <cstdio>
#include <vector>
constexpr int N = 1 << 24; // ~16 juta elemen
struct ParticleAoS { float x, y, z, mass; };
int main() {
std::vector<ParticleAoS> aos(N, {1, 2, 3, 4});
std::vector<float> soa_x(N, 1.0f);
// --- AoS: jumlahkan x ---
auto t0 = std::chrono::steady_clock::now();
float s1 = 0;
for (int i = 0; i < N; ++i) s1 += aos[i].x;
auto t1 = std::chrono::steady_clock::now();
// --- SoA: jumlahkan x ---
float s2 = 0;
for (int i = 0; i < N; ++i) s2 += soa_x[i];
auto t2 = std::chrono::steady_clock::now();
auto ms = [](auto a, auto b) {
return std::chrono::duration<double, std::milli>(b - a).count();
};
std::printf("AoS: %.2f ms | SoA: %.2f ms (s1=%f s2=%f)\n",
ms(t0, t1), ms(t1, t2), s1, s2);
}
Yang perlu diamati: loop SoA membaca 64 MB, loop AoS harus "menyeret" 256 MB dari RAM untuk hasil yang sama — selisih waktunya biasanya mendekati rasio bandwidth itu (sekitar 3–4x) pada dataset yang jauh lebih besar dari cache. Cek juga laporan vektorisasi compiler dengan -fopt-info-vec (GCC) atau -Rpass=loop-vectorize (Clang).
Tabel Perbandingan
| Aspek | AoS | SoA | AoSoA |
|---|---|---|---|
| Pola akses ideal | Semua field dari 1 elemen | 1 field dari banyak elemen | Streaming + butuh beberapa field |
| Auto-vectorization (SIMD) | Susah (gather/shuffle) | Mudah (load berurutan) | Mudah |
| GPU memory coalescing | Buruk | Sangat baik | Baik |
| Efisiensi cache (akses parsial) | Rendah (bisa 25% atau kurang) | ~100% | Tinggi |
| Efisiensi cache (akses seluruh objek) | Tinggi | Rendah (miss di banyak array) | Sedang–tinggi |
| Keterbacaan / kenyamanan kode | Paling natural (OOP) | Agak repot | Paling repot (indexing 2 tingkat) |
| Insert/delete elemen | Mudah | Repot (semua array harus sinkron) | Repot |
Tips Praktis
- Profiling dulu. Ubah layout hanya untuk struktur data di hotspot. Mengubah layout itu mahal secara refactoring.
- Align array SoA ke 64 byte (
alignas(64),std::aligned_alloc) supaya load SIMD aligned dan tidak membelah cache line. - Beri tahu compiler bahwa pointer tidak saling tumpang tindih dengan
__restrict__— sering jadi penentu berhasil/tidaknya auto-vectorization. - Di CUDA/OpenCL: field yang diakses per-thread hampir selalu lebih cepat dalam bentuk SoA; tipe vektor bawaan seperti
float4bisa jadi kompromi karena di-load sebagai satu transaksi 16 byte. - Library yang membantu: Kokkos (
Kokkos::Viewbisa gantiLayoutLeft/LayoutRighttanpa mengubah kode kernel), Intel SDLT (SIMD Data Layout Templates), dan ISPC dengan tipesoa. - Ingat trade-off-nya: SoA menukar kenyamanan kode dengan bandwidth. Untuk kode non-kritis, AoS yang enak dibaca sering kali pilihan yang benar.
Lihat Juga
Referensi
- Ulrich Drepper, What Every Programmer Should Know About Memory, Red Hat, Inc., 2007 — https://people.freebsd.org/~lstewart/articles/cpumemory.pdf
- NVIDIA, CUDA C++ Best Practices Guide — bagian "Coalesced Access to Global Memory" — https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/
- Agner Fog, Optimizing Software in C++: An Optimization Guide for Windows, Linux, and Mac Platforms — https://www.agner.org/optimize/
- Intel Corporation, Intel 64 and IA-32 Architectures Optimization Reference Manual — pembahasan data layout dan vectorization.
- Richard Fabian, Data-Oriented Design: Software Engineering for Limited Resources and Short Schedules, 2018 — https://www.dataorienteddesign.com/dodbook/
- Mike Acton, "Data-Oriented Design and C++", CppCon 2014 — https://www.youtube.com/watch?v=rX0ItVEVjHc
- Intel, ISPC User's Guide — "Structure of Array Types" — https://ispc.github.io/ispc.html
- John L. Hennessy & David A. Patterson, Computer Architecture: A Quantitative Approach, 6th Edition, Morgan Kaufmann, 2017 — bab Memory Hierarchy Design.
Kategori:HPC Kategori:Optimasi Performa Kategori:Arsitektur Komputer