Apa aturan aliasing yang ketat?

804

Ketika bertanya tentang perilaku umum yang tidak terdefinisi dalam C , orang kadang-kadang merujuk pada aturan aliasing yang ketat.
Apa yang mereka bicarakan?

Benoit
sumber
12
@Ben Voigt: Aturan aliasing berbeda untuk c ++ dan c. Mengapa pertanyaan ini ditandai dengan cdan c++faq.
MikeMB
6
@MikeMB: Jika Anda memeriksa riwayatnya, Anda akan melihat bahwa saya menyimpan tag seperti semula, meskipun ada upaya beberapa ahli lain untuk mengubah pertanyaan dari jawaban yang ada. Selain itu, ketergantungan bahasa dan ketergantungan versi adalah bagian yang sangat penting dari jawaban untuk "Apa aturan aliasing yang ketat?" dan mengetahui perbedaan itu penting untuk tim yang memigrasi kode antara C dan C ++, atau menulis makro untuk digunakan di keduanya.
Ben Voigt
6
@ Ben Voigt: Sebenarnya - sejauh yang saya tahu - sebagian besar jawaban hanya berhubungan dengan c dan bukan c ++ juga kata-kata dari pertanyaan menunjukkan fokus pada aturan-C (atau OP hanya tidak sadar, bahwa ada perbedaan ). Untuk sebagian besar aturan dan Ide umum tentu saja sama, tetapi terutama, di mana serikat terkait jawabannya tidak berlaku untuk c ++. Saya sedikit khawatir, bahwa beberapa programmer c ++ akan mencari aturan aliasing yang ketat dan hanya akan menganggap bahwa semua yang dinyatakan di sini juga berlaku untuk c ++.
MikeMB
Di sisi lain, saya setuju bahwa itu adalah masalah untuk mengubah pertanyaan setelah banyak jawaban yang baik telah diposting dan masalah ini masih kecil.
MikeMB
1
@MikeMB: Saya pikir Anda akan melihat bahwa C fokus pada jawaban yang diterima, membuatnya salah untuk C ++, diedit oleh pihak ketiga. Bagian itu mungkin harus direvisi lagi.
Ben Voigt

Jawaban:

562

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_ts atau uint16_ts). 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 buffsetiap 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 buffdapat 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 SendMessagemerupakan 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 chardan 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.

Catatan kaki

1 Jenis yang dapat diakses oleh C 2011 6.5 7 adalah nilai:

  • 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 terkualifikasi dari tipe efektif objek,
  • 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.
Doug T.
sumber
16
Saya datang setelah pertempuran tampaknya .. dapat unsigned char*digunakan jauh char*sebagai gantinya? Saya cenderung menggunakan unsigned chardaripada charsebagai tipe yang mendasari bytekarena byte saya tidak ditandatangani dan saya tidak ingin keanehan perilaku yang ditandatangani (terutama wrt to overflow)
Matthieu M.
30
@Matthieu: Signedness tidak membuat perbedaan dengan aturan alias, jadi menggunakan unsigned char *tidak apa-apa.
Thomas Eding
22
Tidakkah perilaku yang tidak jelas untuk membaca dari anggota serikat berbeda dari yang terakhir ditulis?
R. Martinho Fernandes
23
Bollocks, jawaban ini sepenuhnya mundur . Contoh yang ditampilkan sebagai ilegal sebenarnya legal, dan contoh yang ditunjukkan sebagai legal sebenarnya ilegal.
R. Martinho Fernandes
7
Deklarasi buffer 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 mallocpanggilan 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 ...
nonsensickle
233

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 intdan kemudian Anda mengarahkan float*ke memori itu dan menggunakannya sebagai floatAnda 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.

