Akhirnya, saya menulis posting tentang [[trivial_abi]]!
Ini adalah fitur eksklusif baru di dentang Clang, baru pada Februari 2018. Ini adalah ekstensi vendor bahasa C ++, ini bukan standar C ++, tidak didukung oleh trunk GCC, dan tidak ada proposal aktif oleh WG21 untuk memasukkannya ke dalam standar C ++, sejauh yang saya tahu.

Saya tidak berpartisipasi dalam penerapan fitur ini. Saya hanya melihat tambalan di milis cfe-commit dan bertepuk tangan dalam hati kepada diri saya. Tapi ini adalah fitur yang keren sehingga saya pikir semua orang harus mengetahuinya.
Jadi, hal pertama yang akan kita mulai dengan: ini bukan atribut standar, dan dentang dentang tidak mendukung ejaan standar atribut [[trivial_abi]] untuk itu. Sebagai gantinya, Anda harus menulisnya dengan gaya lama, seperti yang ditunjukkan di bawah ini:
__attribute__((trivial_abi)) __attribute__((__trivial_abi__)) [[clang::trivial_abi]]
Dan, karena ini adalah atribut, kompiler sangat pilih-pilih tentang tempat Anda menempelkannya, dan secara pasif diam-diam agresif jika Anda menempelkannya di tempat yang salah (karena atribut yang tidak dikenal diabaikan begitu saja tanpa pesan). Ini bukan bug, ini fitur. Sintaks yang benar adalah ini:
#define TRIVIAL_ABI __attribute__((trivial_abi)) class TRIVIAL_ABI Widget {
Masalah apa yang dipecahkan ini?
Ingat posting saya pada 17/04/2018 di mana saya menunjukkan dua versi kelas?
Catatan perev: Karena pos 04/17/2018 memiliki volume kecil, saya tidak menerbitkannya secara terpisah, tetapi memasukkannya di sini di bawah spoiler.
posting dari 17/04/2018Kekurangan Panggilan Destructor Trivial Hilang
Lihat C ++ Mailing List Proposal Standar. Manakah dari dua fungsi, foo atau bar, akan memiliki kode terbaik yang dihasilkan oleh kompiler?
struct Integer { int value; ~Integer() {}
Kompilasi dengan GCC dan libstdc ++. Tebak kan?
foo: movq 8(%rdi), %rax imull $-559038737, -4(%rax), %edx subq $4, %rax movl %edx, (%rax) movq %rax, 8(%rdi) ret bar: subq $4, 8(%rdi) ret
Inilah yang terjadi di sini: GCC cukup pintar untuk memahami bahwa ketika destruktor untuk area memori dimulai, masa pakainya berakhir, dan semua entri sebelumnya ke area memori ini "mati". Tetapi GCC juga cukup pintar untuk memahami bahwa destruktor sepele (seperti destruktor semu ~ int ()) tidak melakukan apa-apa dan tidak menghasilkan efek.
Jadi, fungsi bar memanggil pop_back, yang menjalankan ~ Integer (), yang membuat vec.back () mati, dan GCC sepenuhnya menghapus perkalian dengan 0xDEADBEEF.
Di sisi lain, foo memanggil pop_back, yang meluncurkan ~ int () pseudo-destructor (ia dapat sepenuhnya melewati panggilan, tetapi tidak), GCC melihat bahwa itu kosong dan lupa tentangnya. Oleh karena itu, GCC tidak melihat bahwa vec.back () sudah mati, dan tidak menghapus perkalian dengan 0xDEADBEEF.
Ini terjadi untuk destruktor sepele, tetapi tidak untuk destruktor semu seperti ~ int (). Ganti ~ Integer () {} kami dengan ~ Integer () = default; dan lihat bagaimana instruksi imull muncul lagi!
struct Foo { int value; ~Foo() = default;
Dalam posting itu, kode diberikan di mana kompiler menghasilkan kode untuk Foo lebih buruk daripada untuk Bar. Perlu dibahas mengapa ini tidak terduga. Pemrogram secara intuitif mengharapkan kode "sepele" menjadi lebih baik daripada kode "tidak trivial". Ini adalah kasus di sebagian besar situasi. Secara khusus, ini adalah kasus ketika kita membuat panggilan fungsi atau kembali:
template<class T> T incr(T obj) { obj.value += 1; return obj; }
incr
mengkompilasi ke kode berikut:
leal 1(%rdi), %eax retq
(leal adalah
perintah x86 yang berarti "add.") Kita melihat bahwa objek 4-byte kami dilewatkan ke incr dalam register% edi, dan kami menambahkan 1 ke nilainya dan mengembalikannya ke% eax. Empat byte pada input, empat byte pada output, mudah dan sederhana.
Sekarang mari kita lihat incr (kasus dengan destruktor non-sepele).
movl (%rsi), %eax addl $1, %eax movl %eax, (%rsi) movl %eax, (%rdi) movq %rdi, %rax retq
Di sini, obj tidak diteruskan dalam register, meskipun faktanya di sini sama 4 byte dengan semantik yang sama. Di sini obj diteruskan dan dikembalikan ke alamat. Di sini pemanggil menyimpan beberapa ruang untuk nilai kembali dan memberikan kita sebuah pointer ke ruang ini di rdi, dan pemanggil memberi kita sebuah pointer untuk objek nilai pengembalian di register argumen berikutnya% rsi. Kami mengekstrak nilai dari (% rsi), menambahkan 1, menyimpannya kembali ke (% rsi) untuk memperbarui nilai objek itu sendiri, dan kemudian (secara sepele) menyalin 4 byte objek ke slot untuk nilai pengembalian yang ditunjukkan oleh% rdi. Akhirnya, kita menyalin pointer asli yang dilewati oleh pemanggil dari% rdi ke% rax, karena dokumen
ABI x86-64 (p. 22) memberitahu kita untuk melakukan ini.
Alasan Bar sangat berbeda dari Foo adalah karena Bar memiliki destruktor nontrivial, dan
ABI x86-64 (p. 19) secara khusus menyatakan:
Jika objek C ++ memiliki konstruktor salinan nontrivial atau destruktor nontrivial, objek tersebut dilewatkan melalui tautan tak terlihat (objek diganti dengan pointer [...] di daftar parameter)
Dokumen
Itanium C ++ ABI yang lebih baru mendefinisikan sebagai berikut:
Jika tipe parameter tidak trivial untuk tujuan panggilan, pemanggil harus mengalokasikan tempat sementara dan meneruskan tautan ke tempat sementara ini:
[...]
Suatu jenis dianggap tidak penting untuk tujuan panggilan jika:
Ini memiliki konstruktor salinan nontrivial, konstruktor bergerak, destruktor, atau semua konstruktor bergerak dan menyalin dihapus.
Jadi ini menjelaskan semuanya: Bar memiliki generasi kode yang lebih buruk karena dilewatkan melalui tautan tak terlihat. Ini ditransmisikan melalui tautan yang tidak terlihat karena kombinasi sial dari dua keadaan independen telah terjadi:
- Dokumen ABI mengatakan objek dengan destruktor non-sepele dilewatkan melalui tautan tak terlihat
- Bar memiliki destruktor non-sepele.
Ini adalah
silogisme klasik: poin pertama adalah premis utama, yang kedua adalah privat. Akibatnya, Bar ditransmisikan melalui tautan yang tak terlihat.
Biarkan seseorang memberi kami silogisme:
- Semua orang fana
- Socrates adalah seorang pria.
- Akibatnya, Socrates fana.
Jika kita ingin menyangkal kesimpulan "Socrates fana," kita harus menyangkal salah satu premis: entah untuk menyangkal hal utama (mungkin beberapa orang tidak fana), atau untuk menyangkal pribadi (mungkin Socrates bukan orang).
Agar Bar dapat dilewatkan dalam register (seperti Foo), kita harus menyangkal salah satu dari dua tempat. Jalur C ++ standar adalah untuk memberikan Bar destruktor sepele, menghancurkan premis pribadi. Tapi ada cara lain!
Bagaimana [[trivial_abi]] menyelesaikan masalah
Atribut Dentang baru menghancurkan premis utama. Dentang memperpanjang dokumen ABI sebagai berikut:
Jika tipe parameter tidak trivial untuk tujuan panggilan, pemanggil harus mengalokasikan tempat sementara dan meneruskan tautan ke tempat sementara ini:
[...]
Suatu jenis dianggap nontrivial untuk keperluan panggilan jika ditandai sebagai [[trivial_abi]] dan:
Ini memiliki konstruktor salinan nontrivial, konstruktor bergerak, destruktor, atau semua konstruktor bergerak dan menyalin dihapus.
Bahkan jika kelas dengan konstruktor atau destruktor bergerak nontrivial dapat dianggap sepele untuk tujuan panggilan, jika ditandai sebagai [[trivial_abi]].
Jadi sekarang, menggunakan Dentang, kita dapat menulis seperti ini:
#define TRIVIAL_ABI __attribute__((trivial_abi)) struct TRIVIAL_ABI Baz { int value; ~Baz() {}
kompilasi incr <Baz>, dan dapatkan kode yang sama dengan incr <Foo>!
Peringatan # 1: [[trivial_abi]] terkadang tidak melakukan apa-apa
Saya berharap bahwa kita dapat membuat pembungkus "sepele untuk keperluan panggilan" atas jenis perpustakaan standar, seperti ini:
template<class T, class D> struct TRIVIAL_ABI trivial_unique_ptr : std::unique_ptr<T, D> { using std::unique_ptr<T, D>::unique_ptr; };
Sayangnya, ini tidak berhasil. Jika kelas Anda memiliki kelas dasar atau bidang non-statis yang “tidak penting untuk tujuan panggilan”, maka ekstensi Dentang dalam bentuk yang ditulisnya sekarang membuat kelas Anda “tidak dapat dikembalikan lagi”, dan atribut tidak akan berpengaruh. (Tidak ada pesan diagnostik yang dikeluarkan. Ini berarti bahwa Anda dapat menggunakan [[trivial_abi]] di templat kelas sebagai atribut opsional, dan kelas akan "sepele kondisional", yang kadang-kadang berguna. Kerugiannya, tentu saja, adalah Anda bisa tandai kelas sebagai sepele, dan kemudian temukan bahwa kompiler diam-diam memperbaikinya.)
Atribut diabaikan tanpa pesan jika kelas Anda memiliki kelas dasar virtual, atau fungsi virtual. Dalam kasus ini, ini mungkin tidak cocok dengan register, dan saya tidak tahu apa yang ingin Anda dapatkan dengan memberikan nilai, tetapi Anda mungkin tahu.
Jadi, sejauh yang saya tahu, satu-satunya cara untuk menggunakan TRIVIAL_ABI untuk “tipe utilitas standar” seperti opsional <T>, unique_ptr <T> dan shared_ptr <T> adalah
- implementasikan sendiri dari awal dan terapkan atributnya, atau
- masuk ke salinan libc ++ lokal Anda dan masukkan atribut di sana dengan tangan Anda
(di dunia open source, kedua metode pada dasarnya sama)
Peringatan # 2: tanggung jawab destruktor
Dalam contoh dengan Foo / Bar, kelas memiliki destruktor kosong. Biarkan kelas kita benar-benar memiliki destruktor nontrivial.
struct Up1 { int value; Up1(Up1&& u) : value(u.value) { u.value = 0; } ~Up1() { puts("destroyed"); } };
Ini seharusnya tidak asing bagi Anda, ini unique_ptr <int>, disederhanakan hingga batasnya, dengan pesan dicetak saat dihapus.
Tanpa TRIVIAL_ABI, incr <Up1> terlihat seperti incr <Bar>:
movl (%rsi), %eax addl $1, %eax movl %eax, (%rdi) movl $0, (%rsi) movq %rdi, %rax retq
Dengan TRIVIAL_ABI, incr terlihat
lebih besar dan lebih menakutkan !
pushq %rbx leal 1(%rdi), %ebx movl $.L.str, %edi callq puts movl %ebx, %eax popq %rbx retq
Dalam konvensi pemanggilan tradisional, tipe dengan destruktor non-sepele selalu dilewati oleh tautan tak terlihat, yang berarti bahwa sisi penerima (termasuk dalam kasus ini) selalu menerima pointer ke objek parameter tanpa memiliki objek ini. Objek tersebut dimiliki oleh penelepon. Ini membuat pekerjaan elisi berhasil!
Ketika jenis dengan [[trivial_abi]] diteruskan dalam register, kami pada dasarnya membuat salinan objek parameter.
Karena x86-64 hanya memiliki satu register untuk kembali (tepuk tangan), fungsi yang dipanggil tidak memiliki cara untuk mengembalikan objek di akhir. Fungsi yang dipanggil harus memiliki kepemilikan atas objek yang kita berikan padanya! Ini berarti bahwa fungsi yang dipanggil harus memanggil penghancur objek parameter ketika selesai.
Dalam contoh kita sebelumnya, Foo / Bar / Baz, destructor dipanggil, tetapi itu kosong, dan kami tidak menyadarinya. Sekarang dalam incr <Up2> kita melihat kode tambahan yang dihasilkan oleh destructor di sisi fungsi yang dipanggil.
Dapat diasumsikan bahwa kode tambahan ini dapat dihasilkan dalam beberapa kasus pengguna. Tetapi, sebaliknya, panggilan destruktor tidak muncul di mana pun! Disebut incr karena
tidak dipanggil dalam fungsi panggilan. Dan secara umum, harga dan manfaat akan seimbang.
Peringatan # 3: Destructor Order
Destuktor untuk parameter dengan ABI yang sepele akan dipanggil oleh fungsi yang dipanggil, dan bukan yang memanggil (peringatan No. 2). Richard Smith menunjukkan bahwa ini berarti bahwa dia tidak akan dipanggil dalam urutan di mana penghancur parameter lainnya berada.
struct TRIVIAL_ABI alpha { alpha() { puts("alpha constructed"); } ~alpha() { puts("alpha destroyed"); } }; struct beta { beta() { puts("beta constructed"); } ~beta() { puts("beta destroyed"); } }; void foo(alpha, beta) {} int main() { foo(alpha{}, beta{}); }
Kode ini mencetak:
alpha constructed beta constructed alpha destroyed beta destroyed
ketika TRIVIAL_ABI didefinisikan sebagai [[dentang :: trivial_abi]], ia mencetak:
alpha constructed beta constructed beta destroyed alpha destroyed
Hubungan dengan objek “triocally relocatable” / “move-relocates”
Tidak ada hubungan ..., ya?
Seperti yang Anda lihat, tidak ada persyaratan untuk kelas [[trivial_abi]] untuk memiliki semantik spesifik untuk konstruktor bergerak, destruktor, atau konstruktor default. Setiap kelas tertentu mungkin akan direlokasi secara sepele, hanya karena sebagian besar kelas mudah direlokasi.
Kita cukup membuat kelas offset_ptr sehingga tidak mudah dipindahkan:
template<class T> class TRIVIAL_ABI offset_ptr { intptr_t value_; public: offset_ptr(T *p) : value_((const char*)p - (const char*)this) {} offset_ptr(const offset_ptr& rhs) : value_((const char*)rhs.get() - (const char*)this) {} T *get() const { return (T *)((const char *)this + value_); } offset_ptr& operator=(const offset_ptr& rhs) { value_ = ((const char*)rhs.get() - (const char*)this); return *this; } offset_ptr& operator+=(int diff) { value_ += (diff * sizeof (T)); return *this; } }; int main() { offset_ptr<int> top = &a[4]; top = incr(top); assert(top.get() == &a[5]); }
Ini kode lengkapnya.Ketika TRIVIAL_ABI didefinisikan, tring Dentang melewati tes ini pada -O0 dan -O1, tetapi pada -O2 (mis., Segera setelah mencoba untuk inline panggilan ke trivial_offset_ptr :: operator + = dan copy constructor), crash pada pernyataan.
Jadi satu peringatan lagi. Jika tipe Anda melakukan sesuatu yang sangat gila dengan pointer ini, Anda mungkin tidak ingin meneruskannya dalam register.
Bug 37319 , pada kenyataannya, permintaan untuk dokumentasi. Dalam hal ini, ternyata tidak ada cara untuk membuat kode berfungsi seperti yang diinginkan programmer. Kami mengatakan bahwa nilai value_ harus tergantung pada nilai pointer ini, tetapi pada perbatasan antara fungsi yang dipanggil dan yang dipanggil, objek ada di register dan pointer ke sana tidak ada! Oleh karena itu, fungsi panggilan menuliskannya ke memori, dan meneruskan pointer ini lagi, dan bagaimana seharusnya fungsi yang dipanggil menghitung nilai yang benar untuk menuliskannya ke value_? Mungkin lebih baik untuk bertanya bagaimana cara kerjanya di -O0?
Kode ini seharusnya tidak berfungsi sama sekali.Jadi, jika Anda ingin menggunakan [[trivial_abi]], Anda harus menghindari fungsi anggota (tidak hanya khusus, tetapi ada secara umum) yang sangat bergantung pada alamat objek itu sendiri (dengan beberapa arti kata "esensial").
Secara intuitif, ketika sebuah kelas ditandai sebagai [[trivial_abi]], kapan pun Anda berharap untuk menyalin, Anda bisa mendapatkan salinan plus memcpy. Dan juga, ketika Anda mengharapkan langkah, Anda benar-benar bisa mendapatkan langkah plus memcpy.
Ketika suatu tipe “trivially relocatable” (seperti yang didefinisikan oleh saya dalam
C ++ Now ), maka kapan saja Anda mengharapkan copy dan menghancurkan, Anda sebenarnya bisa mendapatkan memcpy. Dan juga, ketika Anda mengharapkan perpindahan dan kehancuran, Anda sebenarnya bisa mendapatkan memcpy. Bahkan, panggilan ke fungsi khusus hilang jika kita berbicara tentang "relokasi sepele", tetapi ketika kelas memiliki atribut [[trivial_abi]] Dentang, panggilan tidak hilang. Anda hanya mendapatkan (seolah-olah) memcpy di samping panggilan yang Anda harapkan. Memcpy (semacam) ini adalah harga yang Anda bayar untuk konvensi register panggilan yang lebih cepat.
Tautan untuk bacaan lebih lanjut:
Utas cfe-dev Akira Hatanaka mulai November 2017Dokumentasi Dentang resmiUnit ini menguji trivial_abiBug 37319: trivial_offset_ptr tidak dapat bekerja