Apa itu Aliasing Ketat dan mengapa kita harus peduli? Bagian 1

(ATAU quibble mengetik, perilaku dan penyelarasan yang samar, oh my God!)

Halo semuanya, dalam beberapa minggu kami meluncurkan utas baru di kursus "Pengembang C ++" . Acara ini akan didedikasikan untuk materi hari ini.

Apa itu alias ketat? Pertama, kami menggambarkan apa arti aliasing, dan kemudian kami mencari tahu untuk apa keketatan itu.

Dalam C dan C ++, aliasing terkait dengan jenis ekspresi apa yang diizinkan untuk mengakses nilai yang disimpan. Dalam C dan C ++, standar mendefinisikan ekspresi penamaan mana yang valid untuk tipe apa. Kompilator dan pengoptimal diizinkan untuk menganggap bahwa kami benar-benar mengikuti aturan aliasing, maka istilah - aturan aliasing yang ketat (aturan aliasing ketat). Jika kami mencoba mengakses nilai menggunakan tipe yang tidak valid, itu diklasifikasikan sebagai perilaku tidak terdefinisi (UB). Ketika kita memiliki perilaku yang tidak pasti, semua taruhan dilakukan, hasil dari program kami tidak lagi dapat diandalkan.

Sayangnya, dengan pelanggaran aliasing yang ketat, kami sering mendapatkan hasil yang diharapkan, meninggalkan kemungkinan bahwa versi kompiler masa depan dengan optimasi baru akan melanggar kode yang kami anggap valid. Ini tidak diinginkan, ada baiknya untuk memahami aturan ketat alias dan menghindari melanggar mereka.



Untuk lebih memahami mengapa kita harus khawatir tentang hal ini, kita akan membahas masalah yang muncul ketika melanggar aturan aliasing yang ketat, mengetik hukuman, seperti yang sering digunakan dalam aturan aliasing yang ketat, serta cara membuat dengan benar hukuman, bersama dengan beberapa kemungkinan bantuan dengan C ++ 20 untuk menyederhanakan permainan kata dan mengurangi kemungkinan kesalahan. Kami akan meringkas diskusi dengan mempertimbangkan beberapa metode untuk mendeteksi pelanggaran aturan aliasing yang ketat.

Contoh Awal

Mari kita lihat beberapa contoh, dan kemudian kita bisa membahas apa yang sebenarnya dinyatakan dalam standar, pertimbangkan beberapa contoh tambahan, dan kemudian lihat bagaimana menghindari pengucilan yang ketat dan mengidentifikasi pelanggaran yang kita lewatkan. Berikut adalah contoh yang seharusnya tidak mengejutkan Anda:

int x = 10; int *ip = &x; std::cout << *ip << "\n"; *ip = 12; std::cout << x << "\n"; 

Kami memiliki int * menunjuk ke memori yang ditempati oleh int, dan ini adalah alias yang valid. Pengoptimal harus mengasumsikan bahwa penugasan melalui ip dapat memperbarui nilai yang ditempati oleh x.