Niall
sumber
6
Jadi apa cara kanonik untuk secara legal menggunakan memori yang sama dengan variabel dari 2 tipe yang berbeda? atau apakah semua orang hanya menyalin?
jiggunjer
4
Halaman Mike Acton salah. Bagian dari "Casting through a union (2)", setidaknya, benar-benar salah; kode yang menurutnya legal tidak.
davmac
11
@ Davmac: Para penulis C89 tidak pernah bermaksud bahwa itu harus memaksa programmer untuk melompat melalui lingkaran Saya menemukan gagasan yang benar-benar aneh bahwa aturan yang ada untuk tujuan tunggal optimasi harus ditafsirkan sedemikian rupa sehingga mengharuskan programmer untuk menulis kode yang secara berlebihan menyalin data dengan harapan bahwa pengoptimal akan menghapus kode yang berlebihan.
supercat
1
@curiousguy: "Can't have unions"? Pertama, tujuan awal / utama dari serikat pekerja sama sekali tidak terkait dengan aliasing. Kedua, spesifikasi bahasa modern secara eksplisit mengizinkan penggunaan serikat untuk aliasing. Penyusun diharuskan memperhatikan bahwa serikat pekerja digunakan dan memperlakukan situasi adalah cara khusus.
AnT
5
@curiousguy: Salah. Pertama, ide konseptual asli di balik serikat adalah bahwa setiap saat hanya ada satu objek anggota "aktif" di objek serikat yang diberikan, sementara yang lain sama sekali tidak ada. Jadi, tidak ada "objek berbeda di alamat yang sama" seperti yang Anda yakini. Kedua, alias pelanggaran yang dibicarakan semua orang adalah tentang mengakses satu objek sebagai objek yang berbeda, bukan hanya memiliki dua objek dengan alamat yang sama. Selama tidak ada akses jenis-hukuman , tidak ada masalah. Itu adalah ide asli. Kemudian, hukuman jenis melalui serikat diizinkan.
AnT
133

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 charatau unsigned chartipe.

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 charatau unsigned chartipe.

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.

Ben Voigt
sumber
7
Ben, seperti orang sering diarahkan di sini, saya telah membiarkan diri saya menambahkan referensi ke standar C juga, demi kelengkapan.
Kos
1
Lihatlah C89 Rationale cs.technion.ac.il/users/yechiel/CS/C++draft/rationale.pdf bagian 3.3 yang membahasnya.
phorgan1
2
Jika seseorang memiliki nilai dari tipe struktur, mengambil alamat anggota, dan meneruskannya ke fungsi yang menggunakannya sebagai penunjuk ke tipe anggota, apakah itu akan dianggap sebagai mengakses objek dari tipe anggota (legal), atau objek dari jenis struktur (dilarang)? Sebuah banyak kode mengasumsikan itu hukum untuk struktur akses dalam mode tersebut, dan saya pikir banyak orang akan mengomel pada aturan yang dipahami sebagai melarang tindakan tersebut, tapi tidak jelas apa aturan yang pasti. Lebih lanjut, serikat dan struktur diperlakukan sama, tetapi aturan yang masuk akal untuk masing-masing harus berbeda.
supercat
2
@supercat: Cara aturan untuk struktur diucapkan, akses aktual selalu ke tipe primitif. Maka akses melalui referensi ke tipe primitif adalah legal karena jenisnya cocok, dan akses melalui referensi ke tipe struktur yang mengandung legal karena itu diizinkan secara khusus.
Ben Voigt
2
@ BenVoigt: Saya tidak berpikir urutan awal umum berfungsi kecuali jika akses dilakukan melalui serikat. Lihat goo.gl/HGOyoK untuk melihat apa yang dilakukan gcc. Jika mengakses lvalue dari tipe serikat melalui lvalue dari tipe anggota (tidak menggunakan operator akses-anggota-serikat) adalah legal, maka 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.
supercat
81

Catatan

Ini dikutip dari "Apa Aturan Ketegasan Mengasingkan Diri dan Mengapa Kita Peduli?"menulis

Apa itu alias ketat?

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.

Contoh pendahuluan

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 .

