Bagaimana cara mengatur anggota dalam sebuah struct untuk membuang ruang paling sedikit pada penyelarasan?

[Bukan duplikat dari Struktur padding dan packing. Pertanyaan itu adalah tentang bagaimana dan kapan padding terjadi. Yang ini tentang cara menghadapinya.]

Saya baru menyadari betapa banyak memori yang terbuang akibat penyelarasan di C++. Perhatikan contoh sederhana berikut ini:

struct X
{
    int a;
    double b;
    int c;
};

int main()
{
    cout << "sizeof(int) = "                      << sizeof(int)                      << '\n';
    cout << "sizeof(double) = "                   << sizeof(double)                   << '\n';
    cout << "2 * sizeof(int) + sizeof(double) = " << 2 * sizeof(int) + sizeof(double) << '\n';
    cout << "but sizeof(X) = "                    << sizeof(X)                        << '\n';
}

Saat menggunakan g++ program memberikan output berikut:

sizeof(int) = 4
sizeof(double) = 8
2 * sizeof(int) + sizeof(double) = 16
but sizeof(X) = 24

Itu 50% overhead memori! Dalam array 3 gigabyte 134'217'728 Xs 1 gigabyte akan menjadi padding murni.

Untungnya, solusi untuk masalah ini sangat sederhana - kita hanya perlu menukar double b dan int c:

struct X
{
    int a;
    int c;
    double b;
};

Kini hasilnya jauh lebih memuaskan:

sizeof(int) = 4
sizeof(double) = 8
2 * sizeof(int) + sizeof(double) = 16
but sizeof(X) = 16

Namun ada masalah: ini tidak kompatibel secara silang. Ya, di bawah g++, int adalah 4 byte dan double adalah 8 byte, tetapi itu belum tentu selalu benar (penyelarasannya juga tidak harus sama), jadi dalam lingkungan yang berbeda, perbaikan ini tidak hanya tidak berguna, tetapi juga hal ini juga berpotensi memperburuk keadaan dengan meningkatkan jumlah padding yang dibutuhkan.

Apakah ada cara lintas platform yang dapat diandalkan untuk mengatasi masalah ini (meminimalkan jumlah padding yang diperlukan tanpa mengalami penurunan kinerja yang disebabkan oleh ketidakselarasan)? Mengapa kompiler tidak melakukan optimasi seperti itu (menukar anggota struct/kelas untuk mengurangi padding)?

Klarifikasi

Karena kesalahpahaman dan kebingungan, saya ingin menekankan bahwa Saya tidak ingin mengemas struct saya. Artinya, saya tidak ingin anggotanya menjadi tidak selaras sehingga aksesnya menjadi lebih lambat. Sebaliknya, saya tetap ingin semua anggota menyelaraskan diri, namun dengan cara yang menggunakan memori paling sedikit pada padding. Hal ini dapat diatasi dengan menggunakan, misalnya, penataan ulang secara manual seperti dijelaskan di sini dan dalam The Lost Art of Packing oleh Eric Raymond. Saya mencari cara otomatis dan lintas platform sebanyak mungkin untuk melakukan ini, mirip dengan apa yang dijelaskan di proposal P1112 untuk standar C++20 mendatang.


