Sebelum mengelola data dalam skala besar, ada lapisan yang lebih kecil namun menentukan: bagaimana satu objek disusun byte demi byte. sizeof sebuah struct bukan sekadar penjumlahan ukuran member-nya — compiler menyelipkan byte pengisi (padding) agar setiap member berada di alamat yang memenuhi aturan alignment-nya, dan jumlah padding ini bergantung pada urutan member. Menata ulang member bisa memangkas ukuran struct tanpa mengubah isinya sedikit pun, dan ukuran inilah yang dikalikan satu juta kali ketika struct tersebut dijadikan elemen array. Chapter ini menghitung padding secara konkret, merangkum pembagian stack versus heap, lalu menutup dengan false sharing — masalah tata letak yang muncul saat program memakai beberapa thread.
Padding dan Alignment: Aturan Mainnya #
Setiap tipe punya requirement alignment: char cukup di alamat mana pun, int biasanya di kelipatan 4, dan double di kelipatan 8 (platform ini memakai alignof(double) = 8). Saat menyusun member, compiler menyisipkan padding agar setiap member jatuh di alamat kelipatan alignment-nya, lalu menambah padding penutup agar sizeof struct sendiri menjadi kelipatan alignment terbesar membernya — syarat agar array of struct tidak merusak alignment member di elemen berikutnya. Padding bukan pemborosan tak berarti, melainkan biaya agar akses member cukup satu instruksi; beberapa arsitektur bahkan melarang akses tidak sejajar sama sekali.
Mengukur Padding dengan sizeof #
Program berikut membandingkan struct dengan member yang sama persis, hanya beda urutan:
// Urutan member menentukan banyaknya padding — dan ukuran struct
// berubah hanya dengan menata ulang member. Semua angka di sini
// hasil penghitungan sizeof, deterministik di platform uji ini.
#include <iostream>
#include <new>
// Disusun tanpa memikirkan alignment: lubang padding bermunculan.
struct CatatanSalah {
char jenis; // offset 0, memakai 1 byte
double nilai; // butuh alignof(double) = 8 -> 7 byte padding di depan
int jumlah; // offset 16, 4 byte, lalu 4 byte padding penutup
};
// Disusun dari member terbesar ke terkecil: padding minim.
struct CatatanRapi {
double nilai; // offset 0, 8 byte
int jumlah; // offset 8, 4 byte
char jenis; // offset 12, 1 byte + 3 byte padding penutup
};
int main() {
std::cout << "alignof(double) = " << alignof(double) << '\n';
std::cout << "sizeof(CatatanSalah) = " << sizeof(CatatanSalah) << " byte\n";
std::cout << "sizeof(CatatanRapi) = " << sizeof(CatatanRapi) << " byte\n";
// Isi data sama-sama 13 byte (8 + 4 + 1); sisanya padding.
std::cout << "padding CatatanSalah = "
<< sizeof(CatatanSalah) - 13 << " byte\n";
std::cout << "padding CatatanRapi = "
<< sizeof(CatatanRapi) - 13 << " byte\n";
std::cout << "satu juta CatatanSalah = "
<< 1000000LL * sizeof(CatatanSalah) << " byte\n";
std::cout << "satu juta CatatanRapi = "
<< 1000000LL * sizeof(CatatanRapi) << " byte\n";
// Persiapan anti false sharing: dua penghitung pada thread berbeda.
struct Dempet { long a; long b; };
struct Terpisah { alignas(64) long a; alignas(64) long b; };
std::cout << "sizeof(Dempet) = " << sizeof(Dempet)
<< " byte (berbagi satu cache line)\n";
std::cout << "sizeof(Terpisah) = " << sizeof(Terpisah)
<< " byte (satu penghitung per cache line)\n";
std::cout << "hardware_destructive_interference_size = "
<< std::hardware_destructive_interference_size << '\n';
return 0;
}Output yang Dihasilkan:
alignof(double) = 8
sizeof(CatatanSalah) = 24 byte
sizeof(CatatanRapi) = 16 byte
padding CatatanSalah = 11 byte
padding CatatanRapi = 3 byte
satu juta CatatanSalah = 24000000 byte
satu juta CatatanRapi = 16000000 byte
sizeof(Dempet) = 16 byte (berbagi satu cache line)
sizeof(Terpisah) = 128 byte (satu penghitung per cache line)
hardware_destructive_interference_size = 64Bedah baris penting:
CatatanSalah= 24 byte:jenis(1 byte) diikuti 7 byte padding agarnilaijatuh di offset 8, lalu 4 byte padding penutup. Total padding 11 byte — lebih dari separuh ukurannya.CatatanRapi= 16 byte: member terbesar dulu, sehingga tidak ada lubang di tengah; padding tinggal 3 byte penutup. Isi data keduanya identik (13 byte), hanya urutannya yang berbeda.- Efek skala: satu juta elemen berarti 24.000.000 byte versus 16.000.000 byte — hemat 8.000.000 byte (~7,6 MB) hanya dengan menata ulang member. Memori yang lebih kecil juga berarti lebih sedikit cache line yang harus diperjalankan.
DempetvsTerpisah: dualongberdempet memakai 16 byte (satu cache line), sedangkanalignas(64)memaksa masing-masing menempati line sendiri (128 byte). Inilah bekalan untuk topik false sharing di bawah.
Aturan praktisnya sederhana dan bisa langsung Anda pakai: susun member dari yang terbesar ke terkecil (double/pointer dulu, int berikutnya, char/bool terakhir). Compiler tidak boleh menata ulang member untuk kita karena urutan member bagian dari kontrak layout yang bisa diandalkan kode lain (misalnya saat serialisasi atau interop dengan C).
Stack vs Heap: Dua Ruang Tata Letak #
Recap singkat dari pembahasan memori sebelumnya, kini dari sudut performa. Variabel lokal dan parameter disusun di stack: alokasinya secepat menggeser pointer stack, letaknya berdempet sehingga lokality-nya bagus, dan dibersihkan otomatis saat function return. Objek yang dibuat dengan new atau disimpan std::vector besar berada di heap: alokasinya mahal (allocator harus mencari lubang, mengunci struktur datanya), letaknya tersebar sehingga locality buruk, dan pembebasannya jadi tanggung jawab kita (biasanya lewat smart pointer). Implikasi performanya konsisten dengan chapter sebelumnya — utamakan objek stack dan kontainer berbasis blok kontinu, dan panggil new hanya saat memang perlu umur panjang atau ukuran yang tidak diketahui saat kompilasi.
False Sharing: Salem Satu Cache Line #
False sharing terjadi ketika dua thread menulis variabel yang berbeda tetapi kebetulan menumpang satu cache line yang sama. Hardware menjaga konsistensi cache per line, sehingga setiap penulisan thread A memaksa line ditarik ulang ke core B — dua variabel yang seharusnya bebas saling terkini menjadi berbagi antrean. Demonya klasik: dua penghitung yang dinaikkan dua thread, pertama berdempet dalam satu struct, lalu dipisah dengan alignas(64):
#include <chrono>
#include <iostream>
#include <thread>
#include <vector>
struct DuaPenghitung { long a = 0; long b = 0; };
struct DuaPenghitungJauh { alignas(64) long a = 0; alignas(64) long b = 0; };
template <typename T>
std::vector<long> uji(long putaran) {
std::vector<long> hasil;
for (int r = 0; r < 5; ++r) {
T c{};
auto mulai = std::chrono::steady_clock::now();
std::thread t1([&] { for (long i = 0; i < putaran; ++i) ++c.a; });
std::thread t2([&] { for (long i = 0; i < putaran; ++i) ++c.b; });
t1.join();
t2.join();
auto selesai = std::chrono::steady_clock::now();
hasil.push_back(std::chrono::duration_cast<std::chrono::milliseconds>(selesai - mulai).count());
}
return hasil;
}
int main() {
auto a = uji<DuaPenghitung>(200000000);
auto b = uji<DuaPenghitungJauh>(200000000);
auto tampil = [](const char* nama, const std::vector<long>& v) {
std::cout << nama;
for (long x : v) std::cout << ' ' << x;
std::cout << " ms\n";
};
tampil("berdempet :", a);
tampil("terpisah :", b);
return 0;
}Hasil Eksekusi (satu run nyata di platform uji, CPU 2 core; angka berubah antar run, urutan perbandingannya tidak):
berdempet : 495 483 497 548 547 ms
terpisah : 52 73 76 52 76 msKode yang sama, data yang sama-sama “tidak berbagi apa-apa” secara logika, tetapi versi berdempet melambat sekitar 8 kali hanya karena kedua long itu menumpang satu cache line. Perhatikan biayanya sekarang jelas: Terpisah memakai 128 byte versus 16 byte — kita sengaja memboroskan memori untuk membeli kecepatan pada program multithread. Pola ini terjadi nyata di produksi pada array of counter, array of flag, dan node yang diakses beberapa thread; obatnya selalu sama: pisahkan data per-thread hingga batas cache line, lalu gabungkan hasilnya di akhir.
Tata letak objek memberi kita alat mengatur memori per struct. Langkah berikutnya lebih besar: menata seluruh array data agar hot loop hanya menyentuh field yang benar-benar diperlukan — prinsip data-oriented design yang menjadi topik chapter berikutnya.
Rujukan resmi: non-static data members dan alignof .