(A + B + C) ≠ (A + C + B) dan penyusunan ulang compiler


108

Menambahkan dua bilangan bulat 32-bit dapat menghasilkan luapan bilangan bulat:

uint64_t u64_z = u32_x + u32_y;

Kelebihan ini dapat dihindari jika salah satu dari bilangan bulat 32-bit pertama kali dicor atau ditambahkan ke bilangan bulat 64-bit.

uint64_t u64_z = u32_x + u64_a + u32_y;

Namun, jika kompilator memutuskan untuk menyusun ulang penambahan:

uint64_t u64_z = u32_x + u32_y + u64_a;

overflow integer mungkin masih terjadi.

Apakah kompiler diperbolehkan melakukan pengubahan urutan seperti itu atau dapatkah kita mempercayai mereka untuk melihat hasil yang tidak konsisten dan menjaga urutan ekspresi sebagaimana adanya?


15
Anda tidak benar-benar memperlihatkan bilangan bulat overflow karena Anda tampak seperti uint32_tnilai tambah - yang tidak meluap, mereka membungkus. Ini bukanlah perilaku yang berbeda.
Martin Bonner mendukung Monica

5
Lihat bagian 1.9 dari standar c ++, ini langsung menjawab pertanyaan Anda (bahkan ada contoh yang hampir persis sama dengan milik Anda).
Holt

3
@Tal: Seperti yang telah dinyatakan orang lain: tidak ada luapan bilangan bulat. Unsigned didefinisikan untuk membungkus, untuk ditandatangani itu adalah perilaku yang tidak ditentukan, jadi implementasi apa pun akan dilakukan, termasuk daemon nasal.
terlalu jujur ​​untuk situs ini