Contoh berikut menunjukkan aliasing, yang mengarah ke perilaku tidak terdefinisi:

 int foo( float *f, int *i ) { *i = 1; *f = 0.f; return *i; } int main() { int x = 0; std::cout << x << "\n"; // Expect 0 x = foo(reinterpret_cast<float*>(&x), &x); std::cout << x << "\n"; // Expect 0? } 

Dalam fungsi foo, kita ambil int * dan float *. Dalam contoh ini, kami memanggil foo dan mengatur kedua parameter untuk menunjuk ke lokasi memori yang sama, yang dalam contoh ini berisi int. Perhatikan bahwa reinterpret_cast memberi tahu kompiler untuk memperlakukan ekspresi seolah-olah memiliki tipe yang ditentukan oleh parameter template. Dalam kasus ini, kami katakan padanya untuk memproses ekspresi & x seolah-olah tipe float *. Kita bisa dengan naif berharap bahwa hasil dari cout kedua adalah 0, tetapi ketika optimasi diaktifkan menggunakan -O2 dan gcc, dan dentang akan mendapatkan hasil berikut:
0
1

Yang mungkin tidak terduga, tetapi sepenuhnya benar, karena kami menyebabkan perilaku yang tidak terdefinisi. Float tidak bisa menjadi alias valid dari objek int. Oleh karena itu, pengoptimal dapat mengasumsikan bahwa konstanta 1 yang disimpan selama dereferencing i akan menjadi nilai balik, karena menyimpan melalui f tidak dapat secara benar mempengaruhi objek int. Menghubungkan kode di Compiler Explorer menunjukkan bahwa inilah yang terjadi ( contoh ):

 foo(float*, int*): # @foo(float*, int*) mov dword ptr [rsi], 1 mov dword ptr [rdi], 0 mov eax, 1 ret 

Pengoptimal menggunakan Analisis Alias โ€‹โ€‹Berbasis Jenis (TBAA) mengasumsikan bahwa 1 akan dikembalikan, dan langsung memindahkan nilai konstan ke register eax, yang menyimpan nilai kembali. TBAA menggunakan aturan bahasa tentang jenis mana yang diperbolehkan untuk aliasing untuk mengoptimalkan pemuatan dan penyimpanan. Dalam hal ini, TBAA tahu bahwa float tidak dapat menjadi alias int, dan mengoptimalkan pemuatan saya hingga mati.

Sekarang untuk referensi

Apa sebenarnya yang dikatakan standar tentang apa yang diizinkan dan tidak boleh kita lakukan? Bahasa standar tidak langsung, jadi untuk setiap elemen saya akan mencoba memberikan contoh kode yang menunjukkan makna.

Apa yang dikatakan standar C11?

Standar C11 mengatakan yang berikut di bagian โ€œ6.5 Ekspresiโ€ paragraf 7:

Objek harus memiliki nilai tersimpannya sendiri, akses yang dilakukan hanya menggunakan ekspresi lvalue, yang memiliki salah satu dari jenis berikut: 88) - jenis yang kompatibel dengan jenis objek yang efektif,

 int x = 1; int *p = &x; printf("%d\n", *p); //* p   lvalue-  int,    int 

- versi yang memenuhi syarat dari jenis yang kompatibel dengan jenis objek saat ini,

 int x = 1; const int *p = &x; printf("%d\n", *p); // * p   lvalue-  const int,    int 

- jenis yang merupakan jenis dengan atau tanpa tanda yang sesuai dengan jenis objek yang memenuhi syarat,

 int x = 1; unsigned int *p = (unsigned int*)&x; printf("%u\n", *p ); // *p   lvalue-  unsigned int,      

Lihat Catatan Kaki 12 untuk ekstensi gcc / dentang , yang memungkinkan Anda untuk menetapkan int * int * yang tidak ditandatangani, bahkan jika itu bukan tipe yang kompatibel.

- jenis yang merupakan jenis dengan atau tanpa tanda yang sesuai dengan versi yang memenuhi syarat dari jenis objek saat ini,

 int x = 1; const unsigned int *p = (const unsigned int*)&x; printf("%u\n", *p ); // *p   lvalue-  const unsigned int,     ,        

- tipe agregat atau gabungan yang mencakup salah satu tipe di atas di antara anggotanya (termasuk, secara rekursif, anggota dari sub-agregat atau asosiasi yang terkandung), atau

 struct foo { int x; }; void foobar( struct foo *fp, int *ip );// struct foo -  ,   int   ,       *ip // foo f; foobar( &f, &f.x ); 

- tipe karakter.

 int x = 65; char *p = (char *)&x; printf("%c\n", *p ); // * p   lvalue-  char,    . //    -    . 

Apa C ++ 17 Draft Standard Says

Standar proyek C ++ 17 di bagian 11 [basic.lval] menyatakan: jika suatu program mencoba mengakses nilai yang tersimpan dari suatu objek melalui nilai lebih selain salah satu dari jenis berikut, perilaku tidak terdefinisi: 63 (11.1) adalah jenis objek yang dinamis,

 void *p = malloc( sizeof(int) ); //   ,       int *ip = new (p) int{0}; // placement new      int std::cout << *ip << "\n"; // * ip   glvalue-  int,       

(11.2) - versi cv-kualifikasi (cv - const and volatile) dari tipe dinamis suatu objek,

 int x = 1; const int *cip = &x; std::cout << *cip << "\n"; // * cip    glvalue  const int,   cv-    x 

(11.3) - jenis yang serupa (sebagaimana didefinisikan dalam 7.5) dengan jenis dinamis dari suatu objek,