Sekarang, ke Buku Aturan

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.

Apa yang dikatakan standar C11?

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.

Apa yang dikatakan Standar Draf C ++ 17

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 .

Apa itu Tipe Punning

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.

Bagaimana cara kita Mengetik Pun dengan benar?

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 ).

C ++ 20 dan bit_cast

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 .

Menangkap Pelanggaran yang Mengasingkan Ketat

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.

Shafik Yaghmour
sumber
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Bhargav Rao
3
Jika saya bisa, +10, ditulis dengan baik dan dijelaskan, juga dari kedua sisi, penulis kompiler dan programmer ... satu-satunya kritik: Akan menyenangkan untuk memiliki contoh balasan di atas, untuk melihat apa yang dilarang oleh standar, itu tidak jelas jenis :-)
Gabriel
2
Jawaban yang sangat bagus Saya hanya menyesal bahwa contoh-contoh awal diberikan dalam C ++, yang membuatnya sulit untuk diikuti bagi orang-orang seperti saya yang hanya tahu atau peduli tentang C dan tidak tahu apa yang reinterpret_castmungkin dilakukan atau apa yang coutmungkin berarti. (Tidak apa-apa menyebutkan C ++ tetapi pertanyaan aslinya adalah tentang C dan IIUC contoh-contoh ini dapat ditulis dalam bahasa C.)
Gro-Tsen
Mengenai jenis puning: jadi jika saya menulis sebuah array dari beberapa tipe X ke dalam file, maka baca dari file itu array ini ke dalam memori yang ditunjukkan dengan void *, maka saya melemparkan pointer itu ke tipe data yang sebenarnya untuk menggunakannya - itu perilaku yang tidak terdefinisi?
Michael IV
44

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.

phorgan1
sumber
3
" Aliasing yang ketat tidak hanya merujuk ke pointer, itu juga mempengaruhi referensi " Sebenarnya, ini merujuk pada nilai . " Menggunakan memcpy adalah satu-satunya perbaikan portabel " Dengar!
curiousguy
5
Kertas bagus Saya ambil: (1) 'masalah-alias' ini adalah reaksi berlebihan terhadap pemrograman yang buruk - berusaha melindungi programmer yang buruk dari kebiasaan buruknya. Jika programmer memiliki kebiasaan yang baik maka aliasing ini hanya gangguan dan cek aman dapat dimatikan. (2) Optimalisasi sisi-kompiler hanya boleh dilakukan dalam kasus-kasus terkenal dan harus ketika ragu-ragu mengikuti kode-sumber secara ketat; memaksa programmer untuk menulis kode untuk memenuhi kekhasan kompiler adalah, sederhananya, salah. Lebih buruk lagi menjadikannya bagian dari standar.
slashmais
4
@ slashmais (1) " adalah reaksi berlebihan terhadap pemrograman yang buruk " Omong kosong. Itu adalah penolakan terhadap kebiasaan buruk. Kamu melakukan itu? Anda membayar harganya: tidak ada jaminan untuk Anda! (2) Kasus terkenal? Yang mana Aturan aliasing yang ketat harus "terkenal"!
curiousguy
5
@curiousguy: Setelah membersihkan beberapa titik kebingungan, jelas bahwa bahasa C dengan aturan aliasing membuat tidak mungkin bagi program untuk mengimplementasikan pool memori agnostik tipe-agnostik. Beberapa jenis program dapat bertahan dengan malloc / gratis, tetapi yang lain membutuhkan logika manajemen memori yang lebih baik disesuaikan dengan tugas-tugas yang dihadapi. Saya bertanya-tanya mengapa alasan C89 menggunakan contoh payah seperti alasan aturan aliasing, karena contoh mereka membuatnya tampak seperti aturan tidak akan menimbulkan kesulitan besar dalam melakukan tugas yang masuk akal.
supercat
5
@curiousguy, sebagian besar suite kompiler di luar sana termasuk -fstrict-aliasing sebagai default pada -O3 dan kontrak tersembunyi ini dipaksakan pada pengguna yang belum pernah mendengar tentang TBAA dan menulis kode seperti bagaimana programmer sistem mungkin. Saya tidak bermaksud terdengar tidak jujur ​​bagi pemrogram sistem, tetapi pengoptimalan seperti ini harus dibiarkan di luar pilihan bawaan -O3 dan harus menjadi pengoptimalan keikutsertaan bagi mereka yang tahu apa itu TBAA. Tidak menyenangkan melihat compiler 'bug' yang ternyata adalah kode pengguna yang melanggar TBAA, terutama melacak pelanggaran tingkat sumber dalam kode pengguna.
kchoi
34

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.

