Pernah mencoba menjumlahkan semua angka dari 1 hingga 2.000.000 dalam bahasa pemrograman favorit Anda? Hasilnya mudah dihitung secara manual: 2.000.001.000.000, yang sekitar 900 kali lebih besar dari nilai maksimum integer 32bit yang tidak ditandatangani.
C # print out -1453759936
- nilai negatif! Dan saya kira Java melakukan hal yang sama.
Itu berarti ada beberapa bahasa pemrograman umum yang mengabaikan Arithmetic Overflow secara default (dalam C #, ada opsi tersembunyi untuk mengubah itu). Itu perilaku yang terlihat sangat berisiko bagi saya, dan bukankah tabrakan Ariane 5 disebabkan oleh luapan seperti itu?
Jadi: apa keputusan desain di balik perilaku berbahaya seperti itu?
Sunting:
Jawaban pertama untuk pertanyaan ini mengungkapkan biaya pemeriksaan yang berlebihan. Mari kita jalankan program C # singkat untuk menguji asumsi ini:
Stopwatch watch = Stopwatch.StartNew();
checked
{
for (int i = 0; i < 200000; i++)
{
int sum = 0;
for (int j = 1; j < 50000; j++)
{
sum += j;
}
}
}
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalMilliseconds);
Di komputer saya, versi yang diperiksa membutuhkan 11015 ms, sedangkan versi yang tidak diperiksa mengambil 4125 ms. Yaitu langkah-langkah pemeriksaan memakan waktu hampir dua kali lipat dari menambahkan angka (total 3 kali waktu asli). Tetapi dengan 10.000.000.000 pengulangan, waktu yang dibutuhkan oleh cek masih kurang dari 1 nanosecond. Mungkin ada situasi di mana itu penting, tetapi untuk sebagian besar aplikasi, itu tidak masalah.
Edit 2:
Saya mengkompilasi ulang aplikasi server kami (layanan Windows yang menganalisis data yang diterima dari beberapa sensor, melibatkan sejumlah angka) dengan /p:CheckForOverflowUnderflow="false"
parameter (biasanya, saya mengaktifkan pemeriksaan overflow) dan menggunakannya pada perangkat. Pemantauan nagios menunjukkan bahwa rata-rata beban CPU tetap pada 17%.
Ini berarti bahwa hit kinerja yang ditemukan dalam contoh buatan di atas sama sekali tidak relevan untuk aplikasi kita.
sumber
checked { }
bagian untuk menandai bagian-bagian dari kode yang harus melakukan pemeriksaan Arithmetic Overflow. Ini karena kinerja(1..2_000_000).sum #=> 2000001000000
. Lain salah satu bahasa favorit saya:sum [1 .. 2000000] --=> 2000001000000
. Tidak favorit saya:Array.from({length: 2000001}, (v, k) => k).reduce((acc, el) => acc + el) //=> 2000001000000
. (Agar adil, yang terakhir adalah curang.)Integer
di Haskell adalah presisi sewenang-wenang, itu akan menampung nomor berapa pun selama Anda tidak kehabisan RAM yang dapat dialokasikan.But with the 10,000,000,000 repetitions, the time taken by a check is still less than 1 nanosecond.
itu indikasi loop sedang dioptimalkan. Kalimat itu juga bertentangan dengan angka-angka sebelumnya yang tampak sangat valid bagi saya.Jawaban:
Ada 3 alasan untuk ini:
Biaya untuk memeriksa luapan (untuk setiap operasi aritmatika tunggal) pada saat run-time berlebihan.
Kompleksitas membuktikan bahwa pemeriksaan luapan dapat dihilangkan pada waktu kompilasi adalah berlebihan.
Dalam beberapa kasus (misalnya perhitungan CRC, pustaka angka besar, dll) "wrap on overflow" lebih nyaman bagi programmer.
sumber
unsigned int
tidak boleh terlintas dalam pikiran karena bahasa dengan pemeriksaan overflow harus memeriksa semua jenis integer secara default. Anda harus menuliswrapping unsigned int
.didOverflow()
Fungsi inline sederhana atau bahkan variabel global__carry
yang memungkinkan akses ke flag carry akan dikenakan biaya nol waktu CPU jika Anda tidak menggunakannya.ADD
tidak mengatur carry (Anda perluADDS
). Itanium bahkan tidak memiliki bendera pembawa. Dan bahkan pada x86, AVX tidak memiliki flag.unchecked
itu cukup mudah; tetapi Anda mungkin melebih-lebihkan seberapa sering masalah meluap.adds
sama denganadd
(itu hanya instruksi 1-bit flag yang memilih apakah flag carry diperbarui).add
Instruksi MIPS menjebak overflow - Anda harus meminta untuk tidak menjebak overflow dengan menggunakanaddu
gantinya!Siapa bilang itu pengorbanan yang buruk ?!
Saya menjalankan semua aplikasi produksi saya dengan pemeriksaan overflow diaktifkan. Ini adalah opsi kompiler C #. Saya benar-benar membandingkan ini dan saya tidak dapat menentukan perbedaannya. Biaya mengakses basis data untuk menghasilkan (bukan mainan) HTML membayangi biaya pemeriksaan melimpah.
Saya menghargai kenyataan bahwa saya tahu tidak ada operasi yang meluap dalam produksi. Hampir semua kode akan berperilaku tidak menentu dengan adanya luapan. Bug tidak akan jinak. Korupsi data kemungkinan besar, masalah keamanan kemungkinan.
Jika saya membutuhkan kinerja, yang kadang-kadang terjadi, saya menonaktifkan pemeriksaan overflow menggunakan
unchecked {}
pada dasar granular. Ketika saya ingin mengatakan bahwa saya mengandalkan operasi yang tidak meluap, saya mungkin secara berlebihan menambahkanchecked {}
kode untuk mendokumentasikan fakta itu. Saya sadar akan luapan tetapi saya tidak perlu berterima kasih pada pemeriksaan.Saya percaya tim C # membuat pilihan yang salah ketika mereka memilih untuk tidak memeriksa overflow secara default, tetapi pilihan itu sekarang disegel karena masalah kompatibilitas yang kuat. Perhatikan, bahwa pilihan ini dibuat sekitar tahun 2000. Perangkat keras kurang mampu dan .NET belum memiliki banyak daya tarik. Mungkin .NET ingin menarik programmer Java dan C / C ++ dengan cara ini. .NET juga dimaksudkan untuk bisa dekat dengan logam. Itu sebabnya ia memiliki kode yang tidak aman, struct dan kemampuan panggilan asli yang semuanya tidak dimiliki Java.
Semakin cepat perangkat keras kita dan kompiler yang lebih pintar mendapatkan pengecekan overflow yang lebih menarik secara default.
Saya juga percaya bahwa pengecekan overflow seringkali lebih baik daripada angka dengan ukuran tak terbatas. Angka yang tak terhingga memiliki biaya kinerja yang bahkan lebih tinggi, lebih sulit untuk dioptimalkan (saya percaya) dan mereka membuka kemungkinan konsumsi sumber daya tanpa batas.
Cara JavaScript menangani overflow bahkan lebih buruk. Nomor JavaScript adalah ganda floating point. "Overflow" memanifestasikan dirinya sebagai meninggalkan set integer yang sepenuhnya tepat. Hasil yang sedikit salah akan terjadi (seperti dimatikan oleh satu - ini dapat mengubah loop terbatas menjadi yang tak terbatas).
Untuk beberapa bahasa seperti C / C ++ pengecekan overflow secara default jelas tidak pantas karena jenis aplikasi yang sedang ditulis dalam bahasa-bahasa ini memerlukan kinerja bare metal. Namun, ada upaya untuk membuat C / C ++ menjadi bahasa yang lebih aman dengan memungkinkan untuk ikut serta dalam mode yang lebih aman. Ini patut dipuji karena 90-99% kode cenderung dingin. Contohnya adalah
fwrapv
opsi kompiler yang memaksa pembungkus komplemen 2's. Ini adalah fitur "kualitas implementasi" oleh kompiler, bukan oleh bahasa.Haskell tidak memiliki tumpukan panggilan logis dan tidak ada urutan evaluasi yang ditentukan. Ini membuat pengecualian terjadi pada titik yang tidak dapat diprediksi. Dalam
a + b
hal ini tidak ditentukan apakaha
ataub
dievaluasi pertama dan apakah mereka ekspresi menghentikan sama sekali atau tidak. Oleh karena itu, masuk akal bagi Haskell untuk menggunakan bilangan bulat tak terikat sebagian besar waktu. Pilihan ini cocok untuk bahasa yang murni fungsional karena pengecualian benar-benar tidak sesuai di sebagian besar kode Haskell. Dan pembagian dengan nol memang merupakan titik bermasalah dalam desain bahasa Haskells. Alih-alih bilangan bulat tak berbatas, mereka bisa menggunakan bilangan bulat pembungkus dengan lebar tetap juga tapi itu tidak sesuai dengan tema "fokus pada kebenaran" yang fitur bahasa.Alternatif untuk pengecualian melimpah adalah nilai racun yang dibuat oleh operasi yang tidak ditentukan dan diperbanyak melalui operasi (seperti nilai float
NaN
). Itu tampaknya jauh lebih mahal daripada memeriksa melimpah dan membuat semua operasi lebih lambat, bukan hanya yang bisa gagal (kecuali akselerasi perangkat keras yang umumnya dimiliki dan ints tidak dimiliki - walaupun Itanium memiliki NaT yang "Bukan Masalah" ). Saya juga tidak begitu mengerti gunanya membuat program terus lemas bersama dengan data yang buruk. Itu sepertiON ERROR RESUME NEXT
. Itu menyembunyikan kesalahan tetapi tidak membantu mendapatkan hasil yang benar. supercat menunjukkan bahwa kadang-kadang optimasi kinerja untuk melakukan ini.sumber
unsigned
bilangan bulat saja. Perilaku dari integer overflow yang ditandatangani sebenarnya adalah perilaku yang tidak terdefinisi dalam C dan C ++. Ya, perilaku yang tidak terdefinisi . Kebetulan hampir semua orang mengimplementasikannya sebagai pelengkap komplemen 2's. C # benar-benar membuatnya resmi, daripada meninggalkannya di UB seperti C / C ++gcc -O2
untukx + 1 > x
(di manax
adalahint
). Juga lihat gcc.gnu.org/onlinedocs/gcc-6.3.0/gcc/… . Perilaku 2s-komplemen pada overflow yang ditandatangani di C adalah opsional , bahkan dalam kompiler nyata, dangcc
default untuk mengabaikannya pada tingkat optimisasi normal.Karena itu buruk trade-off untuk membuat semua perhitungan jauh lebih mahal untuk secara otomatis menangkap kasus yang jarang terjadi bahwa suatu overflow tidak terjadi. Jauh lebih baik untuk membebani programmer dengan mengenali kasus-kasus langka di mana ini merupakan masalah dan menambahkan pencegahan khusus daripada membuat semua programmer membayar harga untuk fungsionalitas yang tidak mereka gunakan.
sumber
"Jangan memaksa pengguna untuk membayar penalti kinerja untuk fitur yang mungkin tidak mereka butuhkan."
Ini adalah salah satu prinsip paling dasar dalam desain C dan C ++, dan bermula dari waktu yang berbeda ketika Anda harus melalui kontraksi konyol untuk mendapatkan kinerja yang hampir tidak memadai untuk tugas-tugas yang sekarang dianggap sepele.
Bahasa yang lebih baru terputus dengan sikap ini untuk banyak fitur lainnya, seperti pemeriksaan batas array. Saya tidak yakin mengapa mereka tidak melakukannya untuk pengecekan overflow; bisa jadi itu hanya kekhilafan.
sumber
checked
danunchecked
, menambahkan sintaks untuk beralih di antara mereka secara lokal dan juga saklar baris perintah (dan pengaturan proyek di VS) untuk mengubahnya secara global. Anda mungkin tidak setuju dengan membuatunchecked
default (saya lakukan), tetapi semua ini jelas sangat disengaja.Warisan
Saya akan mengatakan bahwa masalah ini kemungkinan berakar pada warisan. Dalam C:
Ini dilakukan untuk mendapatkan kinerja terbaik, mengikuti prinsip bahwa programmer tahu apa yang dilakukannya .
Menuntun ke Statu-Quo
Fakta bahwa C (dan dengan ekstensi C ++) tidak memerlukan deteksi overflow secara bergantian berarti bahwa pengecekan overflow lamban.
Perangkat keras kebanyakan melayani C / C ++ (serius, x86 memiliki
strcmp
instruksi (alias PCMPISTRI pada SSE 4.2)!), Dan karena C tidak peduli, CPU umum tidak menawarkan cara yang efisien untuk mendeteksi luapan. Di x86, Anda harus memeriksa bendera per-inti setelah setiap operasi yang berpotensi meluap; ketika apa yang benar-benar Anda inginkan adalah bendera "tercemar" pada hasilnya (seperti propagasi NaN). Dan operasi vektor bahkan mungkin lebih bermasalah. Beberapa pemain baru mungkin muncul di pasar dengan penanganan overflow yang efisien; tetapi untuk saat ini x86 dan ARM tidak peduli.Pengoptimal kompiler tidak pandai mengoptimalkan pemeriksaan luapan, atau bahkan mengoptimalkan dengan adanya luapan. Beberapa akademisi seperti John Regher mengeluh tentang statu-quo ini , tetapi faktanya adalah ketika fakta sederhana membuat overflow "kegagalan" mencegah optimisasi bahkan sebelum majelis mengenai CPU dapat melumpuhkan. Terutama ketika itu mencegah auto-vektorisasi ...
Dengan efek cascading
Jadi, dengan tidak adanya strategi optimasi yang efisien dan dukungan CPU yang efisien, pengecekan overflow adalah mahal. Jauh lebih mahal daripada membungkus.
Tambahkan beberapa perilaku menjengkelkan, seperti
x + y - 1
mungkin meluap ketikax - 1 + y
tidak, yang mungkin mengganggu pengguna secara sah, dan pengecekan melimpah umumnya dibuang demi pembungkus (yang menangani contoh ini dan banyak lainnya dengan anggun).Meski begitu, tidak semua harapan hilang
Telah ada upaya dalam kompiler clang dan gcc untuk mengimplementasikan "sanitizers": cara untuk memasukkan binari untuk mendeteksi kasus-kasus Perilaku Tidak Terdefinisi. Saat menggunakan
-fsanitize=undefined
, limpahan yang ditandatangani terdeteksi dan membatalkan program; sangat berguna saat pengujian.The Rust bahasa pemrograman memiliki meluap-checking diaktifkan secara default dalam mode Debug (itu menggunakan pembungkus aritmatika dalam mode Rilis untuk alasan kinerja).
Jadi, ada kekhawatiran yang berkembang tentang pengecekan overflow dan bahaya hasil palsu tidak terdeteksi, dan mudah-mudahan ini pada gilirannya akan memicu minat pada komunitas riset, komunitas kompiler, dan komunitas perangkat keras.
sumber
jo
's, dan lebih banyak efek global polusi yang mereka tambahkan ke status prediktor cabang dan ukuran kode yang meningkat. Jika bendera itu lengket itu akan menawarkan beberapa potensi nyata .. dan kemudian Anda masih tidak dapat melakukannya dengan benar dalam kode vektor.1..100
sebagai gantinya - secara eksplisit tentang rentang yang diharapkan, daripada "dipaksa" menjadi 2 ^ 31 dll. Beberapa bahasa menawarkan ini, tentu saja, dan mereka cenderung melakukan pengecekan overflow secara default (kadang-kadang pada waktu kompilasi, bahkan).x * 2 - 2
dapat meluap ketikax
51 meskipun hasilnya cocok, memaksa Anda untuk mengatur ulang perhitungan Anda (kadang-kadang dengan cara yang tidak wajar). Dalam pengalaman saya, saya telah menemukan bahwa saya umumnya lebih suka menjalankan perhitungan dalam tipe yang lebih besar, dan kemudian memeriksa apakah hasilnya cocok atau tidak.x = x * 2 - 2
harus bekerja untuk semua dix
mana penugasan menghasilkan 1 yang valid. .100) Artinya, operasi pada tipe numerik mungkin memiliki presisi yang lebih tinggi daripada tipe itu sendiri selama penugasan cocok. Ini akan sangat berguna dalam kasus-kasus seperti di(a + b) / 2
mana mengabaikan (tidak ditandatangani) meluap mungkin merupakan pilihan yang benar.Bahasa-bahasa yang berusaha mendeteksi luapan secara historis mendefinisikan semantik terkait dengan cara-cara yang sangat membatasi apa yang seharusnya menjadi optimisasi yang berguna. Di antara hal-hal lain, walaupun sering kali berguna untuk melakukan perhitungan dalam urutan berbeda dari apa yang ditentukan dalam kode, sebagian besar bahasa yang menjebak kelebihan menjamin bahwa kode yang diberikan seperti:
jika nilai awal x akan menyebabkan overflow terjadi pada pass ke-47 melalui loop, Operation1 akan mengeksekusi 47 kali dan Operation2 akan mengeksekusi 46. Dengan tidak adanya jaminan seperti itu, jika tidak ada yang lain dalam loop menggunakan x, dan tidak ada akan menggunakan nilai x mengikuti pengecualian yang dilemparkan oleh Operation1 atau Operation2, kode dapat diganti dengan:
Sayangnya, melakukan optimasi seperti itu sambil menjamin semantik yang benar dalam kasus-kasus di mana terjadi overflow dalam loop itu sulit - pada dasarnya membutuhkan sesuatu seperti:
Jika seseorang menganggap bahwa banyak kode dunia nyata menggunakan loop yang lebih terlibat, akan jelas bahwa mengoptimalkan kode sambil menjaga semantik melimpah sulit. Lebih lanjut, karena masalah caching, sangat mungkin bahwa peningkatan ukuran kode akan membuat keseluruhan program berjalan lebih lambat meskipun ada lebih sedikit operasi di jalur yang biasanya dijalankan.
Apa yang diperlukan untuk membuat deteksi limpahan menjadi murah adalah seperangkat semantik pendeteksi luapan yang lebih longgar yang akan memudahkan kode untuk melaporkan apakah perhitungan dilakukan tanpa luapan apa pun yang mungkin memengaruhi hasil (*), tetapi tanpa membebani hasil. kompiler dengan rincian lebih dari itu. Jika spec bahasa difokuskan pada pengurangan biaya deteksi overflow ke minimum yang diperlukan untuk mencapai hal di atas, itu bisa dibuat jauh lebih murah daripada di bahasa yang ada. Saya tidak mengetahui adanya upaya untuk memfasilitasi deteksi overflow yang efisien.
(*) Jika suatu bahasa menjanjikan bahwa semua luapan akan dilaporkan, maka ekspresi seperti
x*y/y
tidak dapat disederhanakanx
kecualix*y
dapat dijamin tidak meluap. Demikian juga, bahkan jika hasil perhitungan akan diabaikan, bahasa yang berjanji untuk melaporkan semua luapan harus tetap melakukannya sehingga dapat melakukan pemeriksaan luapan. Karena overflow dalam kasus seperti itu tidak dapat menghasilkan perilaku yang salah secara aritmetika, sebuah program tidak perlu melakukan pemeriksaan tersebut untuk menjamin bahwa tidak ada luapan yang menyebabkan hasil yang berpotensi tidak akurat.Kebetulan, luapan di C sangat buruk. Meskipun hampir setiap platform perangkat keras yang mendukung C99 menggunakan semantik silent-wraparound dua-pelengkap, itu adalah modis untuk kompiler modern untuk menghasilkan kode yang dapat menyebabkan efek samping yang sewenang-wenang jika terjadi overflow. Misalnya, diberikan sesuatu seperti:
GCC akan menghasilkan kode untuk test2 yang meningkatkan tanpa syarat (* p) satu kali dan mengembalikan 32768 terlepas dari nilai yang diteruskan ke q. Dengan alasannya, perhitungan (32769 * 65535) & 65535u akan menyebabkan overflow dan karenanya tidak perlu bagi kompiler untuk mempertimbangkan setiap kasus di mana (q | 32768) akan menghasilkan nilai yang lebih besar dari 32768. Meskipun tidak ada alasan bahwa perhitungan (32769 * 65535) & 65535u harus peduli dengan bit bagian atas dari hasil, gcc akan menggunakan ditandatangani melimpah sebagai pembenaran untuk mengabaikan loop.
sumber
-fwrapv
menghasilkan perilaku yang ditentukan, meskipun bukan perilaku yang diinginkan si penanya. Memang, optimasi gcc mengubah segala jenis pengembangan C menjadi ujian menyeluruh pada standar dan perilaku kompiler.x+y > z
dengan cara yang tidak akan pernah melakukan apa pun selain hasil 0 atau hasil 1, tetapi salah satu hasilnya akan sama-sama dapat diterima dalam kasus melimpah, sebuah kompiler yang menawarkan jaminan yang sering dapat menghasilkan kode yang lebih baik untuk ekspresix+y > z
daripada kompiler mana pun dapat menghasilkan versi ekspresi yang ditulis secara defensif. Realistis berbicara, apa fraksi berguna optimasi terkait meluap akan dilarang oleh jaminan bahwa bilangan bulat perhitungan selain divisi / sisanya akan mengeksekusi tanpa efek samping?-fwhatever-makes-sense
patch saya ", sangat menyarankan kepada saya bahwa ada lebih banyak untuk itu daripada imajinasi di pihak mereka. Argumen yang biasa saya dengar adalah bahwa inlining kode (dan bahkan ekspansi makro) mendapat manfaat dari deduksi sebanyak mungkin tentang penggunaan spesifik dari konstruk kode, karena salah satu hal biasanya menghasilkan kode yang disisipkan yang berhubungan dengan kasus-kasus yang tidak perlu. untuk, bahwa kode di sekitarnya "membuktikan" tidak mungkin.foo(i + INT_MAX + 1)
, kompiler-penulis tertarik untuk menerapkan optimisasi padafoo()
kode inline yang mengandalkan kebenaran pada argumennya yang non-negatif (trik divmod yang aneh, mungkin). Di bawah batasan tambahan Anda, mereka hanya bisa menerapkan optimisasi yang perilakunya untuk input negatif masuk akal untuk platform. Tentu saja, secara pribadi saya akan senang untuk itu menjadi-f
opsi yang mengaktifkan-fwrapv
dll, dan kemungkinan harus menonaktifkan beberapa optimasi tidak ada bendera untuk. Tapi bukan berarti saya bisa repot-repot melakukan semua itu sendiri.Tidak semua bahasa pemrograman mengabaikan bilangan bulat bilangan bulat. Beberapa bahasa menyediakan operasi integer aman untuk semua angka (sebagian besar dialek Lisp, Ruby, Smalltalk, ...) dan lainnya melalui perpustakaan - misalnya ada berbagai kelas BigInt untuk C ++.
Apakah suatu bahasa membuat integer aman dari overflow secara default atau tidak tergantung pada tujuannya: bahasa sistem seperti C dan C ++ perlu memberikan abstraksi biaya nol dan "integer besar" bukanlah satu. Bahasa produktivitas, seperti Ruby, dapat dan memang memberikan bilangan bulat besar di luar kotak. Bahasa seperti Java dan C # yang berada di suatu tempat di antara IMHO harus pergi dengan bilangan bulat aman di luar kotak, oleh mereka tidak.
sumber
Seperti yang telah Anda tunjukkan, C # akan menjadi 3 kali lebih lambat jika memiliki pemeriksaan melimpah diaktifkan secara default (dengan asumsi contoh Anda adalah aplikasi khas untuk bahasa itu). Saya setuju bahwa kinerja tidak selalu merupakan fitur yang paling penting, tetapi bahasa / kompiler biasanya dibandingkan dengan kinerjanya dalam tugas-tugas umum. Ini sebagian karena fakta bahwa kualitas fitur bahasa agak subyektif, sedangkan tes kinerja objektif.
Jika Anda memperkenalkan bahasa baru yang mirip dengan C # di sebagian besar aspek tetapi 3 kali lebih lambat, mendapatkan pangsa pasar tidak akan mudah, bahkan jika pada akhirnya sebagian besar pengguna akhir Anda akan mendapat manfaat dari pemeriksaan melimpah lebih dari yang mereka lakukan. dari kinerja yang lebih tinggi.
sumber
Di luar banyak jawaban yang membenarkan kurangnya pemeriksaan overflow berdasarkan kinerja, ada dua jenis aritmatika yang perlu dipertimbangkan:
perhitungan pengindeksan (array indexing dan / atau pointer aritmatika)
aritmatika lainnya
Jika bahasa menggunakan ukuran integer yang sama dengan ukuran pointer, maka program yang dibangun dengan baik tidak akan meluap melakukan perhitungan pengindeksan karena itu harus kehabisan memori sebelum perhitungan pengindeksan akan menyebabkan overflow.
Dengan demikian, memeriksa alokasi memori sudah cukup ketika bekerja dengan aritmatika pointer dan ekspresi pengindeksan yang melibatkan struktur data yang dialokasikan. Misalnya, jika Anda memiliki ruang alamat 32-bit, dan menggunakan bilangan bulat 32-bit, dan memungkinkan maksimum 2GB tumpukan dialokasikan (sekitar setengah ruang alamat), perhitungan indeks / penunjuk (pada dasarnya) tidak akan meluap.
Selanjutnya, Anda mungkin akan terkejut dengan seberapa banyak penambahan / pengurangan / perkalian melibatkan pengindeksan array atau perhitungan pointer, sehingga jatuh ke dalam kategori pertama. Pointer objek, akses lapangan, dan manipulasi array adalah operasi pengindeksan, dan banyak program tidak melakukan perhitungan aritmatika lebih dari ini! Pada dasarnya, ini alasan utama mengapa program bekerja sebaik yang mereka lakukan tanpa memeriksa integer overflow.
Semua perhitungan non-pengindeksan dan non-pointer harus diklasifikasikan sebagai yang ingin / mengharapkan overflow (mis. Perhitungan hashing), dan yang tidak (misalnya contoh penjumlahan Anda).
Dalam kasus terakhir, pemrogram akan sering menggunakan tipe data alternatif, seperti
double
atau beberapaBigInt
. Banyak perhitungan membutuhkandecimal
tipe data daripadadouble
, misalnya perhitungan keuangan. Jika mereka tidak dan tetap dengan tipe integer, maka mereka perlu berhati-hati untuk memeriksa integer overflow - atau yang lain, ya, program dapat mencapai kondisi kesalahan yang tidak terdeteksi saat Anda menunjukkan.Sebagai programmer, kita harus peka terhadap pilihan kita dalam tipe data numerik dan konsekuensinya dalam hal kemungkinan melimpah, belum lagi presisi. Secara umum (dan terutama ketika bekerja dengan keluarga bahasa C dengan keinginan untuk menggunakan tipe integer cepat) kita harus peka dan sadar akan perbedaan antara penghitungan pengindeksan vs yang lain.
sumber
Bahasa Rust memberikan kompromi yang menarik antara memeriksa overflow dan tidak, dengan menambahkan pemeriksaan untuk debugging build dan menghapusnya dalam versi rilis yang dioptimalkan. Ini memungkinkan Anda menemukan bug selama pengujian, sambil tetap mendapatkan kinerja penuh di versi final.
Karena overflow wraparound terkadang merupakan perilaku yang diinginkan, ada juga versi operator yang tidak pernah memeriksa untuk overflow.
Anda dapat membaca lebih lanjut tentang alasan di balik pilihan dalam RFC untuk perubahan. Ada juga banyak informasi menarik di posting blog ini , termasuk daftar bug yang fitur ini bantu tangkap.
sumber
checked_mul
, yang memeriksa apakah overflow telah terjadi dan kembaliNone
jika demikian,Some
sebaliknya. Ini dapat digunakan dalam produksi serta mode debug: doc.rust-lang.org/std/primitive.i32.html#examples-15Di Swift, bilangan bulat bilangan bulat terdeteksi secara default dan langsung menghentikan program. Dalam kasus di mana Anda memerlukan perilaku sampul, ada beberapa operator & +, & - dan & * yang mencapainya. Dan ada fungsi yang melakukan operasi dan memberi tahu apakah ada overflow atau tidak.
Sangat menyenangkan untuk menonton pemula mencoba untuk mengevaluasi urutan Collatz dan kode mereka crash :-)
Sekarang para perancang Swift juga merupakan perancang LLVM dan Dentang, sehingga mereka tahu sedikit tentang optimisasi, dan cukup mampu menghindari pemeriksaan luapan yang tidak perlu. Dengan semua optimisasi diaktifkan, pemeriksaan luapan tidak menambah banyak ukuran kode dan waktu eksekusi. Dan karena sebagian besar luapan menyebabkan hasil yang benar-benar salah, ukuran kode dan waktu pelaksanaannya dihabiskan dengan baik.
PS. Dalam C, C ++, Objective-C signed integer aritmatika overflow adalah perilaku yang tidak terdefinisi. Itu berarti apa pun yang dikompilasi oleh kompiler dalam hal integer yang ditandatangani ditandatangani adalah benar, menurut definisi. Cara tipikal untuk mengatasi overflow integer yang ditandatangani adalah dengan mengabaikannya, mengambil hasil apa pun yang diberikan CPU kepada Anda, membuat asumsi ke dalam kompiler bahwa meluap seperti itu tidak akan pernah terjadi (dan menyimpulkan misalnya bahwa n + 1> n selalu benar, karena overflow adalah diasumsikan tidak pernah terjadi), dan kemungkinan yang jarang digunakan adalah untuk memeriksa dan crash jika terjadi overflow, seperti yang dilakukan Swift.
sumber
x+1>x
sebagai benar tanpa syarat tidak akan memerlukan kompiler untuk membuat "asumsi" tentang x jika kompiler diperbolehkan untuk mengevaluasi ekspresi integer menggunakan tipe yang lebih besar sembarang sebagai nyaman (atau berperilaku seolah-olah melakukannya). Contoh yang lebih buruk dari "asumsi" berbasis luapan akan memutuskan bahwa diberikanuint32_t mul(uint16_t x, uint16_t y) { return x*y & 65535u; }
kompiler dapat digunakansum += mul(65535, x)
untuk memutuskan bahwax
tidak boleh lebih besar dari 32768 [perilaku yang mungkin akan mengejutkan orang-orang yang menulis C89 Rationale, yang menunjukkan bahwa salah satu faktor penentu. ..unsigned short
mempromosikansigned int
adalah fakta bahwa dua-pelengkap implementasi silent-wraparound (yaitu mayoritas implementasi C yang digunakan) akan memperlakukan kode seperti di atas dengan cara yang sama baikunsigned short
dipromosikan keint
atauunsigned
. Standar tidak memerlukan implementasi pada perangkat tambahan pelengkap silent-wraparound untuk memperlakukan kode seperti di atas secara wajar, tetapi para penulis Standar tampaknya berharap bahwa mereka akan melakukannya.Sebenarnya, penyebab sebenarnya untuk ini adalah murni teknis / historis: sebagian besar tanda mengabaikan CPU . Pada umumnya hanya ada satu instruksi untuk menambahkan dua bilangan bulat dalam register, dan CPU tidak peduli apakah Anda menginterpretasikan dua bilangan bulat ini sebagai ditandatangani atau tidak. Hal yang sama berlaku untuk pengurangan, dan bahkan untuk penggandaan. Satu-satunya operasi aritmatika yang perlu disadari adalah pembagian.
Alasan mengapa ini bekerja, adalah representasi pelengkap 2 dari bilangan bulat yang ditandatangani yang digunakan oleh hampir semua CPU. Misalnya, dalam komplemen 4-bit 2 penambahan 5 dan -3 terlihat seperti ini:
Amati bagaimana perilaku membungkus membuang-buang bit membawa hasil yang ditandatangani benar. Demikian juga, CPU biasanya menerapkan pengurangan
x - y
sebagaix + ~y + 1
:Ini mengimplementasikan pengurangan sebagai tambahan dalam perangkat keras, hanya mengubah input ke unit aritmatika-logis (ALU) dengan cara yang sepele. Apa yang bisa lebih sederhana?
Karena perkalian tidak lain adalah urutan penambahan, ia berperilaku dengan cara yang sama baiknya. Hasil dari menggunakan representasi komplemen 2's dan mengabaikan pelaksanaan operasi aritmatika adalah sirkuit yang disederhanakan, dan set instruksi yang disederhanakan.
Jelas, karena C dirancang untuk bekerja dekat dengan logam, ia mengadopsi perilaku yang sama persis seperti perilaku standar aritmatika yang tidak ditandatangani, yang memungkinkan hanya aritmatika yang ditandatangani untuk menghasilkan perilaku yang tidak terdefinisi. Dan pilihan itu dibawa ke bahasa lain seperti Java, dan, jelas, C #.
sumber
x==INT_MAX
, makax+1
mungkin secara sewenang-wenang berperilaku sebagai +2147483648 atau -2147483648 di kompiler kenyamanan), tapi ...x
dany
yanguint16_t
dan kode pada sistem 32-bit menghitungx*y & 65535u
saaty
ini 65.535, compiler harus mengasumsikan kode yang tidak akan pernah tercapai bilax
lebih besar dari 32.768Beberapa jawaban telah membahas biaya pemeriksaan, dan Anda telah mengedit jawaban Anda untuk membantah bahwa ini adalah pembenaran yang masuk akal. Saya akan mencoba membahas poin-poin itu.
Dalam C dan C ++ (sebagai contoh), salah satu prinsip desain bahasa bukanlah untuk menyediakan fungsionalitas yang tidak diminta. Ini biasanya disimpulkan dengan frasa "tidak membayar untuk apa yang tidak Anda gunakan". Jika programmer ingin mengecek overflow maka dia dapat memintanya (dan membayar penalti). Ini membuat bahasa lebih berbahaya untuk digunakan, tetapi Anda memilih untuk bekerja dengan bahasa yang mengetahui hal itu, sehingga Anda menerima risikonya. Jika Anda tidak menginginkan risiko itu, atau jika Anda menulis kode di mana keselamatan merupakan kinerja terpenting, maka Anda dapat memilih bahasa yang lebih tepat di mana pengorbanan kinerja / risiko berbeda.
Ada beberapa hal yang salah dengan alasan ini:
Ini khusus lingkungan. Biasanya sangat tidak masuk akal untuk mengutip angka-angka spesifik seperti ini, karena kode ditulis untuk semua jenis lingkungan yang bervariasi menurut urutan besarnya dalam hal kinerja mereka. 1 nanosecond Anda pada (saya berasumsi) mesin desktop mungkin tampak luar biasa cepat untuk seseorang pengkodean untuk lingkungan tertanam, dan lambat tak tertahankan untuk seseorang pengkodean untuk cluster komputer super.
1 nanosecond mungkin tampak seperti tidak ada untuk segmen kode yang jarang berjalan. Di sisi lain, jika itu berada di loop dalam dari beberapa perhitungan yang merupakan fungsi utama dari kode, maka setiap fraksi waktu Anda dapat mencukur dapat membuat perbedaan besar. Jika Anda menjalankan simulasi pada sebuah cluster maka fraksi nanodetik yang tersimpan di loop dalam Anda dapat langsung diterjemahkan ke uang yang dihabiskan untuk perangkat keras dan listrik.
Untuk beberapa algoritma dan konteks, 10.000.000.000 iterasi dapat menjadi tidak signifikan. Sekali lagi, pada umumnya tidak masuk akal untuk berbicara tentang skenario tertentu yang hanya berlaku dalam konteks tertentu.
Mungkin Anda benar. Tetapi sekali lagi, ini adalah masalah apa tujuan dari suatu bahasa tertentu. Banyak bahasa sebenarnya dirancang untuk mengakomodasi kebutuhan "sebagian besar" atau untuk mendukung keamanan daripada masalah lain. Lainnya, seperti C dan C ++, memprioritaskan efisiensi. Dalam konteks itu, membuat semua orang membayar penalti kinerja hanya karena kebanyakan orang tidak akan terganggu, bertentangan dengan apa yang coba dicapai oleh bahasa tersebut.
sumber
Ada jawaban yang baik, tapi saya pikir ada titik yang terlewatkan di sini: efek dari bilangan bulat bilangan bulat tidak selalu merupakan hal yang buruk, dan setelah itu sulit untuk mengetahui apakah
i
beralih dari ada menjadiMAX_INT
menjadiMIN_INT
karena masalah melimpah atau jika itu sengaja dilakukan dengan mengalikan -1.Misalnya, jika saya ingin menambahkan semua bilangan bulat yang direpresentasikan lebih besar dari 0 bersama-sama, saya hanya akan menggunakan
for(i=0;i>=0;++i){...}
loop tambahan- dan ketika itu melebihi itu menghentikan penambahan, yang merupakan perilaku tujuan (melempar kesalahan berarti saya harus mengelak perlindungan sewenang-wenang karena mengganggu aritmatika standar). Ini praktik buruk untuk membatasi aritmatika primitif, karena:sumber
INT_MAX
keINT_MIN
dengan mengalikan dengan -1.for(i=0;i>=0;++i){...}
adalah gaya kode yang saya coba untuk tidak berkecil hati dalam tim saya: itu bergantung pada efek khusus / efek samping dan tidak mengungkapkan dengan jelas apa yang seharusnya dilakukan. Tapi saya tetap menghargai jawaban Anda karena menunjukkan paradigma pemrograman yang berbeda.i
adalah tipe 64-bit, bahkan pada implementasi dengan perilaku dua komplemen silent-wraparound yang konsisten, menjalankan satu miliar iterasi per detik, perulangan seperti itu hanya dapat dijamin untuk menemukan nilai terbesarint
jika dibiarkan berjalan untuk ratusan tahun. Pada sistem yang tidak menjanjikan perilaku silent-wraparound yang konsisten, perilaku seperti itu tidak akan dijamin tidak peduli berapa lama kode diberikan.