Operasi bitwise menghasilkan ukuran variabel yang tidak terduga

24

Konteks

Kami mem-porting kode C yang awalnya dikompilasi menggunakan kompiler C 8-bit untuk mikrokontroler PIC. Ungkapan umum yang digunakan untuk mencegah variabel global yang tidak ditandatangani (misalnya, penghitung kesalahan) berguling kembali ke nol adalah sebagai berikut:

if(~counter) counter++;

Operator bitwise di sini membalikkan semua bit dan pernyataan ini hanya benar jika counterkurang dari nilai maksimum. Yang penting, ini berfungsi terlepas dari ukuran variabel.

Masalah

Kami sekarang menargetkan prosesor ARM 32-bit menggunakan GCC. Kami memperhatikan bahwa kode yang sama menghasilkan hasil yang berbeda. Sejauh yang kami tahu, sepertinya operasi komplemen bitwise mengembalikan nilai dengan ukuran yang berbeda dari yang kami harapkan. Untuk mereproduksi ini, kami mengkompilasi, di GCC:

uint8_t i = 0;
int sz;

sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1

sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4

Pada baris pertama output, kita mendapatkan apa yang kita harapkan: iadalah 1 byte. Namun, komplemen bitwise isebenarnya empat byte yang menyebabkan masalah karena perbandingan dengan ini sekarang tidak akan memberikan hasil yang diharapkan. Misalnya, jika melakukan (di mana idiinisialisasi dengan benar uint8_t):

if(~i) i++;

Kita akan melihat i"membungkus" dari 0xFF kembali ke 0x00. Perilaku ini berbeda dalam GCC dibandingkan dengan ketika dulu berfungsi seperti yang kami maksudkan dalam kompilator sebelumnya dan mikrokontroler PIC 8-bit.

Kami menyadari bahwa kami dapat menyelesaikan ini dengan casting seperti:

if((uint8_t)~i) i++;

Atau, oleh

if(i < 0xFF) i++;

Namun dalam kedua penyelesaian ini, ukuran variabel harus diketahui dan rawan kesalahan untuk pengembang perangkat lunak. Pemeriksaan batas atas semacam ini terjadi di seluruh basis kode. Ada beberapa ukuran variabel (mis., uint16_tDan unsigned charlain - lain) dan mengubahnya dalam basis kode yang berfungsi bukan sesuatu yang kami harapkan.

Pertanyaan

Apakah pemahaman kita tentang masalah itu benar, dan apakah ada opsi yang tersedia untuk menyelesaikan ini yang tidak perlu mengunjungi kembali setiap kasus di mana kita telah menggunakan idiom ini? Apakah asumsi kami benar, bahwa operasi seperti pelengkap bitwise harus mengembalikan hasil yang ukurannya sama dengan operan? Sepertinya ini akan pecah, tergantung pada arsitektur prosesor. Saya merasa seperti saya minum pil gila dan C harus sedikit lebih portabel daripada ini. Sekali lagi, pemahaman kita tentang ini bisa salah.

Di permukaan ini mungkin tidak tampak seperti masalah besar tetapi idiom yang sebelumnya berfungsi ini digunakan di ratusan lokasi dan kami ingin memahami ini sebelum melanjutkan dengan perubahan mahal.


Catatan: Ada pertanyaan duplikat yang tampaknya serupa tetapi tidak tepat di sini: Operasi bitwise pada char memberikan hasil 32 bit

Saya tidak melihat inti sebenarnya dari masalah yang dibahas di sana, yaitu, ukuran hasil komplemen bitwise menjadi berbeda dari apa yang diteruskan ke operator.

Charlie Garts
sumber
14
"Apakah asumsi kami benar, bahwa operasi seperti komplemen bitwise harus mengembalikan hasil yang ukurannya sama dengan operan?" Tidak, ini tidak benar, berlaku promosi integer.
Thomas Jager
2
Meskipun pasti relevan, saya tidak yakin itu adalah duplikat dari pertanyaan khusus ini, karena mereka tidak memberikan solusi untuk masalah tersebut.
Cody Gray
3
Saya merasa seperti saya minum pil gila dan C harus sedikit lebih portabel daripada ini. Jika Anda tidak mendapatkan promosi integer pada tipe 8-bit, maka kompiler Anda tidak kompatibel dengan standar C. Dalam hal ini saya pikir Anda harus melalui semua perhitungan untuk memeriksanya dan memperbaikinya jika perlu.
user694733
1
Apakah saya satu-satunya yang bertanya-tanya logika apa, selain dari penghitung yang benar-benar tidak penting, dapat membawanya ke "kenaikan jika ada cukup ruang, lupakan saja"? Jika Anda memasukkan kode porting, dapatkah Anda menggunakan int (4 byte) alih-alih uint_8? Itu akan mencegah masalah Anda dalam banyak kasus.
Keping
1
@puck Anda benar, kami bisa mengubahnya menjadi 4 byte, tetapi itu akan merusak kompatibilitas ketika berkomunikasi dengan sistem yang ada. Maksud adalah untuk mengetahui bila ada setiap kesalahan dan counter 1-byte awalnya cukup, dan sisa-sisa begitu.
Charlie Garts