Ingo Blackman
sumber
jika Anda menambahkan pendek kedua * j untuk memeriksa () dan menggunakannya (* j = 7) maka optimasi menghilang karena ggc tidak tidak jika h dan j sebenarnya tidak menunjuk ke nilai yang sama. ya optimasi benar-benar pintar.
philippe lhardy
2
Untuk membuat hal-hal lebih menyenangkan, gunakan pointer ke tipe yang tidak kompatibel tetapi memiliki ukuran dan representasi yang sama (pada beberapa sistem yang benar misalnya eg 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.
supercat
Grr ... x64 adalah konvensi Microsoft. Gunakan amd64 atau x86_64 sebagai gantinya.
SS Anne
Grr ... x64 adalah konvensi Microsoft. Gunakan amd64 atau x86_64 sebagai gantinya.
SS Anne
17

Jenis punning via cast pointer (sebagai lawan menggunakan union) adalah contoh utama dari melanggar alias ketat.

Chris Jester-Young
sumber
1
Lihat jawaban saya di sini untuk kutipan yang relevan, terutama catatan kaki tetapi jenis hukuman melalui serikat selalu diizinkan di C meskipun kata-kata itu buruk pada awalnya. Anda saya ingin mengklarifikasi jawaban Anda.
Shafik Yaghmour
@ShafikYaghmour: C89 dengan jelas mengizinkan pelaksana untuk memilih kasus-kasus di mana mereka akan atau tidak akan mengenali jenis hukuman melalui serikat pekerja. Suatu implementasi dapat, misalnya, menetapkan bahwa untuk menulis ke satu jenis diikuti oleh pembacaan yang lain untuk diakui sebagai jenis hukuman, jika programmer melakukan salah satu dari berikut ini antara menulis dan membaca : (1) mengevaluasi nilai lv mengandung jenis serikat [mengambil alamat anggota akan memenuhi syarat, jika dilakukan pada titik yang tepat dalam urutan]; (2) mengkonversi pointer ke satu jenis menjadi pointer ke yang lain, dan akses melalui ptr itu.
supercat
@ShafikYaghmour: Suatu implementasi juga dapat menentukan misalnya bahwa jenis hukuman antara nilai integer dan floating-point hanya akan bekerja dengan andal jika kode mengeksekusi 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.
supercat
@ShafikYaghmour: Di bawah C89, implementasi dapat melarang sebagian besar bentuk hukuman jenis, termasuk melalui serikat pekerja, tetapi kesetaraan antara pointer ke serikat pekerja dan pointer ke anggota mereka menyiratkan bahwa hukuman jenis diizinkan dalam implementasi yang tidak secara tegas melarangnya.
supercat
17

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 xantara penugasan dan pernyataan kembali sehingga memungkinkan untuk kemungkinan yang pmenunjuk x, dan penugasan untuk *pdapat 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 intuntuk mengakses objek tipe struct S, dan inttidak 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_intkarena semua akses ke penyimpanan yang diakses melalui *pdilakukan dengan nilai tipe yang tinggi int, dan tidak ada konflik di dalam testkarena pterlihat berasal dari struct S, dan pada saat sdigunakan, semua akses ke penyimpanan yang akan dibuat melalui pakan 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 pdan akses ke s.xpada 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.

supercat
sumber
Yah, akan menarik untuk membaca proposal yang kurang lebih "apa yang bisa dilakukan oleh komite standar" yang mencapai tujuan mereka tanpa memperkenalkan kompleksitas sebanyak mungkin.
jrh
1
@ jrh: Saya pikir itu akan sangat sederhana. Mengakui bahwa 1. Agar aliasing terjadi selama eksekusi fungsi atau loop tertentu, dua pointer atau nilai yang berbeda harus digunakan selama eksekusi untuk mengatasi penyimpanan yang sama dalam fashon yang bertentangan; 2. Mengakui bahwa dalam konteks di mana satu penunjuk atau nilai baru terlihat dari yang lain, akses ke yang kedua adalah akses ke yang pertama; 3. Mengakui bahwa aturan tersebut tidak dimaksudkan untuk diterapkan dalam kasus yang tidak benar-benar melibatkan alias.
supercat
1
Keadaan yang tepat di mana seorang kompiler mengenali nilai yang baru diturunkan mungkin merupakan masalah Kualitas-Implementasi, tetapi setiap kompiler yang layak-jauh harus dapat mengenali bentuk-bentuk yang sengaja diabaikan oleh gcc dan dentang.
supercat
11

Setelah membaca banyak jawaban, saya merasa perlu menambahkan sesuatu:

Aliasing yang ketat (yang akan saya jelaskan sedikit) adalah penting karena :

  1. Akses memori bisa mahal (berdasarkan kinerja), itulah sebabnya data dimanipulasi dalam register CPU sebelum ditulis kembali ke memori fisik.

  2. 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 restrictkata 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:

  1. memuat adan bdari memori.

  2. tambahkan ake b.

  3. simpan b dan muat ulang a .

    (simpan dari register CPU ke memori dan muat dari memori ke register CPU).

  4. tambahkan bke a.

  5. 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 adan bmenunjuk 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).

  1. Ini dapat diceritakan ke kompiler dengan dua cara, dengan menggunakan tipe yang berbeda untuk menunjuk. yaitu:

    void merge_two_numbers(int *a, long *b) {...}
  2. Menggunakan restrictkata 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 restrictkata kunci, seluruh fungsi dapat dioptimalkan untuk:

  1. memuat adan bdari memori.

  2. tambahkan ake b.

  3. simpan hasil untuk adan untuk b.