person Community    schedule 25.06.2019    source sumber
comment
Jika Anda membutuhkan array yang terdiri dari ratusan juta elemen, mungkin array bukanlah struktur data yang benar? Setidaknya bukan array dalam memori (pikirkan file yang dipetakan memori, atau bahkan mungkin semacam database)?   -  person Some programmer dude    schedule 25.06.2019
comment
Dan sungguh, satu-satunya jawaban yang mungkin untuk pertanyaan [i] apakah ada cara lintas platform yang dapat diandalkan untuk menyelesaikan masalah ini (meminimalkan jumlah padding yang diperlukan tanpa mengalami penurunan kinerja yang disebabkan oleh ketidakselarasan)? hanya bisa menjadi tidak sederhana. Mungkin ada cara khusus kompiler dan sistem untuk mengatasinya, tetapi tidak ada yang benar-benar portabel atau kompiler/platform/sistem agnostik.   -  person Some programmer dude    schedule 25.06.2019
comment
Mungkin ada beberapa manfaat portabilitas dari penggunaan bilangan bulat dengan lebar tetap sehingga tidak berubah ukuran pada Anda.   -  person user4581301    schedule 25.06.2019
comment
Dan mengenai [w] mengapa kompiler tidak melakukan optimasi seperti itu (menukar struct/anggota kelas untuk mengurangi padding)? Bagaimana kompiler bisa melakukan itu ketika ia tidak tahu untuk apa struktur itu digunakan? Mungkin itu akan disimpan mentah dalam file biner, atau dikirim melalui protokol komunikasi serial (dalam hal ini struktur yang belum dibongkar (secara manual atau dengan pragma kompiler) benar-benar merupakan ide yang buruk, tetapi hal itu masih terjadi).   -  person Some programmer dude    schedule 25.06.2019
comment
persyaratan penyelarasan terbesar pertama. Jika tidak ada, maka anggota terbesar pertama. Mengenai pertanyaan nyata Anda, ya, ada metode yang kompatibel untuk melakukan hal ini: ini disebut string. Di luar itu, tipe yang menggunakan lebar bit tertentu dapat membantu secara signifikan, namun tetap memerlukan penanganan endian jika Anda benar-benar serius dengan lintas platform. Singkatnya, protokol ada secara khusus untuk mengatasi masalah tersebut dan menjembatani perbedaan besar antar platform. Hal-hal seperti ini adalah salah satu dari banyak alasan mengapa hal itu ada, Peringatan: Kemungkinan besar saya benar-benar salah memahami pertanyaan ini.   -  person WhozCraig    schedule 25.06.2019
comment
Terakhir, bagi saya ini terasa seperti masalah XY. Menata ulang struktur adalah sebuah solusi, namun apa masalah sebenarnya di balik solusi ini? Apa yang sebenarnya ingin Anda capai? Mengapa Anda membutuhkan jutaan struktur? Mungkin ada solusi lain yang mungkin untuk masalah awal tersebut, solusi yang tidak melibatkan array atau yang membuat kemungkinan padding menjadi tidak relevan?   -  person Some programmer dude    schedule 25.06.2019
comment
Karena semua alasan di atas, tidak ada satu hal pun yang menjamin penyimpanan minimum untuk ukuran struct, namun @WhozCraig memberikan penjelasan yang tepat tentang aturan yang terlalu disederhanakan Terbesar Pertama, Terkecil Terakhir dalam mengurangi urutan ukuran penyimpanan yang diperlukan . Itu adalah pendekatan yang masuk akal yang cenderung meminimalkan penyimpanan di seluruh kompiler dan perangkat keras, tetapi tidak ada jaminan dua struct akan dialokasikan jumlah penyimpanan yang sama antar kompiler (selain contoh sepele (seperti struct foo { int a, b; };)   -  person David C. Rankin    schedule 26.06.2019
comment
@Someprogrammerdude Mengapa Anda memerlukan array yang terdiri dari jutaan struktur? Saya yakin, di HPC hal ini cukup umum. Misalnya, kita bekerja dengan matriks renggang yang sangat besar. Alur kerja khas kami adalah menghasilkan elemen matriks dan kemudian mengubahnya menjadi format penyimpanan yang efisien untuk diproses lebih lanjut. Konversi ini biasanya melibatkan penyortiran. Sayangnya, C++ tidak mendukung pengurutan beberapa array sekaligus, oleh karena itu, kami mengurutkannya dalam bentuk array struct, masing-masing memiliki indeks baris/kolom dan nilai. Kita bahkan dapat bekerja dengan miliaran elemen matriks dalam satu proses MPI.   -  person Daniel Langr    schedule 26.06.2019
comment
Deskripsi Anda tentang tidak mengemas struct terdengar persis seperti mengemas struct.   -  person chrylis -cautiouslyoptimistic-    schedule 26.06.2019
comment
di bawah g++, int adalah 4 byte dan double adalah 8 byte. Nah, pada Arduino (kompiler yang mendasarinya adalah GCC, digunakan sebagai kompiler C++), double sama dengan float (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
comment
Kemungkinan duplikat dari Struktur padding dan packing   -  person John Bollinger    schedule 26.06.2019
comment
@chrylis bukankah mengemas struct memerlukan akses yang tidak selaras? Ada jalan tengah untuk menyusun ulang elemen.   -  person RonJohn    schedule 27.06.2019
comment
@RonJohn Belum tentu. Secara khusus, penyelarasan biasanya berupa sesuatu yang berukuran kata atau operan lebih besar, artinya (int, int, double) disejajarkan secara alami tanpa padding.   -  person chrylis -cautiouslyoptimistic-    schedule 27.06.2019
comment
Jika Anda merasa pertanyaan ini berguna maka berikut adalah beberapa cara lain yang dapat Anda gunakan untuk mengoptimalkan kode Anda pada tingkat rendah.   -  person    schedule 27.06.2019
comment
@DanielLangr Saya bertanya karena saya ingin OP menguraikan masalah sebenarnya alih-alih bagaimana memperbaiki solusi untuk masalah yang tidak diketahui (bagi kami).   -  person Some programmer dude    schedule 01.07.2019


Jawaban (7)


(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 doubles. 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
comment
Meskipun ini bukan solusi sepenuhnya lintas platform dan bukan solusi otomatis, solusi ini berisi informasi paling aktual tentang cara mengatasi masalah ini, jadi saya akan menerimanya. Mungkin nanti saya akan membuat wiki komunitas di sini. - person ; 26.06.2019
comment
@YanB.: Saya tidak sepenuhnya membaca pertanyaan sebelum menjawab; Saya tidak menyadari bahwa Anda kebanyakan mencari solusi otomatis dan bukan solusi praktis. Tapi untungnya ada cukup banyak kesamaan antara semua CPU mainstream modern 32 dan 64-bit sehingga kami benar-benar peduli sehingga kami dapat memberikan saran yang berguna meskipun faktanya ISO C++ pada dasarnya tidak menjamin apa pun. Ada banyak asumsi tentang apa yang normal pada C++ (dan CPU modern), terpisah dari standar ISO C++. Sebagian besar dari hal ini hampir diperlukan agar implementasi C++ dapat berguna dalam praktik apa pun! - person Peter Cordes; 26.06.2019
comment
Urutan yang lebih kecil ke lebih besar mungkin secara keseluruhan lebih baik: ini menghasilkan akses yang lebih efisien ke sebagian besar anggota (misalnya karena offsetnya lebih kecil seperti yang Anda tunjukkan, tetapi juga karena lebih banyak anggota struct cenderung berada dalam baris cache). Perampingan utamanya adalah lubang bantalan lebih cenderung muncul di tengah struktur, dibandingkan di bagian akhir, sehingga penyalinan mungkin kurang efisien dalam beberapa kasus yang tidak biasa. - person BeeOnRope; 27.06.2019
comment
@BeeOnRope: terutama dengan optimasi gcc yang terlewat. Penyimpanan GCC8 yang digabungkan untuk struct zeroing menolak menimpa padding: gcc.gnu.org/bugzilla /show_bug.cgi?id=82142 - person Peter Cordes; 27.06.2019
comment
Tampaknya ini bukan masalah universal. Lihat tes cepat saya. - person BeeOnRope; 27.06.2019

gcc memiliki peringatan -Wpadded yang memperingatkan ketika padding ditambahkan ke struktur:

https://godbolt.org/z/iwO5Q3:

<source>:4:12: warning: padding struct to align 'X::b' [-Wpadded]
    4 |     double b;
      |            ^

<source>:1:8: warning: padding struct size to alignment boundary [-Wpadded]
    1 | struct X
      |        ^

Dan Anda dapat mengatur ulang anggota secara manual sehingga paddingnya lebih sedikit/tidak ada. Tapi ini bukan solusi lintas platform, karena tipe yang berbeda dapat memiliki ukuran/penjajaran yang berbeda pada sistem yang berbeda (terutama pointer berukuran 4 atau 8 byte pada arsitektur berbeda). Aturan umumnya adalah beralih dari perataan terbesar ke terkecil saat mendeklarasikan anggota, dan jika Anda masih khawatir, kompilasi kode Anda dengan -Wpadded satu kali (Tapi saya tidak akan menyimpannya secara umum, karena padding terkadang diperlukan).

Adapun alasan mengapa compiler tidak dapat melakukannya secara otomatis adalah karena standar ([ class.mem]/19). Ini menjamin, karena ini adalah struct sederhana dengan hanya anggota publik, &x.a < &x.c (untuk beberapa X x;), sehingga tidak dapat diatur ulang.

person Artyer    schedule 25.06.2019
comment
Sejujurnya saya tidak berpikir saya akan melihat sesuatu yang berguna dari pertanyaan ini. Tidak mengetahui opsi gcc itu (dan sekarang saya berharap dentang juga memilikinya). Terima kasih telah mengajariku sesuatu. kutu. - person WhozCraig; 25.06.2019
comment
@WhozCraig Ya, clang juga memiliki opsi ini (bahkan memiliki nama yang sama). Ini sangat membantu (setidaknya bagi saya) ketika menangani masalah penataan ulang. Sayangnya (setidaknya untuk saat ini) saya belum menemukan solusi otomatis. - person ; 26.06.2019
comment
Apakah ada platform modern jarak jauh yang menempatkan tipe dalam urutan double, [unsigned] long long, [i]int64_t, int64_t, pointer, long, float, int32_t, int, int16_t, short, char, tidak akan menghasilkan penyelarasan yang optimal? - person supercat; 29.06.2019

Sebenarnya tidak ada solusi portabel dalam kasus umum. Dengan tidak adanya persyaratan minimal yang diberlakukan standar, tipe dapat berupa ukuran apa pun yang diinginkan oleh implementasi.

Selain itu, kompiler tidak diperbolehkan menyusun ulang anggota kelas agar lebih efisien. Standar ini mengamanatkan bahwa objek harus ditata sesuai urutan yang dinyatakan (dengan pengubah akses), sehingga objek tersebut juga keluar.

Anda dapat menggunakan tipe lebar tetap seperti

struct foo
{
    int64_t a;
    int16_t b;
    int8_t c;
    int8_t d;
};

dan ini akan sama di semua platform, asalkan mereka menyediakan tipe tersebut, tetapi hanya berfungsi dengan tipe integer. Tidak ada tipe floating point dengan lebar tetap dan banyak objek/kontainer standar dapat memiliki ukuran berbeda pada platform berbeda.

person NathanOliver    schedule 25.06.2019
comment
Menambahkan garam pada luka, tipe floating point seringkali sangat sensitif terhadap posisi pelurusan bus, sehingga meningkatkan mantra tanpa peluru perak. Terlepas dari itu, ini sangat berguna ketika memuat struct dengan apa pun selain floating point dan kemungkinan pointer. Saya sering menggunakannya. - person WhozCraig; 25.06.2019
comment
Mengapa penataan ulang anggota tidak diperbolehkan? Bisakah Anda menjelaskan? - person ; 26.06.2019
comment
Jika Anda membatasi portabilitas lintas platform, perhatikan bahwa jenis lebar persis ini adalah opsional. Setiap platform harus memiliki int_least16_t dan int_fast16_t, namun (misalnya jika CHAR_BIT != 8), int16_t tidak perlu ada pada platform tertentu. - person DevSolar; 26.06.2019
comment
@DevSolar Meskipun bersifat opsional, kode akan gagal dikompilasi jika tidak ada sehingga setidaknya Anda tidak akan mendapatkan biner yang meledak pada Anda. - person NathanOliver; 26.06.2019
comment
Anda dapat menyimpan float dalam int 4 byte. Hanya membaca dan menulis saja yang jelek. - person Oblivion; 26.06.2019
comment
@YanB. Karena standar mengatakan demikian. Lihat juga stackoverflow.com/questions/118068/. Adapun alasannya, banyak hal yang akan rusak jika kompiler bebas melakukan hal tersebut (antara lain, bayangkan sebuah program yang menulis structs langsung ke file dengan fwrite dan membacanya kembali dengan fread; perubahan pada kompiler dapat tiba-tiba merusak format file kompatibilitas untuk program yang dikompilasi). - person jamesdlin; 26.06.2019

Ini adalah masalah memori-vs-kecepatan di buku teks. Paddingnya adalah menukar memori dengan kecepatan. Anda tidak bisa mengatakan:

Saya tidak ingin "mengemas" struct saya.

karena paket pragma adalah alat yang diciptakan untuk melakukan perdagangan ini dengan cara lain: kecepatan untuk memori.

Apakah ada cara lintas platform yang dapat diandalkan

Tidak, tidak mungkin ada. Penyelarasan sepenuhnya merupakan masalah yang bergantung pada platform. Ukuran jenis yang berbeda merupakan masalah yang bergantung pada platform. Menghindari padding dengan melakukan reorganisasi bergantung pada platform.

Kecepatan, memori, dan lintas platform - Anda hanya dapat memiliki dua.

Mengapa kompiler tidak melakukan optimasi seperti itu (menukar struct/anggota kelas untuk mengurangi padding)?

Karena spesifikasi C++ secara khusus menjamin bahwa kompiler tidak akan mengacaukan struct Anda yang telah diatur dengan cermat. Bayangkan Anda memiliki empat pelampung berturut-turut. Terkadang Anda menggunakannya berdasarkan nama, dan terkadang Anda meneruskannya ke metode yang menggunakan parameter float[3].

Anda mengusulkan agar kompiler mengacaknya, berpotensi merusak semua kode sejak tahun 1970-an. Dan untuk alasan apa? Bisakah Anda menjamin bahwa setiap programmer benar-benar ingin menyimpan 8 byte per struct Anda? Saya, misalnya, yakin bahwa jika saya memiliki array 3 GB, saya mengalami masalah yang lebih besar daripada kurang lebih satu GB.

person Agent_L    schedule 26.06.2019
comment
Saya berpendapat bahwa satu-satunya masalah di sini adalah "terkadang Anda meneruskannya ke metode yang menggunakan parameter float[3]". Ya, itu kasus penggunaan yang cukup istimewa. Sebenarnya menurut saya masalah utama di sini adalah C++ mendukung juggling pointer semacam ini; jika ia tidak melakukan hal tersebut dan malah mengizinkan kompiler untuk selalu menyusun ulang untuk optimasi maka banyak kode akan berjalan lebih cepat, sementara program yang perlu ditulis ulang untuk membungkus float[3] tersebut secara eksplisit dalam sebuah array akan memiliki penalti kinerja yang dapat diabaikan. - person leftaroundabout; 26.06.2019
comment
Saya cukup yakin bahwa mengetikkan empat variabel anggota floating point individu untuk meneruskannya sebagai float[3] akan memunculkan perilaku tidak terdefinisi. - person Jeremy Friesner; 26.06.2019
comment
@JeremyFriesner: Perhatikan bahwa Perilaku Tidak Terdefinisi dimaksudkan untuk memungkinkan implementasi yang dapat menawarkan semantik yang lebih berguna untuk melakukannya bila praktis, sebelum pengacau bahasa mengambil alih dan mulai menggunakannya sebagai alasan untuk tidak menawarkan semantik yang berguna bahkan dalam kasus di mana mereka tidak memerlukan biaya apa pun . - person supercat; 26.06.2019
comment
@supercat terlepas dari maksud historisnya, menerapkan perilaku tidak terdefinisi bukanlah sesuatu yang ingin dilakukan (kecuali jika Anda senang menemukan dan mendiagnosis perilaku buruk runtime yang tidak jelas) - person Jeremy Friesner; 26.06.2019
comment
@JeremyFriesner: Standar tidak pernah mengharuskan implementasi mendukung semua semantik yang diperlukan untuk tujuan tertentu. Pada banyak platform target, I/O tidak mungkin dilakukan tanpa menggunakan pointer untuk mewakili alamat yang tidak mengidentifikasi objek sebagaimana didefinisikan oleh Standar. Jika seseorang tidak diizinkan untuk melakukan tindakan yang tidak diwajibkan oleh Standar, seseorang tidak akan dapat melakukan apa pun pada platform tersebut. - person supercat; 26.06.2019
comment
@JeremyFriesner: Yang pasti, seseorang akan mendapat masalah jika seseorang mencoba menggunakan teknik pemrograman tingkat rendah pada implementasi yang tidak dirancang atau dikonfigurasikan agar sesuai untuk tujuan tersebut, tetapi menggunakan implementasi yang tidak cocok untuk pekerjaan tertentu apa pun yang coba dilakukan seseorang akan menimbulkan masalah. - person supercat; 26.06.2019
comment
@supercat sebenarnya bukan pengacau bahasa yang mengambil alih, melainkan penulis kompiler yang mampu memanfaatkan lebih banyak peluang pengoptimalan dengan mengambil perilaku tidak terdefinisi secara harfiah. Pada dasarnya, Anda berharap kompiler melakukan sesuatu yang masuk akal, sedangkan penulis kompiler lebih memilih untuk melakukan sesuatu dengan cepat (karena hal itu meningkatkan tolok ukur, yang pada gilirannya meningkatkan penjualan/mindshare, dan benar-benar meningkatkan kecepatan runtime bahkan untuk program yang cukup normal). - person toolforger; 26.06.2019
comment
@toolforger: Sudahkah Anda membaca Alasan yang diterbitkan? Menurut Komite, aspek paling mendasar dari Semangat C adalah mempercayai pemrogram dan Jangan menghalangi pemrogram melakukan apa yang perlu dilakukan. Mereka juga secara eksplisit mengakui bahwa salah satu kekuatan C adalah kemampuan untuk menggunakan program non-portabel untuk melakukan hal-hal yang tidak dapat dilakukan oleh program portabel (karena Standar tidak menyediakannya). Jika suatu tugas tidak dapat diselesaikan tanpa melakukan suatu tindakan, semua implementasi yang sesuai untuk tugas tersebut akan mendukung tindakan tersebut, baik Standar mewajibkannya atau tidak. - person supercat; 26.06.2019
comment
@toolforger: Penulis kompiler memperkenalkan dikotomi yang salah antara kecepatan dan semantik. Agar kompiler terkadang memperlakukan matematika bilangan bulat bertanda seolah-olah dilakukan pada tipe yang lebih luas akan memungkinkan 90%+ optimasi berguna yang terkait dengan melompati rel saat meluap. Jika kompiler tersebut diberi kode sumber yang mengeksploitasi fakta bahwa semuanya dapat dilakukan, kompiler tersebut dapat mencapai optimasi yang tidak mungkin dilakukan dengan kode sumber yang ditulis untuk model overflow yang harus dihindari dengan cara apa pun. - person supercat; 26.06.2019
comment
@toolforger: Secara umum, optimasi yang mengasumsikan programmer tidak perlu melakukan X mungkin berguna untuk program yang perlu melakukan X, namun akan menjadi kontra-produktif dalam kasus di mana perilaku yang diperlukan persis seperti yang akan dicapai oleh hanya melakukan X. Jika tindakan X diperlukan untuk beberapa tugas tetapi tidak yang lain, dan jika biaya untuk mendukung X pada implementasi yang berbeda akan berbeda-beda, X harus didukung pada implementasi atau konfigurasi yang digunakan untuk tugas yang memerlukannya, namun tidak pada implementasi atau konfigurasi yang memerlukannya, namun tidak pada implementasi atau konfigurasi yang memerlukan tindakan tersebut. mengenakan biaya yang tidak perlu. Hal ini seharusnya sudah jelas, namun ternyata tidak. - person supercat; 26.06.2019
comment
@supercat poin yang Anda ajukan adalah tentang standar bahasa, bukan tentang penulis kompiler. Selain itu, dikotomi ini tidak salah - kemampuan untuk mengabaikan kasus yang tidak terdefinisi (daripada melakukan apa yang Anda ingin kasus tersebut lakukan) dapat memberikan percepatan hingga 50%. Ini benar-benar masalah kecepatan yang telah mengubah standar C menjadi sesuatu yang penuh dengan perilaku tidak terdefinisi, bukan pengacau bahasa. - person toolforger; 27.06.2019
comment
BTW, ini berubah menjadi diskusi panjang tentang detail latar belakang, yang bukan merupakan tujuan komentar. - person toolforger; 27.06.2019
comment
@toolforger: Satu pertanyaan perpisahan singkat: apakah Anda yakin penulis Standar bermaksud melarang penggunaan bahasa tersebut sebagai bentuk assembler tingkat tinggi? - person supercat; 27.06.2019
comment
@supercat assembler tingkat tinggi tentu saja termasuk dalam daftar prioritas, tetapi pasti ada yang lain. Karena setiap keputusan dalam desain bahasa merupakan trade-off, bahkan tidak akan ada bahasa X yang jelas yang diarahkan pada fitur A, selamanya; itu selalu merupakan hal yang bertahap. - person toolforger; 27.06.2019
comment
Mari kita melanjutkan diskusi ini dalam chat. - person supercat; 27.06.2019

Sobat, jika Anda memiliki data 3GB, Anda mungkin harus mengatasi masalah dengan cara lain lalu menukar anggota data.

Daripada menggunakan 'array of struct', 'struct of arrays' dapat digunakan. Jadi katakan

struct X
{
    int a;
    double b;
    int c;
};

constexpr size_t ArraySize = 1'000'000;
X my_data[ArraySize];

akan menjadi

constexpr size_t ArraySize = 1'000'000;
struct X
{
    int    a[ArraySize];
    double b[ArraySize];
    int    c[ArraySize];
};

X my_data;

Setiap elemen masih mudah diakses mydata.a[i] = 5; mydata.b[i] = 1.5f;....
Tidak ada padding (kecuali beberapa byte antar array). Tata letak memori ramah cache. Prefetcher menangani pembacaan blok memori berurutan dari beberapa wilayah memori terpisah.

Hal ini tidaklah lazim seperti yang terlihat pada pandangan pertama. Pendekatan itu banyak digunakan untuk pemrograman SIMD dan GPU.


Array Struktur (AoS), Struktur Array

person user3124812    schedule 28.06.2019
comment
Ini jauh lebih baik bila SIMD dimungkinkan. Namun ketika Anda memerlukan akses yang tersebar/acak ke struct (dan memerlukan beberapa anggota dari struct yang sama, tetapi tidak apa pun dari struct terdekat) SoA dikenakan biaya 3x lipat dari cache yang hilang. Ini juga membebani Anda lebih banyak pointer/register, terutama untuk alokasi non-CISC dan/atau non-statis. Namun jika SIMD merupakan opsi untuk salah satu loop Anda, maka biasanya jauh lebih baik jika memiliki SoA. - person Peter Cordes; 16.07.2019

Meskipun Standar memberikan keleluasaan pada penerapan untuk menyisipkan jumlah ruang yang sewenang-wenang di antara anggota struktur, hal ini karena penulis tidak ingin mencoba menebak semua situasi di mana padding mungkin berguna, dan prinsip "jangan buang ruang tanpa alasan " dianggap terbukti dengan sendirinya.

Dalam praktiknya, hampir setiap implementasi umum untuk perangkat keras biasa akan menggunakan objek primitif yang ukurannya merupakan pangkat dua, dan penyelarasan yang diperlukan adalah pangkat dua yang tidak lebih besar dari ukurannya. Selanjutnya, hampir setiap implementasi seperti itu akan menempatkan setiap anggota suatu struct pada kelipatan pertama yang tersedia dari penyelarasannya yang sepenuhnya mengikuti anggota sebelumnya.

Beberapa orang yang suka bertele-tele akan mengatakan bahwa kode yang mengeksploitasi perilaku itu adalah "non-portabel". Kepada mereka aku akan membalasnya

Kode C bisa bersifat non-portabel. Meskipun Komite C89 berupaya memberikan kesempatan kepada pemrogram untuk menulis program yang benar-benar portabel, Komite C89 tidak ingin memaksa pemrogram untuk menulis program yang portabel, untuk menghalangi penggunaan C sebagai “assembler tingkat tinggi”: kemampuan untuk menulis kode spesifik mesin adalah hal yang tidak dapat dilakukan. salah satu kelebihan C.

Sebagai sedikit perluasan dari prinsip tersebut, kemampuan kode yang hanya perlu dijalankan pada 90% mesin untuk mengeksploitasi fitur-fitur yang umum pada 90% mesin tersebut--walaupun kode tersebut tidak sepenuhnya "khusus mesin"--adalah salah satu kekuatan C. Gagasan bahwa pemrogram C tidak boleh diharapkan untuk berusaha sekuat tenaga untuk mengakomodasi keterbatasan arsitektur yang selama beberapa dekade hanya digunakan di museum seharusnya sudah jelas, namun ternyata tidak.

person supercat    schedule 26.06.2019

Anda dapat menggunakan #pragma pack(1), namun alasan utamanya adalah karena kompilernya mengoptimalkan. Mengakses variabel melalui register lengkap lebih cepat daripada mengaksesnya hingga bit terkecil.

Pengepakan khusus hanya berguna untuk serialisasi dan kompatibilitas interkompiler, dll.

Seperti yang ditambahkan NathanOliver dengan benar, ini bahkan mungkin gagal di beberapa platform .

person Michael Chourdakis    schedule 25.06.2019
comment
Perlu diperhatikan bahwa hal ini berpotensi menimbulkan masalah kinerja atau dapat menyebabkan kode tidak berfungsi pada beberapa platform: stackoverflow.com/questions/7793511/ - person NathanOliver; 25.06.2019
comment
Sepengetahuan saya, penggunaan #pragma pack menyebabkan potensi masalah kinerja dan karenanya bukanlah solusi yang diinginkan. - person ; 25.06.2019