Jawaban:

26

Apa yang Anda lihat adalah hasil dari promosi bilangan bulat . Dalam kebanyakan kasus di mana nilai integer digunakan dalam ekspresi, jika jenis nilai lebih kecil dari intnilai yang dipromosikan int. Ini didokumentasikan dalam bagian 6.3.1.1p2 dari standar C :

Berikut ini dapat digunakan dalam ekspresi di mana pun intatau unsigned intmungkin digunakan

  • Objek atau ekspresi dengan tipe integer (selain intatau unsigned int) yang peringkat konversi integernya kurang dari atau sama dengan peringkat dari intdan unsigned int.
  • Tipe bit-field _Bool, int ,int ditandatangani int , orunsigned int`.

Jika a intdapat mewakili semua nilai dari tipe asli (seperti dibatasi oleh lebar, untuk bidang-bit), nilai tersebut dikonversi menjadi int; jika tidak, itu dikonversi menjadi unsigned int. Ini disebut promosi bilangan bulat . Semua tipe lainnya tidak berubah oleh promosi bilangan bulat.

Jadi, jika suatu variabel memiliki tipe uint8_tdan nilai 255, menggunakan operator apa pun selain pemeran atau penugasan di atasnya terlebih dahulu akan mengubahnya untuk mengetik intdengan nilai 255 sebelum melakukan operasi. Inilah sebabnya mengapa sizeof(~i)memberi Anda 4 bukannya 1.

Bagian 6.5.3.3 menjelaskan bahwa promosi bilangan bulat berlaku untuk ~operator:

Hasil dari ~operator adalah komplemen bitwise dari operandnya (dipromosikan) (yaitu, setiap bit dalam hasil diatur jika dan hanya jika bit yang sesuai dalam operan yang dikonversi tidak disetel). Promosi integer dilakukan pada operan, dan hasilnya memiliki tipe yang dipromosikan. Jika tipe yang dipromosikan adalah tipe yang tidak ditandatangani, ekspresi ~Etersebut setara dengan nilai maksimum yang dapat diwakili dalam tipe tersebut dikurangi E.

Jadi dengan asumsi 32 bit int, jika countermemiliki nilai 8 bit 0xffitu dikonversi ke nilai 32 bit 0x000000ff, dan menerapkannya ~memberi Anda 0xffffff00.

Mungkin cara paling sederhana untuk menangani ini adalah tanpa harus tahu jenisnya adalah untuk memeriksa apakah nilainya 0 setelah bertambah, dan jika demikian kurangi.

if (!++counter) counter--;

Sampul bilangan bulat tak bertanda bekerja di kedua arah, sehingga penurunan nilai 0 memberi Anda nilai positif terbesar.

dbush
sumber
1
if (!++counter) --counter;mungkin kurang aneh untuk beberapa programmer daripada menggunakan operator koma.
Eric Postpischil
1
Alternatif lain adalah ++counter; counter -= !counter;.
Eric Postpischil
@EricPostpischil Sebenarnya, saya lebih suka opsi pertama Anda. Diedit.
dbush
15
Ini jelek dan tidak dapat dibaca, tidak peduli bagaimana Anda menulisnya. Jika Anda harus menggunakan idiom seperti ini, bantulah setiap programmer pemeliharaan dan membungkusnya sebagai fungsi sebaris : sesuatu seperti increment_unsigned_without_wraparoundatau increment_with_saturation. Secara pribadi, saya akan menggunakan clampfungsi tri-operan generik .
Cody Gray
5
Selain itu, Anda tidak dapat menjadikan ini fungsi, karena harus berperilaku berbeda untuk tipe argumen yang berbeda. Anda harus menggunakan makro tipe-generik .
user2357112 mendukung Monica
7

dalam sizeof (i); Anda meminta ukuran variabel i , jadi 1

dalam sizeof (~ i); Anda meminta ukuran jenis ekspresi, yang merupakan int , dalam kasus Anda 4


Menggunakan

jika (~ i)

untuk mengetahui apakah saya tidak menghargai 255 (dalam kasus Anda dengan uint8_t) tidak terlalu mudah dibaca, lakukan saja

if (i != 255)

dan Anda akan memiliki kode portabel dan mudah dibaca


Ada beberapa ukuran variabel (mis., Uint16_t dan unsigned char dll.)

Untuk mengelola ukuran yang tidak ditandatangani:

if (i != (((uintmax_t) 2 << (sizeof(i)*CHAR_BIT-1)) - 1))

Ungkapannya konstan, jadi dihitung pada waktu kompilasi.

#include <Limit.h> untuk CHAR_BIT dan #include <stdint.h> untuk uintmax_t

bruno
sumber
3
Pertanyaannya secara eksplisit menyatakan bahwa mereka memiliki beberapa ukuran yang harus dihadapi, jadi != 255tidak memadai.
Eric Postpischil
@EricPostpischil ah ya, saya lupa itu, jadi "if (i! = ((1u << sizeof (i) * 8) - 1))" seandainya selalu unsigned?
Bruno
1
Itu akan ditentukan untuk unsignedobjek karena pergeseran lebar objek penuh tidak ditentukan oleh standar C, tetapi dapat diperbaiki dengan (2u << sizeof(i)*CHAR_BIT-1) - 1.
Eric Postpischil
oh ya tentu, CHAR_BIT, salah saya
bruno
2
Untuk keamanan dengan tipe yang lebih luas, dapat digunakan ((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1.
Eric Postpischil
5

Berikut adalah beberapa opsi untuk menerapkan "Tambah 1 ke xtetapi jepit pada nilai representable maksimum," mengingat bahwa xada beberapa jenis integer yang tidak ditandatangani:

  1. Tambahkan satu jika dan hanya jika xkurang dari nilai maksimum yang diwakili dalam jenisnya:

    x += x < Maximum(x);

    Lihat item berikut untuk definisi Maximum. Metode ini berpeluang besar untuk dioptimalkan oleh kompiler ke instruksi yang efisien seperti perbandingan, beberapa bentuk set atau pemindahan kondisional, dan tambahan.

  2. Bandingkan dengan nilai terbesar dari jenis:

    if (x < ((uintmax_t) 2u << sizeof x * CHAR_BIT - 1) - 1) ++x

    (Ini menghitung 2 N , di mana N adalah jumlah bit x, dengan menggeser 2 oleh N −1 bit. Kami melakukan ini alih-alih menggeser 1 N bit karena pergeseran oleh jumlah bit dalam jenis tidak ditentukan oleh C standar. CHAR_BITMakro mungkin asing bagi beberapa; itu adalah jumlah bit dalam satu byte, demikian sizeof x * CHAR_BITjuga jumlah bit dalam jenis x.)

    Ini dapat dibungkus dalam makro yang diinginkan untuk estetika dan kejelasan:

    #define Maximum(x) (((uintmax_t) 2u << sizeof (x) * CHAR_BIT - 1) - 1)
    if (x < Maximum(x)) ++x;
  3. Bertambah xdan perbaiki jika membungkus ke nol, menggunakan if:

    if (!++x) --x; // !++x is true if ++x wraps to zero.
  4. Bertambah xdan perbaiki jika membungkus ke nol, menggunakan ekspresi:

    ++x; x -= !x;

    Ini adalah tanpa cabang nominal (kadang-kadang bermanfaat untuk kinerja), tetapi kompiler dapat mengimplementasikannya sama seperti di atas, menggunakan cabang jika diperlukan tetapi mungkin dengan instruksi tanpa syarat jika arsitektur target memiliki instruksi yang sesuai.

  5. Opsi tanpa cabang, menggunakan makro di atas, adalah:

    x += 1 - x/Maximum(x);

    Jika xmaksimum jenisnya, ini dievaluasi menjadi x += 1-1. Kalau tidak demikian x += 1-0. Namun, pembagian agak lambat pada banyak arsitektur. Kompiler dapat mengoptimalkan ini untuk instruksi tanpa divisi, tergantung pada kompiler dan arsitektur target.

Eric Postpischil
sumber
1
Saya tidak bisa mengubah jawaban yang merekomendasikan penggunaan makro. C memiliki fungsi sebaris. Anda tidak melakukan apa pun di dalam definisi makro yang tidak dapat dengan mudah dilakukan di dalam fungsi inline. Dan jika Anda akan menggunakan makro, pastikan Anda secara strategis mengurung untuk kejelasan: operator << memiliki prioritas sangat rendah. Dentang memperingatkan tentang ini dengan -Wshift-op-parentheses. Berita baiknya adalah, kompiler pengoptimal tidak akan menghasilkan divisi di sini, jadi Anda tidak perlu khawatir itu lambat.
Cody Gray
1
@CodyGray, jika Anda pikir Anda bisa melakukan ini dengan suatu fungsi, tulis jawaban.
Carsten S
2
@CodyGray: sizeof xtidak dapat diimplementasikan di dalam fungsi C karena xharus menjadi parameter (atau ekspresi lainnya) dengan beberapa tipe tetap. Itu tidak dapat menghasilkan ukuran dari jenis argumen apa pun yang digunakan penelepon. Makro bisa.
Eric Postpischil
2

Sebelum stdint.h ukuran variabel dapat bervariasi dari kompiler ke kompiler dan tipe variabel aktual di C masih int, panjang, dll dan masih ditentukan oleh pembuat kompiler untuk ukurannya. Bukan asumsi standar atau target spesifik. Penulis kemudian perlu membuat stdint.h untuk memetakan dua dunia, yaitu tujuan stdint.h untuk memetakan uint_his yang untuk int, panjang, pendek.

Jika Anda porting kode dari kompiler lain dan menggunakan char, pendek, int, lama maka Anda harus melalui setiap jenis dan melakukan port sendiri, tidak ada jalan lain. Dan baik Anda berakhir dengan ukuran yang tepat untuk variabel, deklarasi berubah tetapi kode sebagai karya tertulis ....

if(~counter) counter++;

atau ... menyediakan topeng atau tipografi secara langsung

if((~counter)&0xFF) counter++;
if((uint_8)(~counter)) counter++;

Pada akhirnya jika Anda ingin kode ini berfungsi, Anda harus mem-port-nya ke platform baru. Pilihan Anda bagaimana caranya. Ya, Anda harus menghabiskan waktu untuk setiap kasus dan melakukannya dengan benar, jika tidak, Anda akan terus kembali ke kode ini yang bahkan lebih mahal.

Jika Anda mengisolasi tipe variabel pada kode sebelum porting dan berapa ukuran tipe variabel, maka mengisolasi variabel yang melakukan ini (harus mudah dipahami) dan mengubah deklarasi mereka menggunakan definisi stdint.h yang diharapkan tidak akan berubah di masa depan, dan Anda akan terkejut tetapi header yang salah digunakan kadang-kadang bahkan memasukkan cek sehingga Anda bisa tidur lebih nyenyak di malam hari

if(sizeof(uint_8)!=1) return(FAIL);

Dan sementara itu gaya pengkodean bekerja (if (~ counter) counter ++;), untuk portabilitas yang diinginkan sekarang dan di masa depan yang terbaik adalah menggunakan masker untuk secara khusus membatasi ukuran (dan tidak bergantung pada deklarasi), lakukan ini ketika kode ditulis di tempat pertama atau hanya menyelesaikan port dan kemudian Anda tidak perlu port ulang lagi hari lain. Atau untuk membuat kode lebih mudah dibaca maka lakukan jika x <0xFF lalu atau x! = 0xFF atau sesuatu seperti itu maka kompiler dapat mengoptimalkannya ke dalam kode yang sama dengan solusi-solusi ini, hanya membuatnya lebih mudah dibaca dan kurang berisiko. ...

Bergantung pada seberapa penting produk tersebut atau berapa kali Anda ingin mengirimkan tambalan / pembaruan atau menggulung truk atau berjalan ke laboratorium untuk memperbaiki masalah apakah Anda mencoba menemukan solusi cepat atau hanya menyentuh garis kode yang terpengaruh. jika hanya seratus atau sedikit yang tidak sebesar pelabuhan.

old_timer
sumber
0
6.5.3.3 Operator aritmatika unary
...
4 Hasil dari ~operator adalah komplemen bitwise dari operandnya (yang dipromosikan) (yaitu, setiap bit dalam hasilnya diatur jika dan hanya jika bit yang sesuai dalam operan yang dikonversi tidak disetel ). Promosi integer dilakukan pada operan, dan hasilnya memiliki tipe yang dipromosikan . Jika tipe yang dipromosikan adalah tipe yang tidak ditandatangani, ekspresi ~Etersebut setara dengan nilai maksimum yang dapat diwakili dalam tipe tersebut dikurangi E.

C 2011 Draf Online

Masalahnya adalah bahwa operan ~sedang dipromosikan intsebelum operator diterapkan.

Sayangnya, saya tidak berpikir ada jalan keluar yang mudah untuk ini. Penulisan

if ( counter + 1 ) counter++;

tidak akan membantu karena promosi juga berlaku di sana. Satu-satunya hal yang dapat saya sarankan adalah menciptakan beberapa konstanta simbolis untuk maksimum nilai yang Anda inginkan yang objek untuk mewakili dan pengujian terhadap bahwa:

#define MAX_COUNTER 255
...
if ( counter < MAX_COUNTER-1 ) counter++;
John Bode
sumber
Saya menghargai poin tentang promosi integer - sepertinya ini adalah masalah yang kita hadapi. Layak menunjukkan, bagaimanapun, adalah bahwa dalam sampel kode kedua Anda, -1tidak diperlukan, karena ini akan menyebabkan penghitung untuk menetap di 254 (0xFE). Bagaimanapun, pendekatan ini, seperti yang disebutkan dalam pertanyaan saya, tidak ideal karena ukuran variabel yang berbeda dalam basis kode yang berpartisipasi dalam idiom ini.
Charlie Garts