Optimalisasi ini tidak dapat dilakukan sebelumnya, karena kemungkinan tabrakan (di mana adan bakan menjadi tiga kali lipat daripada dua kali lipat).

Mister
sumber
dengan membatasi kata kunci, pada langkah 3, bukankah seharusnya menyimpan hasil hanya 'b'? Kedengarannya seolah-olah hasil penjumlahan akan disimpan dalam 'a' juga. Apakah itu perlu dimuat lagi?
NeilB
1
@ NeilB - Ya Anda benar. Kami hanya menyimpan b(tidak memuat ulang) dan memuat ulang a. Saya harap ini lebih jelas sekarang.
Myst
Aliasing berbasis tipe mungkin telah menawarkan beberapa manfaat sebelumnya restrict, tetapi saya akan berpikir bahwa yang terakhir akan dalam kebanyakan keadaan lebih efektif, dan melonggarkan beberapa kendala registerakan memungkinkannya untuk mengisi beberapa kasus di mana restricttidak 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 .
supercat
Perhatikan bahwa walaupun memuat dari RAM utama sangat lambat (dan dapat menunda inti CPU untuk waktu yang lama jika mengikuti operasi tergantung pada hasilnya), memuat dari L1 cache cukup cepat, dan begitu juga menulis ke baris cache yang baru saja menulis oleh inti yang sama. Jadi semua kecuali yang pertama membaca atau menulis ke alamat biasanya akan cukup cepat: perbedaan antara akses add / reg lebih kecil daripada perbedaan antara addr cache / tidak di-cache.
curiousguy
@curiousguy - meskipun Anda benar, "cepat" dalam hal ini adalah relatif. Cache L1 mungkin masih urutan besarnya lebih lambat dari register CPU (saya pikir lebih dari 10 kali lebih lambat). Selain itu, restrictkata kunci meminimalkan tidak hanya kecepatan operasi tetapi jumlah mereka juga, yang bisa bermakna ... Maksudku, bagaimanapun juga, operasi tercepat adalah tidak ada operasi sama sekali :)
Myst
6