5
@Tal: Omong kosong! Seperti yang sudah saya tulis: standarnya sangat jelas dan membutuhkan pembungkus, tidak jenuh (itu mungkin dengan ditandatangani, karena itu adalah standar UB.
terlalu jujur ​​untuk situs ini

15
@rustyx: Apakah Anda menyebutnya membungkus atau meluap, intinya tetap yang ((uint32_t)-1 + (uint32_t)1) + (uint64_t)0menghasilkan 0, sedangkan (uint32_t)-1 + ((uint32_t)1 + (uint64_t)0)hasilnya 0x100000000, dan kedua nilai ini tidak sama. Jadi penting apakah kompilator dapat menerapkan transformasi itu atau tidak. Tapi ya, standarnya hanya menggunakan kata "overflow" untuk integer bertanda tangan, bukan unsigned.
Steve Jessop

Jawaban:


84

Jika pengoptimal melakukan penataan ulang seperti itu, pengoptimalan tersebut masih terikat dengan spesifikasi C, sehingga pengubahan urutan tersebut akan menjadi:

uint64_t u64_z = (uint64_t)u32_x + (uint64_t)u32_y + u64_a;

Alasan:

Kami mulai dengan

uint64_t u64_z = u32_x + u64_a + u32_y;

Penambahan dilakukan dari kiri ke kanan.

Aturan promosi bilangan bulat menyatakan bahwa dalam penambahan pertama pada ekspresi asli, u32_xdipromosikan menjadi uint64_t. Selain kedua, u32_yjuga akan dipromosikan menjadi uint64_t.

Jadi, agar sesuai dengan spesifikasi C, setiap pengoptimal harus mempromosikan u32_xdan u32_yke nilai 64 bit unsigned. Ini sama dengan menambahkan pemeran. (Pengoptimalan sebenarnya tidak dilakukan pada level C, tetapi saya menggunakan notasi C karena itu adalah notasi yang kami pahami.)


Bukankah itu asosiasi kiri (u32_x + u32_t) + u64_a?
Tak berguna

12
@Useless: Klas melakukan cast semuanya ke 64 bit. Sekarang urutannya tidak ada bedanya sama sekali. Kompilator tidak perlu mengikuti asosiatif, ia hanya harus menghasilkan hasil yang sama persis seperti jika ia melakukannya.
gnasher729

2
Tampaknya menyarankan bahwa kode OP akan dievaluasi seperti itu, yang tidak benar.
Tak berguna

@Klas - peduli untuk menjelaskan mengapa ini terjadi dan bagaimana tepatnya Anda sampai pada sampel kode Anda?
rustyx

1
@rustyx Itu memang butuh penjelasan. Terima kasih telah mendorong saya untuk menambahkannya.
Klas Lindbäck

28

Kompilator hanya diperbolehkan untuk menyusun ulang di bawah aturan seolah-olah . Artinya, jika pengurutan ulang akan selalu memberikan hasil yang sama dengan pengurutan yang ditentukan, maka pengurutan diperbolehkan. Jika tidak (seperti dalam contoh Anda), tidak.

Misalnya, diberikan ekspresi berikut

i32big1 - i32big2 + i32small

yang telah dibangun dengan hati-hati untuk mengurangi dua nilai yang diketahui besar tetapi serupa, dan hanya kemudian menambahkan nilai kecil lainnya (sehingga menghindari luapan), penyusun dapat memilih untuk menyusun ulang menjadi:

(i32small - i32big2) + i32big1

dan mengandalkan fakta bahwa platform target menggunakan aritmatika dua komplemen dengan pembungkus untuk mencegah masalah. (Pengubahan urutan seperti itu mungkin masuk akal jika kompiler ditekan untuk register, dan kebetulan sudah ada i32smalldi register).


Contoh OP menggunakan tipe unsigned. i32big1 - i32big2 + i32smallmenyiratkan tipe yang ditandatangani. Kekhawatiran tambahan mulai berlaku.
chux - Kembalikan Monica

@ Linux Benar-benar. Hal yang ingin saya sampaikan adalah bahwa walaupun saya tidak dapat menulis (i32small-i32big2) + i32big1, (karena dapat menyebabkan UB), compiler dapat mengatur ulang secara efektif karena compiler dapat yakin bahwa perilakunya akan benar.
Martin Bonner mendukung Monica

3
@chux: Masalah tambahan seperti UB tidak ikut bermain, karena kita berbicara tentang penyusunan ulang kompiler di bawah aturan seolah-olah. Kompiler tertentu mungkin memanfaatkan mengetahui perilaku overflow-nya sendiri.
MSalters

16

Ada aturan "seolah-olah" dalam C, C ++, dan Objective-C: Kompilator dapat melakukan apa saja selama tidak ada program yang sesuai yang dapat membedakannya.

Dalam bahasa ini, a + b + c didefinisikan sama dengan (a + b) + c. Jika Anda dapat membedakan antara ini dan misalnya a + (b + c) maka compiler tidak dapat mengubah urutannya. Jika Anda tidak dapat membedakannya, maka compiler bebas untuk mengubah urutannya, tetapi tidak apa-apa, karena Anda tidak dapat membedakannya.

Dalam contoh Anda, dengan b = 64 bit, a dan c 32 bit, kompilator akan diizinkan untuk mengevaluasi (b + a) + c atau bahkan (b + c) + a, karena Anda tidak dapat membedakannya, tetapi bukan (a + c) + b karena Anda dapat membedakannya.

Dengan kata lain, kompilator tidak diperbolehkan melakukan apa pun yang membuat kode Anda berperilaku berbeda dari yang seharusnya. Anda tidak diharuskan untuk menghasilkan kode yang menurut Anda akan dihasilkannya, atau yang menurut Anda harus dibuat, tetapi kode tersebut akan memberi Anda hasil yang seharusnya.


Tapi dengan peringatan besar; kompilator bebas untuk mengasumsikan tidak ada perilaku tidak terdefinisi (dalam hal ini melimpah). Ini mirip dengan bagaimana pemeriksaan overflow if (a + 1 < a)dapat dioptimalkan.
csiz

7
@csiz ... pada variabel bertanda tangan . Variabel unsigned memiliki semantik overflow yang terdefinisi dengan baik (wrap-around).
Gavin S. Yancey

7

Mengutip dari standar :

[Catatan: Operator dapat dikelompokkan kembali menurut aturan matematika biasa hanya jika operator benar-benar asosiatif atau komutatif.7 Sebagai contoh, dalam fragmen berikut int a, b;

/∗ ... ∗/
a = a + 32760 + b + 5;

pernyataan ekspresi berperilaku persis sama seperti

a = (((a + 32760) + b) + 5);

karena asosiatif dan prioritas dari operator tersebut. Jadi, hasil penjumlahan (a + 32760) selanjutnya ditambahkan ke b, dan hasil tersebut kemudian ditambahkan ke 5 yang menghasilkan nilai yang ditetapkan ke a. Pada mesin di mana luapan menghasilkan pengecualian dan di mana kisaran nilai yang direpresentasikan oleh int adalah [-32768, + 32767], implementasi tidak dapat menulis ulang ekspresi ini sebagai

a = ((a + b) + 32765);

karena jika nilai a dan b masing-masing adalah -32754 dan -15, jumlah a + b akan menghasilkan pengecualian sedangkan ekspresi aslinya tidak; ekspresi juga tidak dapat ditulis ulang sebagai

a = ((a + 32765) + b);

atau

a = (a + (b + 32765));

karena nilai untuk a dan b masing-masing mungkin adalah, 4 dan -8 atau -17 dan 12. Namun pada mesin di mana luapan tidak menghasilkan pengecualian dan di mana hasil luapan dapat dibalik, pernyataan ekspresi di atas dapat ditulis ulang oleh implementasi dengan salah satu cara di atas karena hasil yang sama akan terjadi. - catatan akhir]


4

Apakah kompiler diperbolehkan melakukan pengubahan urutan seperti itu atau dapatkah kita mempercayai mereka untuk melihat hasil yang tidak konsisten dan menjaga urutan ekspresi sebagaimana adanya?

Kompilator dapat menyusun ulang hanya jika memberikan hasil yang sama - di sini, seperti yang Anda amati, ternyata tidak.


Dimungkinkan untuk menulis templat fungsi, jika Anda menginginkannya, yang mempromosikan semua argumen std::common_typesebelum menambahkan - ini akan aman, dan tidak bergantung pada urutan argumen atau transmisi manual, tetapi itu cukup kikuk.


Saya tahu casting eksplisit harus digunakan, tetapi saya ingin mengetahui perilaku compiler ketika casting seperti itu dihilangkan secara keliru.
Tal

1
Seperti yang saya katakan, tanpa casting eksplisit: penambahan kiri dilakukan terlebih dahulu, tanpa promosi integral, dan karena itu tunduk pada pembungkus. The hasil penambahan itu, mungkin dibungkus, yang kemudian dipromosikan untuk uint64_tuntuk Selain paling kanan nilai.
Tak berguna

Penjelasan Anda tentang aturan seolah-olah benar-benar salah. Bahasa C misalnya menentukan operasi apa yang harus dilakukan pada mesin abstrak. Aturan "seolah-olah" memungkinkannya melakukan apa pun yang diinginkannya selama tidak ada yang bisa membedakannya.
gnasher729

Artinya, compiler dapat melakukan apapun yang diinginkannya selama hasilnya sama dengan yang ditentukan oleh aturan konversi asosiatif kiri dan aritmatika yang ditampilkan.
Tak berguna

1

Itu tergantung pada lebar bit unsigned/int.

2 di bawah ini tidak sama (ketika unsigned <= 32bit). u32_x + u32_ymenjadi 0.

u64_a = 0; u32_x = 1; u32_y = 0xFFFFFFFF;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + u32_y + u64_a;  // u32_x + u32_y carry does not add to sum.

Mereka sama (ketika unsigned >= 34bit). Promosi u32_x + u32_ybilangan bulat menyebabkan penambahan terjadi pada matematika 64-bit. Urutan tidak relevan.

Ini adalah UB (saat unsigned == 33bit). Promosi integer menyebabkan penambahan terjadi pada matematika 33-bit yang ditandatangani dan overflow yang ditandatangani adalah UB.

Apakah penyusun diperbolehkan melakukan penataan ulang seperti itu ...?

(32 bit matematika): ya Re-order, tapi hasil yang sama harus terjadi, sehingga tidak yang memesan ulang OP mengusulkan. Di bawah ini sama

// Same
u32_x + u64_a + u32_y;
u64_a + u32_x + u32_y;
u32_x + (uint64_t) u32_y + u64_a;
...

// Same as each other below, but not the same as the 3 above.
uint64_t u64_z = u32_x + u32_y + u64_a;
uint64_t u64_z = u64_a + (u32_x + u32_y);

... dapatkah kita mempercayai mereka untuk melihat hasil yang tidak konsisten dan menjaga urutan ekspresi sebagaimana adanya?

Percaya ya, tetapi tujuan pengkodean OP tidak begitu jelas. Haruskah u32_x + u32_ymembawa berkontribusi? Jika OP menginginkan kontribusi itu, kode harus

uint64_t u64_z = u64_a + u32_x + u32_y;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + (u32_y + u64_a);

Tapi tidak

uint64_t u64_z = u32_x + u32_y + u64_a;
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.