Ketika bertanya tentang perilaku umum yang tidak terdefinisi dalam C , orang kadang-kadang merujuk pada aturan aliasing yang ketat.
Apa yang mereka bicarakan?
Ketika bertanya tentang perilaku umum yang tidak terdefinisi dalam C , orang kadang-kadang merujuk pada aturan aliasing yang ketat.
Apa yang mereka bicarakan?
Jawaban:
Situasi umum di mana Anda menemukan masalah aliasing yang ketat adalah ketika overlay struct (seperti perangkat / pesan jaringan) ke buffer ukuran kata sistem Anda (seperti pointer ke uint32_t
s atau uint16_t
s). Ketika Anda overlay struct ke buffer tersebut, atau buffer ke struct tersebut melalui casting pointer Anda dapat dengan mudah melanggar aturan aliasing yang ketat.
Jadi dalam pengaturan seperti ini, jika saya ingin mengirim pesan ke sesuatu, saya harus memiliki dua petunjuk yang tidak kompatibel yang menunjuk ke potongan memori yang sama. Saya kemudian mungkin secara naif kode sesuatu seperti ini (pada sistem dengan sizeof(int) == 2
):
typedef struct Msg
{
unsigned int a;
unsigned int b;
} Msg;
void SendWord(uint32_t);
int main(void)
{
// Get a 32-bit buffer from the system
uint32_t* buff = malloc(sizeof(Msg));
// Alias that buffer through message
Msg* msg = (Msg*)(buff);
// Send a bunch of messages
for (int i =0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendWord(buff[0]);
SendWord(buff[1]);
}
}
Aturan aliasing yang ketat membuat pengaturan ini ilegal: mendereferensi penunjuk yang alias objek yang bukan tipe yang kompatibel atau salah satu dari tipe lain yang diizinkan oleh C 2011 6.5 paragraf 7 1 adalah perilaku yang tidak terdefinisi. Sayangnya, Anda masih bisa membuat kode dengan cara ini, mungkin mendapatkan beberapa peringatan, mengkompilasinya dengan baik, hanya untuk memiliki perilaku aneh yang tidak terduga ketika Anda menjalankan kode.
(GCC tampaknya agak tidak konsisten dalam kemampuannya untuk memberikan peringatan alias, kadang-kadang memberi kita peringatan ramah dan kadang-kadang tidak.)
Untuk melihat mengapa perilaku ini tidak terdefinisi, kita harus berpikir tentang apa aturan aliasing yang ketat membeli kompiler. Pada dasarnya, dengan aturan ini, tidak perlu memikirkan memasukkan instruksi untuk menyegarkan konten dari buff
setiap putaran. Alih-alih, ketika mengoptimalkan, dengan beberapa asumsi yang tidak didukung tentang aliasing, ini dapat menghilangkan instruksi tersebut, memuat buff[0]
dan buff[1
] ke register CPU sekali sebelum loop dijalankan, dan mempercepat tubuh loop. Sebelum alias ketat diperkenalkan, kompiler harus hidup dalam keadaan paranoia bahwa isi buff
dapat berubah kapan saja dari mana saja oleh siapa saja. Jadi untuk mendapatkan keunggulan kinerja tambahan, dan dengan asumsi kebanyakan orang tidak mengetik pointer kata-kata, aturan aliasing yang ketat diperkenalkan.
Perlu diingat, jika Anda pikir contohnya dibuat-buat, ini bahkan dapat terjadi jika Anda meneruskan buffer ke fungsi lain yang melakukan pengiriman untuk Anda, jika sebaliknya Anda memilikinya.
void SendMessage(uint32_t* buff, size_t size32)
{
for (int i = 0; i < size32; ++i)
{
SendWord(buff[i]);
}
}
Dan tulis ulang loop kami sebelumnya untuk memanfaatkan fungsi yang nyaman ini
for (int i = 0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendMessage(buff, 2);
}
Kompiler mungkin atau mungkin tidak bisa atau cukup pintar untuk mencoba menyatukan SendMessage dan mungkin atau mungkin tidak memutuskan untuk memuat atau tidak memuat buff lagi. Jika SendMessage
merupakan bagian dari API lain yang dikompilasi secara terpisah, ia mungkin memiliki instruksi untuk memuat konten buff. Kemudian lagi, mungkin Anda berada di C ++ dan ini adalah beberapa implementasi templated header saja yang menurut kompiler dapat inline. Atau mungkin itu hanya sesuatu yang Anda tulis dalam file .c Anda untuk kenyamanan Anda sendiri. Bagaimanapun perilaku yang tidak terdefinisi mungkin masih terjadi. Bahkan ketika kita mengetahui sebagian dari apa yang terjadi di bawah tenda, itu masih merupakan pelanggaran aturan sehingga tidak ada perilaku yang jelas yang dijamin. Jadi hanya dengan membungkus suatu fungsi yang mengambil kata buffer terbatas kami tidak selalu membantu.
Jadi bagaimana saya mengatasi ini?
Gunakan serikat pekerja. Kebanyakan kompiler mendukung ini tanpa mengeluh tentang alias ketat. Ini diizinkan di C99 dan secara eksplisit diizinkan di C11.
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
Anda dapat menonaktifkan alias ketat di kompiler Anda ( f [no-] strict-aliasing di gcc))
Anda dapat menggunakan char*
untuk alias daripada kata-kata sistem Anda. Aturan memungkinkan pengecualian untuk char*
(termasuk signed char
dan unsigned char
). Itu selalu dianggap bahwa char*
alias jenis lain. Namun ini tidak akan bekerja sebaliknya: tidak ada asumsi bahwa struct Anda alias buffer chars.
Hati-hati pemula
Ini hanya satu ladang ranjau yang potensial ketika overlay dua jenis satu sama lain. Anda juga harus belajar tentang endianness , penyelarasan kata , dan cara menangani masalah penyelarasan melalui pengemasan struct dengan benar.
1 Jenis yang dapat diakses oleh C 2011 6.5 7 adalah nilai:
unsigned char*
digunakan jauh char*
sebagai gantinya? Saya cenderung menggunakan unsigned char
daripada char
sebagai tipe yang mendasari byte
karena byte saya tidak ditandatangani dan saya tidak ingin keanehan perilaku yang ditandatangani (terutama wrt to overflow)
unsigned char *
tidak apa-apa.
uint32_t* buff = malloc(sizeof(Msg));
serikat Anda dan selanjutnya unsigned int asBuffer[sizeof(Msg)];
akan memiliki ukuran yang berbeda dan tidak ada yang benar. The malloc
panggilan mengandalkan pada keselarasan 4 byte bawah tenda (tidak melakukannya) dan serikat pekerja akan 4 kali lebih besar dari itu perlu ... Saya mengerti bahwa itu adalah untuk kejelasan tetapi mengganggu saya tidak ada-the- less ...
Penjelasan terbaik yang saya temukan adalah oleh Mike Acton, Understanding Strict Aliasing . Ini sedikit berfokus pada pengembangan PS3, tapi itu pada dasarnya hanya GCC.
Dari artikel:
"Aliasing ketat adalah asumsi, dibuat oleh kompiler C (atau C ++), bahwa pointer dereferencing ke objek dari tipe yang berbeda tidak akan pernah merujuk ke lokasi memori yang sama (yaitu saling alias.)"
Jadi pada dasarnya jika Anda memiliki int*
menunjuk ke beberapa memori yang mengandung int
dan kemudian Anda mengarahkan float*
ke memori itu dan menggunakannya sebagai float
Anda melanggar aturan. Jika kode Anda tidak menghargai ini, maka pengoptimal kompiler kemungkinan besar akan memecahkan kode Anda.
Pengecualian aturan adalah a char*
, yang diizinkan untuk menunjuk ke jenis apa pun.
Ini adalah aturan aliasing yang ketat, ditemukan di bagian 3.10 dari standar C ++ 03 (jawaban lain memberikan penjelasan yang baik, tetapi tidak ada yang memberikan aturan itu sendiri):
Jika suatu program mencoba mengakses nilai yang tersimpan dari suatu objek melalui nilai lebih dari satu dari jenis berikut ini, perilaku tersebut tidak terdefinisi:
- jenis objek yang dinamis,
- versi yang memenuhi syarat cv dari tipe dinamis objek,
- tipe yang tipe bertanda tangan atau tidak bertanda tangan yang sesuai dengan tipe objek yang dinamis,
- tipe yang merupakan tipe bertanda tangan atau tidak bertanda tangan yang sesuai dengan versi yang memenuhi syarat cv dari tipe objek yang dinamis,
- suatu jenis agregat atau serikat yang mencakup salah satu dari jenis-jenis yang disebutkan di atas di antara para anggotanya (termasuk, secara rekursif, seorang anggota dari sub-agregat atau serikat yang berisi),
- tipe yang merupakan tipe kelas dasar (mungkin berkualifikasi cv) dari tipe dinamis objek,
- a
char
atauunsigned char
tipe.
Kata-kata C ++ 11 dan C ++ 14 (perubahan ditekankan):
Jika program upaya untuk mengakses nilai yang disimpan dalam sebuah objek melalui glvalue dari selain salah satu jenis berikut perilaku yang tidak terdefinisi:
- jenis objek yang dinamis,
- versi yang memenuhi syarat cv dari tipe dinamis objek,
- tipe yang mirip (seperti yang didefinisikan dalam 4.4) dengan tipe dinamis dari objek,
- tipe yang tipe bertanda tangan atau tidak bertanda tangan yang sesuai dengan tipe objek yang dinamis,
- tipe yang merupakan tipe bertanda tangan atau tidak bertanda tangan yang sesuai dengan versi yang memenuhi syarat cv dari tipe objek yang dinamis,
- tipe agregat atau gabungan yang mencakup salah satu jenis yang disebutkan di atas di antara elemen - elemennya atau anggota data non-statis (termasuk, secara rekursif, elemen atau anggota data non-statis dari subagregat atau serikat yang berisi),
- tipe yang merupakan tipe kelas dasar (mungkin berkualifikasi cv) dari tipe dinamis objek,
- a
char
atauunsigned char
tipe.
Dua perubahan kecil: glvalue bukan lvalue , dan klarifikasi kasus agregat / serikat pekerja.
Perubahan ketiga membuat jaminan yang lebih kuat (melonggarkan aturan aliasing yang kuat): Konsep baru jenis serupa yang sekarang aman untuk alias.
Juga kata-kata C (C99; ISO / IEC 9899: 1999 6.5 / 7; kata-kata yang persis sama digunakan dalam ISO / IEC 9899: 2011 §6.5 ¶7):
Objek harus memiliki nilai tersimpan diakses hanya oleh ekspresi lvalue yang memiliki salah satu dari tipe berikut 73) atau 88) :
- jenis yang kompatibel dengan jenis objek yang efektif,
- versi yang memenuhi syarat dari jenis yang kompatibel dengan jenis objek yang efektif,
- tipe yang merupakan tipe bertanda tangan atau tidak bertanda tangan yang sesuai dengan jenis objek yang efektif,
- tipe yang merupakan tipe bertanda tangan atau tidak bertanda tangan yang sesuai dengan versi yang memenuhi syarat dari jenis objek yang efektif,
- suatu jenis agregat atau serikat yang mencakup salah satu dari jenis-jenis yang disebutkan di atas di antara para anggotanya (termasuk, secara rekursif, seorang anggota serikat pekerja sub-agregat atau yang berisi), atau
- tipe karakter.
73) atau 88) Maksud dari daftar ini adalah untuk menentukan keadaan-keadaan di mana suatu objek mungkin atau mungkin tidak alias.
wow(&u->s1,&u->s2)
akan perlu legal bahkan ketika pointer digunakan untuk memodifikasi u
, dan itu akan meniadakan sebagian besar optimisasi bahwa aturan aliasing dirancang untuk memfasilitasi.
Ini dikutip dari "Apa Aturan Ketegasan Mengasingkan Diri dan Mengapa Kita Peduli?"menulis
Dalam C dan C ++ aliasing harus dilakukan dengan tipe ekspresi apa yang diizinkan untuk mengakses nilai yang disimpan. Dalam C dan C ++ standar menentukan jenis ekspresi yang diizinkan untuk alias jenis apa. Kompilator dan pengoptimal diizinkan untuk menganggap kami mengikuti aturan aliasing secara ketat, oleh karena itu istilah aturan aliasing ketat . Jika kami mencoba mengakses nilai menggunakan tipe yang tidak diizinkan, itu diklasifikasikan sebagai perilaku tidak terdefinisi ( UB ). Setelah kami memiliki perilaku yang tidak terdefinisi, semua taruhan dimatikan, hasil dari program kami tidak lagi dapat diandalkan.
Sayangnya dengan pelanggaran alias ketat, kita akan sering mendapatkan hasil yang kita harapkan, meninggalkan kemungkinan versi kompiler masa depan dengan optimasi baru akan memecahkan kode yang kita anggap valid. Ini tidak diinginkan dan merupakan tujuan yang berharga untuk memahami aturan alias yang ketat dan bagaimana cara menghindari pelanggaran.
Untuk memahami lebih lanjut mengapa kami peduli, kami akan membahas masalah yang muncul saat melanggar aturan aliasing yang ketat, mengetik hukuman karena teknik umum yang digunakan dalam hukuman jenis sering melanggar aturan alias yang ketat dan cara mengetik pun dengan benar.
Mari kita lihat beberapa contoh, lalu kita bisa bicara tentang apa yang standar katakan, periksa beberapa contoh lebih lanjut dan kemudian lihat bagaimana menghindari alias ketat dan menangkap pelanggaran yang kita lewatkan. Berikut adalah contoh yang tidak mengejutkan ( contoh langsung ):
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
Kami memiliki int * yang 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 ( contoh langsung ):
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 mengambil int * dan float * , dalam contoh ini kita memanggil foo dan mengatur kedua parameter untuk menunjuk ke lokasi memori yang sama yang dalam contoh ini berisi int . Catatan, reinterpret_cast memberi tahu kompiler untuk memperlakukan ekspresi seolah-olah memiliki tipe yang ditentukan oleh parameter templatnya. Dalam hal ini kami mengatakan untuk memperlakukan ekspresi & x seolah-olah ia memiliki tipe float * . Kami mungkin secara naif mengharapkan hasil dari cout kedua menjadi 0 tetapi dengan optimasi yang diaktifkan menggunakan -O2 gcc dan dentang menghasilkan hasil berikut:
0
1
Yang mungkin tidak diharapkan tetapi sangat valid karena kami telah memanggil perilaku yang tidak terdefinisi. Sebuah pelampung tidak bisa secara sah alias sebuah int objek. Oleh karena itu pengoptimal dapat mengasumsikan konstanta 1 yang disimpan ketika dereferencing i akan menjadi nilai kembali karena toko melalui f tidak dapat secara valid memengaruhi objek int . Memasukkan kode di Compiler Explorer menunjukkan ini persis seperti apa yang terjadi ( contoh langsung ):
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 1 akan dikembalikan dan langsung memindahkan nilai konstan ke register eax yang membawa nilai kembali. TBAA menggunakan aturan bahasa tentang jenis apa yang diizinkan alias untuk mengoptimalkan pemuatan dan penyimpanan. Dalam hal ini TBAA tahu bahwa float tidak bisa alias dan int dan mengoptimalkan beban i .
Apa sebenarnya yang menurut standar ini diizinkan dan tidak boleh kita lakukan? Bahasa standar tidak langsung, jadi untuk setiap item saya akan mencoba memberikan contoh kode yang menunjukkan artinya.
Standar C11 mengatakan yang berikut ini di bagian 6.5 Ekspresi paragraf 7 :
Objek harus memiliki nilai tersimpan diakses hanya oleh 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 gives us an lvalue expression of type int which is compatible with int
- versi yang memenuhi syarat dari jenis yang kompatibel dengan jenis objek yang efektif,
int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int
- tipe yang merupakan tipe bertanda tangan atau tidak bertanda tangan yang sesuai dengan jenis objek yang efektif,
int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to
// the effective type of the object
gcc / clang memiliki ekstensi dan juga yang memungkinkan menetapkan int * ke int * yang tidak ditandatangani meskipun mereka bukan tipe yang kompatibel.
- tipe yang merupakan tipe bertanda tangan atau tidak bertanda tangan yang sesuai dengan versi terkualifikasi dari jenis objek yang efektif,
int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type
// that corresponds with to a qualified verison of the effective type of the object
- suatu jenis agregat atau serikat yang mencakup salah satu dari jenis-jenis yang disebutkan di atas di antara para anggotanya (termasuk, secara rekursif, seorang anggota serikat pekerja sub-agregat atau yang berisi), atau
struct foo {
int x;
};
void foobar( struct foo *fp, int *ip ); // struct foo is an aggregate that includes int among its members so it can
// can alias with *ip
foo f;
foobar( &f, &f.x );
- tipe karakter.
int x = 65;
char *p = (char *)&x;
printf("%c\n", *p ); // *p gives us an lvalue expression of type char which is a character type.
// The results are not portable due to endianness issues.
Draf standar C ++ 17 pada bagian [basic.lval] paragraf 11 mengatakan:
Jika suatu program mencoba untuk mengakses nilai yang tersimpan dari suatu objek melalui nilai lain selain salah satu dari jenis berikut perilaku tidak terdefinisi: 63 (11.1) - tipe dinamis dari objek,
void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0}; // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n"; // *ip gives us a glvalue expression of type int which matches the dynamic type
// of the allocated object
(11.2) - versi yang memenuhi syarat cv dari tipe dinamis objek,
int x = 1;
const int *cip = &x;
std::cout << *cip << "\n"; // *cip gives us a glvalue expression of type const int which is a cv-qualified
// version of the dynamic type of x
(11.3) - jenis yang serupa (sebagaimana didefinisikan dalam 7.5) dengan jenis dinamis objek,
(11.4) - jenis yang bertanda tangan atau tidak bertanda yang sesuai dengan jenis objek yang dinamis,
// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
si = 1;
ui = 2;
return si;
}
(11.5) - tipe yang tipe bertanda tangan atau tidak bertanda yang sesuai dengan versi dinamis dari tipe objek yang dilindungi cv,
signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
(11.6) - suatu jenis agregat atau gabungan yang mencakup salah satu dari jenis-jenis tersebut di atas di antara elemen-elemennya atau anggota data yang tidak statis (termasuk, secara rekursif, elemen atau anggota data non-statis dari suatu sub-agregat atau serikat yang terkandung),
struct foo {
int x;
};
// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
fp.x = 1;
ip = 2;
return fp.x;
}
foo f;
foobar( f, f.x );
(11.7) - tipe yang merupakan tipe kelas dasar (mungkin cv-kualifikasi) dari tipe dinamis objek,
struct foo { int x ; };
struct bar : public foo {};
int foobar( foo &f, bar &b ) {
f.x = 1;
b.x = 2;
return f.x;
}
(11.8) - tipe 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 gives us a glvalue expression of type std::byte which can alias
// an object of type uint32_t
}
Perlu dicatat char yang ditandatangani tidak termasuk dalam daftar di atas, ini adalah perbedaan penting dari C yang mengatakan tipe karakter .
Kami telah sampai pada titik ini dan kami mungkin bertanya-tanya, mengapa kami ingin alias untuk? Jawabannya biasanya adalah mengetik pun , seringkali metode yang digunakan melanggar aturan aliasing yang ketat.
Kadang-kadang kita ingin menghindari sistem tipe dan menafsirkan objek sebagai tipe yang berbeda. Ini disebut type punning , untuk menafsirkan kembali segmen memori sebagai tipe lain. Jenis punning berguna untuk tugas-tugas yang menginginkan akses ke representasi objek yang mendasarinya untuk dilihat, dipindahkan, atau dimanipulasi. Area umum yang kami temukan jenis punning yang digunakan adalah kompiler, serialisasi, kode jaringan, dll ...
Secara tradisional ini telah dicapai dengan mengambil alamat objek, melemparkannya ke pointer dari jenis yang ingin kita tafsirkan sebagai dan kemudian mengakses nilai, atau dengan kata lain dengan alias. Sebagai contoh:
int x = 1 ;
// In C
float *fp = (float*)&x ; // Not a valid aliasing
// In C++
float *fp = reinterpret_cast<float*>(&x) ; // Not a valid aliasing
printf( "%f\n", *fp ) ;
Seperti yang telah kita lihat sebelumnya, ini bukan alias yang valid, jadi kami menerapkan perilaku yang tidak terdefinisi. Tapi kompiler tradisional tidak mengambil keuntungan dari aturan aliasing yang ketat dan jenis kode ini biasanya hanya bekerja, sayangnya pengembang sudah terbiasa melakukan hal-hal seperti ini. Metode alternatif umum untuk jenis hukuman adalah melalui serikat pekerja, yang berlaku di C tetapi perilaku tidak terdefinisi dalam C ++ ( lihat contoh langsung ):
union u1
{
int n;
float f;
} ;
union u1 u;
u.f = 1.0f;
printf( "%d\n”, u.n ); // UB in C++ n is not the active member
Ini tidak valid dalam C ++ dan beberapa orang menganggap tujuan serikat pekerja semata-mata untuk menerapkan jenis varian dan merasa menggunakan serikat pekerja untuk jenis hukuman adalah penyalahgunaan.
Metode standar untuk mengetik jenis dalam C dan C ++ adalah memcpy . Ini mungkin tampak agak berat, tetapi pengoptimal harus mengenali penggunaan memcpy untuk jenis hukuman dan mengoptimalkannya dan menghasilkan register untuk mendaftar pindah. Sebagai contoh jika kita tahu int64_t berukuran sama dengan ganda :
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17 does not require a message
kita bisa menggunakan memcpy :
void func1( double d ) {
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
//...
Pada tingkat optimisasi yang memadai setiap kompiler modern yang layak menghasilkan kode yang identik dengan metode reinterpret_cast yang disebutkan sebelumnya atau metode gabungan untuk jenis punning . Meneliti kode yang dihasilkan, kami melihatnya hanya menggunakan mov saja ( contoh Compiler Explorer langsung ).
Dalam C ++ 20 kita dapat memperoleh bit_cast ( implementasi tersedia dalam tautan dari proposal ) yang memberikan cara sederhana dan aman untuk mengetik-pun serta dapat digunakan dalam konteks constexpr.
Berikut ini adalah contoh cara menggunakan bit_cast untuk mengetik pun int yang tidak ditandatangani ke float , ( lihat langsung ):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
Dalam kasus di mana jenis Ke dan Dari tidak memiliki ukuran yang sama, itu mengharuskan kita untuk menggunakan struktur perantara15. Kami akan menggunakan struct yang berisi array karakter sizeof (unsigned int) ( mengasumsikan 4 byte unsigned int ) sebagai tipe Dari dan unsigned int sebagai tipe Ke . :
struct uint_chars {
unsigned char arr[sizeof( unsigned int )] = {} ; // Assume sizeof( unsigned int ) == 4
};
// Assume len is a multiple of 4
int bar( unsigned char *p, size_t len ) {
int result = 0;
for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
uint_chars f;
std::memcpy( f.arr, &p[index], sizeof(unsigned int));
unsigned int result = bit_cast<unsigned int>(f);
result += foo( result );
}
return result ;
}
Sangat disayangkan bahwa kita membutuhkan tipe perantara ini tetapi itu adalah batasan bit_cast saat ini .
Kami tidak memiliki banyak alat bagus untuk menangkap aliasing ketat di C ++, alat yang kami miliki akan menangkap beberapa kasus pelanggaran aliasing ketat dan beberapa kasus pemuatan dan penyimpanan yang tidak selaras.
gcc menggunakan flag -fstrict-aliasing dan -Wstrict-aliasing dapat menangkap beberapa case meskipun bukan tanpa false positive / negative. Misalnya, kasus-kasus berikut akan menghasilkan peringatan dalam gcc ( lihat langsung ):
int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught
// it was being accessed w/ an indeterminate value below
printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
meskipun tidak akan menangkap kasus tambahan ini ( lihat langsung ):
int *p;
p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));
Meskipun dentang memungkinkan bendera ini, tampaknya itu tidak benar-benar menerapkan peringatan.
Alat lain yang kami miliki adalah ASan yang dapat menangkap banyak barang dan toko yang tidak selaras. Meskipun ini bukan pelanggaran alias langsung yang ketat, namun ini adalah hasil umum dari pelanggaran alias yang ketat. Sebagai contoh kasus-kasus berikut akan menghasilkan kesalahan runtime ketika dibangun dengan dentang menggunakan -fsanitize = alamat
int *x = new int[2]; // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6); // regardless of alignment of x this will not be an aligned address
*u = 1; // Access to range [6-9]
printf( "%d\n", *u ); // Access to range [6-9]
Alat terakhir yang akan saya rekomendasikan adalah C ++ spesifik dan tidak sepenuhnya alat tetapi praktik pengkodean, jangan izinkan gips C-style. Baik gcc dan dentang akan menghasilkan diagnostik untuk cast gaya-C menggunakan -Wold-style-cast . Ini akan memaksa setiap jenis permainan kata yang tidak terdefinisi untuk menggunakan reinterpret_cast, secara umum reinterpret_cast harus menjadi bendera untuk peninjauan kode yang lebih dekat. Juga lebih mudah untuk mencari basis kode Anda untuk reinterpret_cast untuk melakukan audit.
Untuk C kami memiliki semua alat yang sudah dibahas dan kami juga memiliki tis-interpreter, penganalisa statis yang secara mendalam menganalisis program untuk sebagian besar bahasa C. Diberikan versi C dari contoh sebelumnya di mana menggunakan -fstrict-aliasing melewatkan satu kasus ( lihat langsung )
int a = 1;
short j;
float f = 1.0 ;
printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));
int *p;
p=&a;
printf("%i\n", j = *((short*)p));
tis-interpeter dapat menangkap ketiganya, contoh berikut memanggil tis-kernal sebagai tis-interpreter (output diedit untuk singkatnya):
./bin/tis-kernel -sa example1.c
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
rules by accessing a cell with effective type int.
...
example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
accessing a cell with effective type float.
Callstack: main
...
example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
accessing a cell with effective type int.
Akhirnya ada TySan yang saat ini dalam pengembangan. Pembersih ini menambahkan tipe memeriksa informasi dalam segmen memori bayangan dan memeriksa akses untuk melihat apakah mereka melanggar aturan alias. Alat tersebut berpotensi dapat menangkap semua pelanggaran alias tetapi mungkin memiliki overhead run-time yang besar.
reinterpret_cast
mungkin dilakukan atau apa yang cout
mungkin berarti. (Tidak apa-apa menyebutkan C ++ tetapi pertanyaan aslinya adalah tentang C dan IIUC contoh-contoh ini dapat ditulis dalam bahasa C.)
Aliasing yang ketat tidak hanya merujuk ke pointer, tetapi juga mempengaruhi referensi, saya menulis makalah tentang itu untuk meningkatkan wiki pengembang dan diterima dengan sangat baik sehingga saya mengubahnya menjadi halaman di situs web konsultasi saya. Ini menjelaskan sepenuhnya apa itu, mengapa hal itu membingungkan banyak orang dan apa yang harus dilakukan. Kertas Putih Aliasing Yang Ketat . Secara khusus ini menjelaskan mengapa serikat pekerja adalah perilaku berisiko untuk C ++, dan mengapa menggunakan memcpy adalah satu-satunya portable fix di C dan C ++. Semoga ini bermanfaat.
Sebagai tambahan untuk apa yang sudah ditulis Doug T., berikut adalah contoh kasus sederhana yang mungkin memicunya dengan gcc:
check.c
#include <stdio.h>
void check(short *h,long *k)
{
*h=5;
*k=6;
if (*h == 5)
printf("strict aliasing problem\n");
}
int main(void)
{
long k[1];
check((short *)k,k);
return 0;
}
Kompilasi dengan gcc -O2 -o check check.c
. Biasanya (dengan sebagian besar versi gcc yang saya coba) ini menghasilkan "masalah aliasing yang ketat", karena kompilator mengasumsikan bahwa "h" tidak boleh alamat yang sama dengan "k" dalam fungsi "centang". Karena itu kompiler mengoptimalkan if (*h == 5)
pergi dan selalu memanggil printf.
Bagi mereka yang tertarik di sini adalah kode assembler x64, diproduksi oleh gcc 4.6.3, berjalan di ubuntu 12.04.2 untuk x64:
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
Jadi jika kondisi benar-benar hilang dari kode assembler.
long long*
dan int64_t
*). Orang mungkin berharap bahwa sebuah kompiler waras harus mengenali bahwa a long long*
dan int64_t*
dapat mengakses penyimpanan yang sama jika disimpan secara identik, tetapi perlakuan seperti itu tidak lagi modis.
Jenis punning via cast pointer (sebagai lawan menggunakan union) adalah contoh utama dari melanggar alias ketat.
fpsync()
arahan antara menulis sebagai fp dan membaca sebagai int atau sebaliknya [pada implementasi dengan integer terpisah dan jalur pipa dan cache FPU , arahan semacam itu mungkin mahal, tetapi tidak semahal kompiler melakukan sinkronisasi seperti itu pada setiap akses serikat]. Atau suatu implementasi dapat menentukan bahwa nilai yang dihasilkan tidak akan pernah dapat digunakan kecuali dalam keadaan menggunakan Common Initial Sequences.
Menurut alasan C89, penulis Standar tidak ingin mengharuskan kompiler memberikan kode seperti:
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
harus diminta untuk memuat kembali nilai x
antara penugasan dan pernyataan kembali sehingga memungkinkan untuk kemungkinan yang p
menunjuk x
, dan penugasan untuk *p
dapat akibatnya mengubah nilai x
. Gagasan bahwa seorang kompiler harus berhak berasumsi bahwa tidak akan ada alias dalam situasi seperti di atas adalah tidak kontroversial.
Sayangnya, para penulis C89 menulis aturan mereka dengan cara yang, jika dibaca secara harfiah, akan membuat bahkan fungsi berikut memohon Perilaku Tidak Terdefinisi:
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
karena ia menggunakan nilai tipe int
untuk mengakses objek tipe struct S
, dan int
tidak di antara tipe yang dapat digunakan mengaksesstruct S
. Karena tidak masuk akal untuk memperlakukan semua penggunaan anggota tipe non-karakter dari struct dan serikat sebagai Perilaku Tidak Terdefinisi, hampir semua orang mengakui bahwa setidaknya ada beberapa keadaan di mana nilai suatu jenis dapat digunakan untuk mengakses objek dari tipe lain. . Sayangnya, Komite Standar C telah gagal untuk menentukan keadaan apa itu.
Sebagian besar masalah adalah hasil dari Laporan Cacat # 028, yang bertanya tentang perilaku program seperti:
int test(int *ip, double *dp)
{
*ip = 1;
*dp = 1.23;
return *ip;
}
int test2(void)
{
union U { int i; double d; } u;
return test(&u.i, &u.d);
}
Laporan Cacat # 28 menyatakan bahwa program ini memanggil Perilaku Tidak Terdefinisi karena tindakan menulis anggota serikat tipe "ganda" dan membaca salah satu tipe "int" memunculkan perilaku yang Ditetapkan Implementasi. Alasan seperti itu tidak masuk akal, tetapi membentuk dasar bagi aturan Tipe Efektif yang tidak perlu mempersulit bahasa saat tidak melakukan apa pun untuk mengatasi masalah aslinya.
Cara terbaik untuk menyelesaikan masalah asli mungkin dengan memperlakukan catatan kaki tentang tujuan aturan seolah-olah itu normatif, dan membuat aturan tidak dapat diterapkan kecuali dalam kasus yang sebenarnya melibatkan akses yang saling bertentangan menggunakan alias. Diberikan sesuatu seperti:
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
s.x = 1;
p = &s.x;
inc_int(p);
return s.x;
}
Tidak ada konflik di dalamnya inc_int
karena semua akses ke penyimpanan yang diakses melalui *p
dilakukan dengan nilai tipe yang tinggi int
, dan tidak ada konflik di dalam test
karena p
terlihat berasal dari struct S
, dan pada saat s
digunakan, semua akses ke penyimpanan yang akan dibuat melalui p
akan sudah terjadi.
Jika kode diubah sedikit ...
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
p = &s.x;
s.x = 1; // !!*!!
*p += 1;
return s.x;
}
Di sini, ada konflik alias antara p
dan akses ke s.x
pada baris yang ditandai karena pada saat itu dalam eksekusi referensi lain ada yang akan digunakan untuk mengakses penyimpanan yang sama .
Seandainya Laporan Cacat 028 mengatakan contoh asli meminta UB karena tumpang tindih antara penciptaan dan penggunaan dua petunjuk, yang akan membuat segalanya lebih jelas tanpa harus menambahkan "Tipe Efektif" atau kompleksitas lainnya.
Setelah membaca banyak jawaban, saya merasa perlu menambahkan sesuatu:
Aliasing yang ketat (yang akan saya jelaskan sedikit) adalah penting karena :
Akses memori bisa mahal (berdasarkan kinerja), itulah sebabnya data dimanipulasi dalam register CPU sebelum ditulis kembali ke memori fisik.
Jika data dalam dua register CPU yang berbeda akan ditulis ke ruang memori yang sama, kami tidak dapat memprediksi data mana yang akan "bertahan" ketika kami kode dalam C.
Dalam perakitan, di mana kita mengkode pemuatan dan pembongkaran register CPU secara manual, kita akan tahu data mana yang tetap utuh. Tapi C (untungnya) abstrak detail ini.
Karena dua pointer dapat menunjuk ke lokasi yang sama di memori, ini dapat menghasilkan kode kompleks yang menangani kemungkinan tabrakan .
Kode tambahan ini lambat dan mengganggu kinerja karena menjalankan operasi baca / tulis memori ekstra yang lebih lambat dan (mungkin) tidak perlu.
The Aturan aliasing ketat memungkinkan kita untuk menghindari kode mesin berlebihan dalam kasus-kasus di mana harus aman untuk mengasumsikan bahwa dua pointer tidak menunjuk ke blok memori yang sama (lihat juga restrict
kata kunci).
Status aliasing yang ketat aman untuk mengasumsikan bahwa pointer ke tipe yang berbeda menunjuk ke lokasi yang berbeda dalam memori.
Jika kompiler memperhatikan bahwa dua pointer menunjuk ke tipe yang berbeda (misalnya, a int *
dan a float *
), itu akan menganggap alamat memori berbeda dan itu tidak akan melindungi terhadap benturan alamat memori, menghasilkan kode mesin yang lebih cepat.
Sebagai contoh :
Mari kita asumsikan fungsi berikut:
void merge_two_ints(int *a, int *b) {
*b += *a;
*a += *b;
}
Untuk menangani kasus di mana a == b
(kedua pointer menunjuk ke memori yang sama), kita perlu memesan dan menguji cara kita memuat data dari memori ke register CPU, sehingga kode mungkin berakhir seperti ini:
memuat a
dan b
dari memori.
tambahkan a
ke b
.
simpan b
dan muat ulang a
.
(simpan dari register CPU ke memori dan muat dari memori ke register CPU).
tambahkan b
ke a
.
simpan a
(dari register CPU) ke memori.
Langkah 3 sangat lambat karena perlu mengakses memori fisik. Namun, itu diperlukan untuk melindungi terhadap contoh di mana a
dan b
menunjuk ke alamat memori yang sama.
Aliasing yang ketat akan memungkinkan kami untuk mencegah hal ini dengan memberi tahu kompiler bahwa alamat memori ini sangat berbeda (yang, dalam hal ini, akan memungkinkan optimasi lebih lanjut yang tidak dapat dilakukan jika pointer berbagi alamat memori).
Ini dapat diceritakan ke kompiler dengan dua cara, dengan menggunakan tipe yang berbeda untuk menunjuk. yaitu:
void merge_two_numbers(int *a, long *b) {...}
Menggunakan restrict
kata kunci. yaitu:
void merge_two_ints(int * restrict a, int * restrict b) {...}
Sekarang, dengan memenuhi aturan Stasing Aliasing, langkah 3 dapat dihindari dan kode akan berjalan secara signifikan lebih cepat.
Bahkan, dengan menambahkan restrict
kata kunci, seluruh fungsi dapat dioptimalkan untuk:
memuat a
dan b
dari memori.
tambahkan a
ke b
.
simpan hasil untuk a
dan untuk b
.
Optimalisasi ini tidak dapat dilakukan sebelumnya, karena kemungkinan tabrakan (di mana a
dan b
akan menjadi tiga kali lipat daripada dua kali lipat).
b
(tidak memuat ulang) dan memuat ulang a
. Saya harap ini lebih jelas sekarang.
restrict
, tetapi saya akan berpikir bahwa yang terakhir akan dalam kebanyakan keadaan lebih efektif, dan melonggarkan beberapa kendala register
akan memungkinkannya untuk mengisi beberapa kasus di mana restrict
tidak akan membantu. Saya tidak yakin itu pernah "penting" untuk memperlakukan Standar sebagai menggambarkan sepenuhnya semua kasus di mana programmer harus mengharapkan kompiler untuk mengenali bukti aliasing, daripada hanya menggambarkan tempat-tempat di mana kompiler harus mengandaikan aliasing bahkan ketika tidak ada bukti tertentu itu ada .
restrict
kata kunci meminimalkan tidak hanya kecepatan operasi tetapi jumlah mereka juga, yang bisa bermakna ... Maksudku, bagaimanapun juga, operasi tercepat adalah tidak ada operasi sama sekali :)
Aliasing yang ketat tidak memungkinkan tipe pointer yang berbeda untuk data yang sama.
Artikel ini akan membantu Anda memahami masalah ini secara terperinci.
int
dan struct yang berisi a int
).
Secara teknis di C ++, aturan aliasing yang ketat mungkin tidak pernah berlaku.
Perhatikan definisi tipuan ( * operator ):
Operator unary * melakukan tipuan: ekspresi yang diterapkan harus berupa pointer ke tipe objek, atau pointer ke tipe fungsi dan hasilnya adalah nilai yang merujuk ke objek atau fungsi yang menjadi titik ekspresi .
Juga dari definisi glvalue
Glvalue adalah ekspresi yang evaluasinya menentukan identitas suatu objek, (... snip)
Jadi dalam setiap jejak program yang didefinisikan dengan baik, glvalue merujuk ke suatu objek. Jadi aturan aliasing yang ketat tidak berlaku, tidak pernah. Ini mungkin bukan yang diinginkan oleh para desainer.
int foo;
, apa yang diakses oleh ekspresi nilai *(char*)&foo
? Apakah itu tipe objek char
? Apakah objek itu muncul pada saat yang sama foo
? Apakah tulisan akan foo
mengubah nilai yang disimpan dari objek jenis yang disebutkan di atas char
? Jika demikian, apakah ada aturan yang akan memungkinkan nilai yang disimpan dari objek bertipe char
dapat diakses menggunakan nilai ltipe int
?
int i;
membuat empat objek dari setiap jenis karakter in addition to one of type
int ? I see no way to apply a consistent definition of "object" which would allow for operations on both
* (char *) & i` dan i
. Akhirnya, tidak ada dalam Standar yang memungkinkan bahkan volatile
pointer yang memenuhi syarat untuk mengakses register perangkat keras yang tidak memenuhi definisi "objek".
c
danc++faq
.