Jump to content

SoA vs AoS: Difference between revisions

From Haidhi Wiki
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 [[HPC|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 name="drepper" /><ref name="cuda" />
'''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''.<ref name="fabian" /><ref name="acton" />
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).<ref name="drepper" /> Kalau isi boks itu kebanyakan data yang tidak dipakai, bandwidth memori terbuang percuma.
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.<ref name="drepper" /><ref name="hp" />
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.<ref name="agner" /><ref name="intelopt" />
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.<ref name="agner" />
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.<ref name="cuda" />
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&lt;N&gt;</code>)<ref name="ispc" /> dan banyak kode simulasi partikel/MD.
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&lt;N&gt;</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>.<ref name="ispc" />
* 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 ==


<references>
* Ulrich Drepper, ''What Every Programmer Should Know About Memory'', Red Hat, Inc., 2007 — https://people.freebsd.org/~lstewart/articles/cpumemory.pdf
<ref name="drepper">Ulrich Drepper, ''What Every Programmer Should Know About Memory'', Red Hat, Inc., 2007. https://people.freebsd.org/~lstewart/articles/cpumemory.pdf</ref>
* NVIDIA, ''CUDA C++ Best Practices Guide'' — bagian "Coalesced Access to Global Memory" — https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/
<ref name="cuda">NVIDIA, ''CUDA C++ Best Practices Guide'' — bagian "Coalesced Access to Global Memory". https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/</ref>
* Agner Fog, ''Optimizing Software in C++: An Optimization Guide for Windows, Linux, and Mac Platforms'' — https://www.agner.org/optimize/
<ref name="agner">Agner Fog, ''Optimizing Software in C++: An Optimization Guide for Windows, Linux, and Mac Platforms''. https://www.agner.org/optimize/</ref>
* Intel Corporation, ''Intel 64 and IA-32 Architectures Optimization Reference Manual'' — pembahasan data layout dan vectorization.
<ref name="intelopt">Intel Corporation, ''Intel 64 and IA-32 Architectures Optimization Reference Manual'' — pembahasan data layout dan vectorization.</ref>
* Richard Fabian, ''Data-Oriented Design: Software Engineering for Limited Resources and Short Schedules'', 2018 — https://www.dataorienteddesign.com/dodbook/
<ref name="fabian">Richard Fabian, ''Data-Oriented Design: Software Engineering for Limited Resources and Short Schedules'', 2018. https://www.dataorienteddesign.com/dodbook/</ref>
* Mike Acton, "Data-Oriented Design and C++", CppCon 2014 — https://www.youtube.com/watch?v=rX0ItVEVjHc
<ref name="acton">Mike Acton, "Data-Oriented Design and C++", CppCon 2014. https://www.youtube.com/watch?v=rX0ItVEVjHc</ref>
* Intel, ''ISPC User's Guide'' — "Structure of Array Types" — https://ispc.github.io/ispc.html
<ref name="ispc">Intel, ''ISPC User's Guide'' — "Structure of Array Types". https://ispc.github.io/ispc.html</ref>
* John L. Hennessy & David A. Patterson, ''Computer Architecture: A Quantitative Approach'', 6th Edition, Morgan Kaufmann, 2017 — bab Memory Hierarchy Design.
<ref name="hp">John L. Hennessy & David A. Patterson, ''Computer Architecture: A Quantitative Approach'', 6th Edition, Morgan Kaufmann, 2017 — bab Memory Hierarchy Design.</ref>
</references>


[[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 float4 bisa jadi kompromi karena di-load sebagai satu transaksi 16 byte.
  • Library yang membantu: Kokkos (Kokkos::View bisa ganti LayoutLeft/LayoutRight tanpa mengubah kode kernel), Intel SDLT (SIMD Data Layout Templates), dan ISPC dengan tipe soa.
  • 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

Kategori:HPC Kategori:Optimasi Performa Kategori:Arsitektur Komputer