Aliasing yang ketat tidak memungkinkan tipe pointer yang berbeda untuk data yang sama.

Artikel ini akan membantu Anda memahami masalah ini secara terperinci.

Jason Dagit
sumber
4
Anda dapat alias antara referensi dan antara referensi dan pointer juga. Lihat tutorial saya dbp-consulting.com/tutorials/StrictAliasing.html
phorgan1
4
Diijinkan untuk memiliki tipe pointer yang berbeda untuk data yang sama. Di mana alias ketat masuk adalah ketika lokasi memori yang sama ditulis melalui satu jenis pointer dan membaca yang lain. Juga, beberapa jenis yang berbeda diizinkan (misalnya intdan struct yang berisi a int).
MM
-3

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.

curiousguy
sumber
4
Standar C menggunakan istilah "objek" untuk merujuk ke sejumlah konsep yang berbeda. Di antara mereka, urutan byte yang secara eksklusif dialokasikan untuk beberapa tujuan, referensi yang tidak harus eksklusif ke urutan byte ke / dari mana nilai tipe tertentu dapat ditulis atau dibaca, atau referensi seperti itu yang sebenarnya memiliki telah atau akan diakses dalam beberapa konteks. Saya tidak berpikir ada cara yang masuk akal untuk mendefinisikan istilah "Objek" yang akan konsisten dengan semua cara Standar menggunakannya.
supercat
@supercat Salah. Terlepas dari imajinasi Anda, itu sebenarnya cukup konsisten. Dalam ISO C itu didefinisikan sebagai "wilayah penyimpanan data di lingkungan eksekusi, konten yang dapat mewakili nilai". Dalam ISO C ++ ada definisi yang mirip. Komentar Anda bahkan lebih tidak relevan daripada jawaban karena semua yang Anda sebutkan adalah cara representasi untuk merujuk konten objek , sedangkan jawabannya mengilustrasikan konsep C ++ (glvalue) dari semacam ekspresi yang terkait erat dengan identitas objek. Dan semua aturan aliasing pada dasarnya relevan dengan identitas tetapi bukan kontennya.
FrankHB
1
@ FrankHB: Jika ada yang menyatakan 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 foomengubah 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 chardapat diakses menggunakan nilai ltipe int?
supercat
@ FrankhB: Dengan tidak adanya 6.5p7, orang bisa dengan mudah mengatakan bahwa setiap wilayah penyimpanan secara bersamaan berisi semua objek dari setiap jenis yang dapat ditampung di wilayah penyimpanan itu, dan bahwa mengakses wilayah penyimpanan itu secara bersamaan mengakses semuanya. Menafsirkan dengan cara seperti itu penggunaan istilah "objek" di 6.5p7, akan tetapi, melarang melakukan banyak hal dengan nilai-nilai non-karakter, yang jelas akan menjadi hasil yang absurd dan benar-benar mengalahkan tujuan aturan. Selanjutnya, konsep "objek" yang digunakan di mana saja selain 6.5p6 memiliki tipe waktu kompilasi statis, tetapi ...
supercat
1
sizeof (int) adalah 4, apakah deklarasi 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 volatilepointer yang memenuhi syarat untuk mengakses register perangkat keras yang tidak memenuhi definisi "objek".
supercat