(Jangan menerapkan aturan ini tanpa berpikir. Lihat poin ESR tentang lokalitas cache untuk anggota yang Anda gunakan bersama. Dan dalam program multi-utas, berhati-hatilah dalam berbagi anggota yang ditulis oleh utas yang berbeda. Umumnya Anda tidak ingin data per-utas masuk satu struct sama sekali karena alasan ini, kecuali jika Anda melakukannya untuk mengontrol pemisahan dengan alignas(128)
yang besar. Ini berlaku untuk atomic
dan var non-atom; yang penting adalah thread menulis ke baris cache terlepas dari bagaimana mereka melakukannya.)
Aturan praktis: terbesar hingga terkecil alignof()
. Tidak ada yang dapat Anda lakukan dengan sempurna di mana pun, namun sejauh ini kasus paling umum saat ini adalah implementasi C++ normal yang wajar untuk CPU normal 32 atau 64-bit. Semua tipe primitif memiliki ukuran pangkat 2.
Kebanyakan tipe memiliki alignof(T) = sizeof(T)
, atau alignof(T)
yang dibatasi pada lebar register implementasi. Jadi tipe yang lebih besar biasanya lebih selaras dibandingkan tipe yang lebih kecil.
Aturan pengepakan struktur di sebagian besar ABI memberi anggota struct penyelarasan alignof(T)
absolut mereka relatif terhadap awal struct, dan struct itu sendiri mewarisi alignof()
terbesar dari semua anggotanya.
Utamakan anggota yang selalu 64-bit (seperti double
, long long
, dan int64_t
). ISO C++ tentu saja tidak memperbaiki jenis ini pada 64 bit / 8 byte, tetapi dalam praktiknya pada semua CPU yang Anda pedulikan, hal tersebut berlaku. Orang yang mem-porting kode Anda ke CPU eksotik dapat mengubah tata letak struct untuk mengoptimalkannya jika diperlukan.
lalu pointer dan bilangan bulat lebar pointer: size_t
, intptr_t
, dan ptrdiff_t
(bisa berukuran 32 atau 64-bit). Ini semua memiliki lebar yang sama pada implementasi C++ modern normal untuk CPU dengan model memori datar.
Pertimbangkan untuk menempatkan daftar tertaut dan penunjuk kiri/kanan pohon terlebih dahulu jika Anda peduli dengan x86 dan CPU Intel. Pengejaran pointer melalui node di pohon atau daftar tertaut memiliki penalti ketika alamat awal struct berada di halaman 4k berbeda dari anggota yang Anda akses. Mendahulukan mereka menjamin hal itu tidak akan terjadi.
lalu long
(yang terkadang 32-bit meskipun pointernya 64-bit, di ABI LLP64 seperti Windows x64). Tapi dijamin setidaknya selebar int
.
lalu 32-bit int32_t
, int
, float
, enum
. (Secara opsional, pisahkan int32_t
dan float
di depan int
jika Anda peduli dengan kemungkinan sistem 8/16-bit yang masih memasukkan tipe tersebut ke 32-bit, atau bekerja lebih baik dengan penyelarasan alaminya. Sebagian besar sistem seperti itu tidak memiliki beban yang lebih luas (FPU atau SIMD) jadi tipe yang lebih luas harus ditangani sebagai beberapa bagian terpisah sepanjang waktu).
ISO C++ memungkinkan int
menjadi sesempit 16 bit, atau lebar sewenang-wenang, namun dalam praktiknya, ini adalah tipe 32-bit bahkan pada CPU 64-bit. Perancang ABI menemukan bahwa program yang dirancang untuk bekerja dengan int
32-bit hanya membuang-buang memori (dan jejak cache) jika int
lebih luas. Jangan membuat asumsi yang akan menyebabkan masalah kebenaran, tapi untuk kinerja portabel Anda hanya harus benar dalam kasus normal.
Orang yang menyetel kode Anda untuk platform eksotik dapat melakukan penyesuaian jika perlu. Jika tata letak struct tertentu sangat penting bagi kinerja, mungkin komentari asumsi dan alasan Anda di header.
lalu short
/ int16_t
lalu char
/ int8_t
/ bool
(untuk beberapa tanda bool
, terutama jika sebagian besar dibaca atau jika semuanya dimodifikasi bersama-sama, pertimbangkan untuk mengemasnya dengan bitfield 1-bit.)
(Untuk tipe bilangan bulat yang tidak ditandatangani, temukan tipe bertanda tangan yang sesuai di daftar saya.)
array kelipatan 8 byte dengan tipe yang lebih sempit dapat dibuat lebih awal jika Anda menginginkannya. Namun jika Anda tidak mengetahui ukuran pasti jenisnya, Anda tidak dapat menjamin bahwa int i
+ char buf[4]
akan mengisi slot sejajar 8 byte antara dua double
s. Tapi itu bukan asumsi yang buruk, jadi saya akan tetap melakukannya jika ada alasan (seperti lokalitas spasial dari anggota yang diakses bersama) untuk menyatukannya, bukan di akhir.
Tipe eksotik: x86-64 System V memiliki alignof(long double) = 16
, namun i386 System V hanya memiliki alignof(long double) = 4
, sizeof(long double) = 12
. Ini adalah tipe x87 80-bit, yang sebenarnya berukuran 10 byte tetapi diisi menjadi 12 atau 16 sehingga merupakan kelipatan dari alignof-nya, sehingga memungkinkan array tanpa melanggar jaminan penyelarasan.
Dan secara umum akan menjadi lebih rumit bila anggota struct Anda sendiri merupakan agregat (struct atau gabungan) dengan sizeof(x) != alignof(x)
.
Perubahan lainnya adalah di beberapa ABI (misalnya Windows 32-bit jika saya ingat dengan benar) anggota struct diselaraskan dengan ukurannya (hingga 8 byte) relatif terhadap awal struct, meskipun alignof(T)
adalah masih hanya 4 untuk double
dan int64_t
.
Hal ini untuk mengoptimalkan kasus umum alokasi terpisah memori selaras 8-byte untuk satu struct, tanpa memberikan jaminan keselarasan. i386 System V juga memiliki alignof(T) = 4
yang sama untuk sebagian besar tipe primitif (tetapi malloc
masih memberi Anda memori selaras 8-byte karena alignof(maxalign_t) = 8
). Tapi bagaimanapun, i386 System V tidak memiliki aturan pengepakan struct, jadi (jika Anda tidak mengatur struct Anda dari yang terbesar ke terkecil) Anda bisa mendapatkan anggota 8-byte yang kurang sejajar dengan awal struct .
Kebanyakan CPU mempunyai mode pengalamatan yang, dengan adanya pointer dalam register, memungkinkan akses ke offset byte apa pun. Offset maks biasanya sangat besar, tetapi pada x86 ini menghemat ukuran kode jika offset byte cocok dengan byte yang ditandatangani ([-128 .. +127]
). Jadi, jika Anda memiliki array apa pun yang besar, lebih baik meletakkannya nanti di struct setelah anggota yang sering digunakan. Meskipun ini membutuhkan sedikit biaya tambahan.
Kompiler Anda akan selalu membuat kode yang memiliki alamat struct di register, bukan alamat di tengah struct untuk memanfaatkan perpindahan negatif pendek.
Eric S. Raymond menulis artikel Seni Pengemasan Struktur yang Hilang. Khususnya bagian Penataan ulang struktur pada dasarnya adalah jawaban atas pertanyaan ini.
Dia juga menyampaikan poin penting lainnya:
9. Keterbacaan dan lokalitas cache
Meskipun menyusun ulang berdasarkan ukuran adalah cara paling sederhana untuk menghilangkan kekotoran, hal ini belum tentu merupakan hal yang benar. Ada dua masalah lagi: keterbacaan dan lokalitas cache.
Dalam struct besar yang dapat dengan mudah dipecah melintasi batas baris cache, masuk akal untuk meletakkan 2 hal di dekatnya jika keduanya selalu digunakan bersama. Atau bahkan berdekatan untuk memungkinkan penggabungan muatan/penyimpanan, mis. menyalin 8 atau 16 byte dengan satu bilangan bulat (tidak selaras) atau memuat/menyimpan SIMD alih-alih memuat anggota yang lebih kecil secara terpisah.
Baris cache biasanya berukuran 32 atau 64 byte pada CPU modern. (Pada x86 modern, selalu 64 byte. Dan keluarga Sandybridge memiliki prefetcher spasial baris yang berdekatan di cache L2 yang mencoba menyelesaikan pasangan baris 128 byte, terpisah dari detektor pola prefetch HW streamer utama L2 dan prefetching L1d).
Fakta menarik: Rust memungkinkan kompiler menyusun ulang struct untuk pengemasan yang lebih baik, atau alasan lainnya. IDK apakah ada kompiler yang benar-benar melakukan itu. Mungkin hanya mungkin dengan optimasi seluruh program link-time jika Anda ingin pilihan didasarkan pada bagaimana struct sebenarnya digunakan. Jika tidak, bagian program yang dikompilasi secara terpisah tidak dapat menyetujui tata letaknya.
(@alexis memposting jawaban hanya tautan yang tertaut ke artikel ESR, jadi terima kasih untuk titik awalnya.)
person
Peter Cordes
schedule
26.06.2019
struct foo { int a, b; };
) - person David C. Rankin   schedule 26.06.2019double
sama denganfloat
(4 byte), yang mungkin mengejutkan bagi sebagian orang (terutama jika lebih dari 7-8 digit signifikan diperlukan, katakanlah, untuk penghitung frekuensi...). - person Peter Mortensen   schedule 26.06.2019