//

(11.4) - tipe yang merupakan tipe dengan atau tanpa tanda yang sesuai dengan tipe dinamis suatu objek,
// si ui ,
// godbolt (https://godbolt.org/g/KowGXB) , .

 signed int foo( signed int &si, unsigned int &ui ) { si = 1; ui = 2; return si; } 

(11.5) - tipe yang merupakan tipe dengan atau tanpa tanda, sesuai dengan versi yang memenuhi syarat cv dari tipe dinamis suatu objek,

 signed int foo( const signed int &si1, int &si2); //  ,     

(11.6) adalah tipe agregat atau gabungan yang mencakup salah satu dari tipe di atas di antara elemen-elemennya atau elemen data non-statis (termasuk, secara rekursif, sub-agregat atau mengandung elemen data atau elemen data non-statis),

 struct foo { int x; }; 

// Compiler Explorer (https://godbolt.org/g/z2wJTC)

 int foobar( foo &fp, int &ip ) { fp.x = 1; ip = 2; return fp.x; } foo f; foobar( f, fx ); 

(11.7) - tipe yang (mungkin memenuhi syarat cv) tipe kelas dasar dari tipe objek dinamis,

 struct foo { int x ; }; struct bar : public foo {}; int foobar( foo &f, bar &b ) { fx = 1; bx = 2; return fx; } 

(11.8) - ketik char, unsigned char atau std :: byte.

 int foo( std::byte &b, uint32_t &ui ) { b = static_cast<std::byte>('a'); ui = 0xFFFFFFFF; return std::to_integer<int>( b ); // b   glvalue-  std::byte,      uint32_t } 

Perlu dicatat bahwa signed char yang signed char tidak termasuk dalam daftar di atas, ini adalah perbedaan yang nyata dari C, yang berbicara tentang jenis karakter.

Perbedaan yang halus

Jadi, meskipun kita dapat melihat bahwa C dan C ++ mengatakan hal yang sama tentang aliasing, ada beberapa perbedaan yang harus kita waspadai. C ++ tidak memiliki konsep C dari tipe yang valid atau kompatibel , dan C tidak memiliki konsep C ++ dari tipe yang dinamis atau serupa. Meskipun keduanya memiliki ekspresi lvalue dan rvalue, C ++ juga memiliki ekspresi glvalue, prvalue, dan xvalue. Perbedaan-perbedaan ini sebagian besar di luar ruang lingkup artikel ini, tetapi satu contoh yang menarik adalah bagaimana membuat objek dari memori yang digunakan oleh malloc. Dalam C, kita dapat mengatur tipe yang valid, misalnya, menulis ke memori melalui lvalue atau memcpy.

 //     C,    C ++ void *p = malloc(sizeof(float)); float f = 1.0f; memcpy( p, &f, sizeof(float)); //   *p - float  C //  float *fp = p; *fp = 1.0f; //   *p - float  C 

Tidak satu pun dari metode ini yang memadai dalam C ++, yang membutuhkan penempatan baru:

 float *fp = new (p) float{1.0f} ; //   *p  float 

Apakah tipe char int8_t dan uint8_t?

Secara teoritis, int8_t atau uint8_t seharusnya bukan tipe char, tetapi dalam praktiknya mereka diimplementasikan dengan cara itu. Ini penting karena jika mereka benar-benar tipe karakter, maka mereka juga alias tipe char. Jika Anda tidak mengetahui hal ini, ini dapat menyebabkan penurunan kinerja yang tidak terduga . Kita melihat bahwa glibc typedef int8_t dan uint8_t untuk signed char dan unsigned char .

Ini akan sulit untuk diubah, karena untuk C ++ itu akan menjadi celah ABI. Ini akan mengubah distorsi nama dan menghancurkan API apa pun yang menggunakan salah satu dari jenis ini di antarmuka mereka.

Akhir dari bagian pertama. Dan kita akan berbicara tentang pengetikan dan pelurusan huruf dalam beberapa hari.

Tulis komentar Anda dan jangan lewatkan webinar terbuka , yang akan diadakan pada 6 Maret oleh kepala pengembangan teknologi di Rambler & Co, Dmitry Shebordaev .

Source: https://habr.com/ru/post/id442554/


All Articles