Mengapa Overflow Aritmatika diabaikan?

76

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.

Bernhard Hiller
sumber
19
hanya sebagai catatan, untuk C # Anda dapat menggunakan checked { }bagian untuk menandai bagian-bagian dari kode yang harus melakukan pemeriksaan Arithmetic Overflow. Ini karena kinerja
Paweł Łukasik
14
"Pernah mencoba meringkas semua angka dari 1 hingga 2.000.000 dalam bahasa pemrograman favoritmu?" - Ya: (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.)
Jörg W Mittag
27
@BernhardHiller Integerdi Haskell adalah presisi sewenang-wenang, itu akan menampung nomor berapa pun selama Anda tidak kehabisan RAM yang dapat dialokasikan.
Polygnome
50
Kecelakaan Ariane 5 disebabkan oleh memeriksa limpahan yang tidak masalah - roket berada di bagian penerbangan di mana hasil perhitungan bahkan tidak diperlukan lagi. Sebaliknya, overflow terdeteksi, dan itu menyebabkan penerbangan dibatalkan.
Simon B
9
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.
usr

Jawaban:

86

Ada 3 alasan untuk ini:

  1. Biaya untuk memeriksa luapan (untuk setiap operasi aritmatika tunggal) pada saat run-time berlebihan.

  2. Kompleksitas membuktikan bahwa pemeriksaan luapan dapat dihilangkan pada waktu kompilasi adalah berlebihan.

  3. Dalam beberapa kasus (misalnya perhitungan CRC, pustaka angka besar, dll) "wrap on overflow" lebih nyaman bagi programmer.

Brendan
sumber
10
@DmitryGrigoryev unsigned inttidak boleh terlintas dalam pikiran karena bahasa dengan pemeriksaan overflow harus memeriksa semua jenis integer secara default. Anda harus menulis wrapping unsigned int.
user253751
32
Saya tidak membeli argumen biaya. CPU tidak memeriksa overflow pada perhitungan integer SETIAP TUNGGAL dan mengatur flag carry di ALU. Dukungan bahasa pemrograman yang hilang. didOverflow()Fungsi inline sederhana atau bahkan variabel global __carryyang memungkinkan akses ke flag carry akan dikenakan biaya nol waktu CPU jika Anda tidak menggunakannya.
slebetman
37
@slebetman: Itu x86. ARM tidak. Misalnya ADDtidak mengatur carry (Anda perlu ADDS). Itanium bahkan tidak memiliki bendera pembawa. Dan bahkan pada x86, AVX tidak memiliki flag.
MSalters
30
@slebetman Mengatur flag carry, ya (pada x86, ingatlah). Tapi kemudian Anda harus membaca flag carry dan memutuskan hasilnya - itu bagian yang mahal. Karena operasi aritmatika sering digunakan dalam loop (dan loop ketat pada saat itu), ini dapat dengan mudah mencegah banyak optimisasi kompiler yang aman yang dapat memiliki dampak yang sangat besar pada kinerja bahkan jika Anda hanya memerlukan satu instruksi tambahan (dan Anda membutuhkan lebih banyak dari itu ). Apakah ini berarti harus menjadi default? Mungkin, terutama dalam bahasa seperti C # di mana mengatakan uncheckeditu cukup mudah; tetapi Anda mungkin melebih-lebihkan seberapa sering masalah meluap.
Luaan
12
Harga ARM addssama dengan add(itu hanya instruksi 1-bit flag yang memilih apakah flag carry diperbarui). addInstruksi MIPS menjebak overflow - Anda harus meminta untuk tidak menjebak overflow dengan menggunakan addugantinya!
user253751
65

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 menambahkan checked {}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 fwrapvopsi 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 + bhal ini tidak ditentukan apakah aatau bdievaluasi 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 seperti ON ERROR RESUME NEXT. Itu menyembunyikan kesalahan tetapi tidak membantu mendapatkan hasil yang benar. supercat menunjukkan bahwa kadang-kadang optimasi kinerja untuk melakukan ini.

usr
sumber
2
Jawaban yang sangat bagus. Jadi apa teori Anda tentang mengapa mereka memutuskan untuk melakukannya dengan cara itu? Hanya menyalin semua orang yang menyalin C dan akhirnya perakitan dan biner?
jpmc26
19
Ketika 99% dari basis pengguna Anda mengharapkan suatu perilaku, Anda cenderung memberikannya kepada mereka. Dan untuk "menyalin C," itu sebenarnya bukan salinan C, tetapi perpanjangan dari itu. C menjamin pengecualian perilaku bebas untuk unsignedbilangan 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 ++
Cort Ammon
10
@CortAmmon: Bahasa yang dirancang Dennis Ritchie telah mendefinisikan perilaku sampul untuk bilangan bulat yang ditandatangani, tetapi tidak benar-benar cocok untuk digunakan pada platform pelengkap yang bukan dua. Walaupun memungkinkan penyimpangan tertentu dari penyelesaian komplementer dua komplit dapat sangat membantu beberapa optimisasi (mis. Memungkinkan kompiler untuk mengganti x * y / y dengan x dapat menyimpan perkalian dan pembagian), penulis kompiler telah menafsirkan Perilaku Tidak Terdefinisi bukan sebagai kesempatan untuk melakukan apa yang masuk akal untuk platform target dan bidang aplikasi yang diberikan, tetapi lebih sebagai kesempatan untuk membuang akal di luar jendela.
supercat
3
@CortAmmon - Periksa kode yang dihasilkan oleh gcc -O2untuk x + 1 > x(di mana xadalah int). 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, dan gccdefault untuk mengabaikannya pada tingkat optimisasi normal.
Jonathan Cast
2
@supercat Ya, sebagian besar penulis kompiler C lebih tertarik untuk memastikan beberapa benchmark tidak realistis berjalan 0,5% lebih cepat daripada mencoba memberikan semantik yang wajar kepada programmer (ya saya mengerti mengapa ini bukan masalah yang mudah untuk dipecahkan dan ada beberapa optimisasi wajar yang dapat menyebabkan hasil yang tak terduga ketika digabungkan, yada, yada tapi tetap saja tidak ada fokus dan Anda menyadarinya jika Anda mengikuti percakapan). Untungnya ada beberapa orang yang mencoba berbuat lebih baik .
Voo
30

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.

Kilian Foth
sumber
28
Entah bagaimana seperti mengatakan bahwa cek untuk Buffer Overflow harus dihilangkan karena hampir tidak pernah terjadi ...
Bernhard Hiller
73
@BernhardHiller: dan itulah yang dilakukan oleh C dan C ++.
Michael Borgwardt
12
@ DavidBrown: Seperti halnya aritmatika meluap. Yang pertama tidak kompromi VM sekalipun.
Deduplicator
35
@Dupuplikator membuat poin yang bagus. CLR dirancang dengan hati-hati sehingga program yang dapat diverifikasi tidak dapat melanggar invarian runtime bahkan ketika hal-hal buruk terjadi. Program yang aman tentu saja dapat melanggar invarian mereka sendiri ketika hal-hal buruk terjadi.
Eric Lippert
7
@svick Operasi aritmatika mungkin jauh lebih umum daripada operasi pengindeksan array. Dan sebagian besar ukuran bilangan bulat cukup besar sehingga sangat jarang untuk melakukan aritmatika yang meluap. Jadi rasio biaya-manfaatnya sangat berbeda.
Barmar
20

apa keputusan desain di balik perilaku berbahaya seperti itu?

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

Michael Borgwardt
sumber
18
Ini jelas bukan pengawasan dalam desain C #. Perancang C # sengaja menciptakan dua mode: checkeddan unchecked, 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 membuat uncheckeddefault (saya lakukan), tetapi semua ini jelas sangat disengaja.
svick
8
@slebetman - hanya untuk catatan: biaya di sini bukan biaya untuk memeriksa overflow (yang sepele), tetapi biaya menjalankan kode yang berbeda tergantung pada apakah overflow terjadi (yang sangat mahal). CPU tidak suka pernyataan cabang bersyarat.
Jonathan Cast
5
@jcast Tidakkah prediksi cabang pada prosesor modern hampir menghilangkan hukuman pernyataan cabang bersyarat itu? Setelah semua kasus normal seharusnya tidak ada overflow, jadi itu perilaku percabangan yang sangat mudah ditebak.
CodeMonkey
4
Setuju dengan @CodeMonkey. Sebuah kompiler akan dimasukkan ke dalam lompatan bersyarat jika meluap, ke halaman yang biasanya tidak dimuat / dingin. Prediksi default untuk "tidak diambil", dan mungkin tidak akan berubah. Total overhead adalah satu instruksi dalam pipa. Tapi itu adalah satu instruksi overhead per instruksi aritmatika.
MSalters
2
@ Pemalas ya, ada overhead instruksi tambahan. Dan dampaknya mungkin besar jika Anda memiliki masalah terikat CPU secara eksklusif. Dalam sebagian besar aplikasi dengan campuran IO dan kode berat CPU saya akan menganggap dampaknya minimal. Saya suka cara Rust, menambahkan overhead hanya di build Debug, tetapi menghapusnya di build Release.
CodeMonkey
20

Warisan

Saya akan mengatakan bahwa masalah ini kemungkinan berakar pada warisan. Dalam C:

  • ditandatangani melimpah adalah perilaku yang tidak terdefinisi (kompiler mendukung bendera untuk membuatnya dibungkus),
  • unsigned overflow didefinisikan sebagai perilaku (ia membungkus).

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 strcmpinstruksi (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 - 1mungkin meluap ketika x - 1 + ytidak, 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.

Matthieu M.
sumber
6
@DmitryGrigoryev itu kebalikan dari cara yang efektif untuk memeriksa luapan, misalnya pada Haswell itu mengurangi throughput dari 4 penambahan normal per siklus menjadi hanya 1 tambahan yang diperiksa, dan itu sebelum mempertimbangkan dampak salah duga cabang dari 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.
3
Karena Anda menautkan ke posting blog yang ditulis oleh John Regehr, saya pikir akan tepat juga untuk menaut ke artikelnya , yang ditulis beberapa bulan sebelum yang Anda tautkan . Artikel-artikel ini berbicara tentang filosofi yang berbeda: Dalam artikel sebelumnya, bilangan bulat adalah ukuran tetap; aritmatika integer diperiksa (mis. kode tidak dapat melanjutkan eksekusi); ada pengecualian atau jebakan. Artikel yang lebih baru berbicara tentang membuang bilangan bulat ukuran tetap, yang menghilangkan luapan.
rwong
2
@rwong Bilangan bulat tak terbatas memiliki masalah mereka juga. Jika overflow Anda adalah hasil dari bug (yang sering terjadi), itu dapat mengubah crash cepat menjadi penderitaan yang berkepanjangan yang menghabiskan semua sumber daya server sampai semuanya gagal mengerikan. Saya sebagian besar penggemar pendekatan "gagal awal" - lebih sedikit kesempatan meracuni seluruh lingkungan. Saya lebih suka tipe Pascal-ish 1..100sebagai 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).
Luaan
1
@Luaan: Yang menarik adalah bahwa sering kali perhitungan menengah dapat sementara meluap, tetapi hasilnya tidak. Misalnya, pada rentang 1..100 Anda, x * 2 - 2dapat meluap ketika x51 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.
Matthieu M.
1
@ MatthieuM. Ya, di situlah Anda masuk ke wilayah "kompiler yang cukup pintar". Idealnya, nilai 103 harus valid untuk tipe 1..100 selama itu tidak pernah digunakan dalam konteks di mana 1..100 yang sebenarnya diharapkan (misalnya x = x * 2 - 2harus bekerja untuk semua di xmana 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) / 2mana mengabaikan (tidak ditandatangani) meluap mungkin merupakan pilihan yang benar.
Luaan
10

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:

for (int i=0; i<100; i++)
{
  Operation1();
  x+=i;
  Operation2();
}

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:

x+=4950;
for (int i=0; i<100; i++)
{
  Operation1();
  Operation2();
}

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:

if (x < INT_MAX-4950)
{
  x+=4950;
  for (int i=0; i<100; i++)
  {
    Operation1();
    Operation2();
  }
}
else
{
  for (int i=0; i<100; i++)
  {
    Operation1();
    x+=i;
    Operation2();
  }
}

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/ytidak dapat disederhanakan xkecuali x*ydapat 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:

#include <stdint.h>
uint32_t test(uint16_t x, uint16_t y) { return x*y & 65535u; }
uint32_t test2(uint16_t q, int *p)
{
  uint32_t total=0;
  q|=32768;
  for (int i = 32768; i<=q; i++)
  {
    total+=test(i,65535);
    *p+=1;
  }
  return total;
}

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.

supercat
sumber
2
"itu modis untuk kompiler modern ..." - sama, itu juga singkat untuk pengembang kernel tertentu yang terkenal untuk memilih untuk tidak membaca dokumentasi mengenai bendera optimasi yang mereka gunakan, dan kemudian bertindak marah di seluruh internet karena mereka dipaksa untuk menambahkan lebih banyak flag compiler untuk mendapatkan perilaku yang mereka inginkan ;-). Dalam hal ini, -fwrapvmenghasilkan 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.
Steve Jessop
1
@SteveJessop: C akan menjadi bahasa yang jauh lebih sehat jika penulis kompiler mengenali dialek tingkat rendah di mana "perilaku tidak terdefinisi" berarti "melakukan apa pun yang masuk akal pada platform yang mendasarinya", dan kemudian menambahkan cara bagi programmer untuk mengesampingkan jaminan yang tidak perlu yang tersirat dengan demikian, daripada menganggap bahwa frasa "non-portabel atau salah" dalam Standar hanya berarti "salah". Dalam banyak kasus, kode optimal yang dapat diperoleh dalam bahasa dengan jaminan perilaku yang lemah akan jauh lebih baik daripada yang bisa diperoleh dengan jaminan yang lebih kuat atau tanpa jaminan. Misalnya ...
supercat
1
... jika seorang programmer perlu mengevaluasi x+y > zdengan 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 ekspresi x+y > zdaripada 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?
supercat
Saya mengaku bahwa saya tidak sepenuhnya ke detail, tetapi fakta bahwa dendam Anda adalah dengan "penulis kompiler" secara umum, dan tidak secara khusus "seseorang di gcc yang tidak akan menerima -fwhatever-makes-sensepatch 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.
Steve Jessop
Jadi untuk contoh yang disederhanakan, jika saya menulis foo(i + INT_MAX + 1), kompiler-penulis tertarik untuk menerapkan optimisasi pada foo()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 -fopsi yang mengaktifkan -fwrapvdll, dan kemungkinan harus menonaktifkan beberapa optimasi tidak ada bendera untuk. Tapi bukan berarti saya bisa repot-repot melakukan semua itu sendiri.
Steve Jessop
9

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.

Nemanja Trifunovic
sumber
Perhatikan bahwa ada perbedaan antara mendeteksi overflow (dan kemudian memiliki sinyal, panik, pengecualian, ...) dan beralih ke num besar. Yang pertama harus bisa dilakukan jauh lebih murah daripada yang kedua.
Matthieu M.
@ MatthieuM. Tentu saja - dan saya sadar saya tidak jelas tentang hal itu dalam jawaban saya.
Nemanja Trifunovic
7

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.

Dmitry Grigoryev
sumber
10
Ini adalah kasus khusus untuk C #, yang pada hari-hari awal dibandingkan dengan Java dan C ++ bukan pada metrik produktivitas pengembang, yang sulit untuk diukur, atau pada metrik penghematan uang dari tidak berurusan dengan keamanan-pelanggaran, yang sulit untuk diukur, tetapi pada tolok ukur kinerja sepele.
Eric Lippert
1
Dan kemungkinan, kinerja CPU diperiksa dengan beberapa angka sederhana. Dengan demikian optimasi untuk deteksi overflow dapat menghasilkan hasil "buruk" pada tes tersebut. Tangkapan22.
Bernhard Hiller
5

Di luar banyak jawaban yang membenarkan kurangnya pemeriksaan overflow berdasarkan kinerja, ada dua jenis aritmatika yang perlu dipertimbangkan:

  1. perhitungan pengindeksan (array indexing dan / atau pointer aritmatika)

  2. 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 doubleatau beberapa BigInt. Banyak perhitungan membutuhkan decimaltipe data daripada double, 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.

Erik Eidt
sumber
3

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.

Hjulle
sumber
2
Rust juga menyediakan metode seperti checked_mul, yang memeriksa apakah overflow telah terjadi dan kembali Nonejika demikian, Somesebaliknya. Ini dapat digunakan dalam produksi serta mode debug: doc.rust-lang.org/std/primitive.i32.html#examples-15
Akavall
3

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

gnasher729
sumber
1
Saya kadang-kadang bertanya-tanya apakah orang-orang yang mendorong kegilaan yang didorong oleh UB di C secara diam-diam mencoba untuk merusaknya demi bahasa lain. Itu masuk akal.
supercat
Memperlakukan x+1>xsebagai 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 diberikan uint32_t mul(uint16_t x, uint16_t y) { return x*y & 65535u; }kompiler dapat digunakan sum += mul(65535, x)untuk memutuskan bahwa xtidak boleh lebih besar dari 32768 [perilaku yang mungkin akan mengejutkan orang-orang yang menulis C89 Rationale, yang menunjukkan bahwa salah satu faktor penentu. ..
supercat
... dalam unsigned shortmempromosikan signed intadalah fakta bahwa dua-pelengkap implementasi silent-wraparound (yaitu mayoritas implementasi C yang digunakan) akan memperlakukan kode seperti di atas dengan cara yang sama baik unsigned shortdipromosikan ke intatau unsigned. 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.
supercat
2

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:

  0101   (5)
  1101   (-3)
(11010)  (carry)
  ----
  0010   (2)

Amati bagaimana perilaku membungkus membuang-buang bit membawa hasil yang ditandatangani benar. Demikian juga, CPU biasanya menerapkan pengurangan x - ysebagai x + ~y + 1:

  0101   (5)
  1100   (~3, binary negation!)
(11011)  (carry, we carry in a 1 bit!)
  ----
  0010   (2)

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

cmaster
sumber
Saya datang ke sini untuk memberikan jawaban ini juga.
Tuan Lister
Sayangnya, beberapa orang tampaknya menganggap sangat tidak masuk akal anggapan bahwa orang yang menulis kode C tingkat rendah pada platform harus memiliki keberanian untuk berharap bahwa kompiler C yang cocok untuk tujuan tersebut akan berperilaku dengan cara terbatas jika terjadi overflow. Secara pribadi, saya pikir masuk akal bagi seorang kompiler untuk berperilaku seolah-olah perhitungan dilakukan dengan menggunakan presisi yang diperluas secara sewenang-wenang pada kenyamanan kompiler (jadi pada sistem 32-bit, jika x==INT_MAX, maka x+1mungkin secara sewenang-wenang berperilaku sebagai +2147483648 atau -2147483648 di kompiler kenyamanan), tapi ...
supercat
beberapa orang tampaknya berpikir bahwa jika xdan yyang uint16_tdan kode pada sistem 32-bit menghitung x*y & 65535usaat yini 65.535, compiler harus mengasumsikan kode yang tidak akan pernah tercapai bila xlebih besar dari 32.768
supercat
1

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

Tetapi dengan 10.000.000.000 pengulangan, waktu yang dibutuhkan oleh cek masih kurang dari 1 nanosecond.

Ada beberapa hal yang salah dengan alasan ini:

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

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

  3. 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 ada situasi di mana itu penting, tetapi untuk sebagian besar aplikasi, itu tidak masalah.

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.

Jon Bentley
sumber
-1

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 iberalih dari ada menjadi MAX_INTmenjadi MIN_INTkarena 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:

  • Mereka digunakan dalam segala hal - perlambatan dalam matematika primitif adalah perlambatan dalam setiap program yang berfungsi
  • Jika seorang programmer membutuhkannya, mereka selalu dapat menambahkannya
  • Jika Anda memilikinya dan programmer tidak memerlukannya (tetapi membutuhkan runtimes yang lebih cepat), mereka tidak dapat menghapusnya dengan mudah untuk optimasi
  • Jika Anda memilikinya dan programmer memerlukannya agar tidak ada di sana (seperti pada contoh di atas), programmer sama-sama mengambil run-time hit (yang mungkin atau mungkin tidak relevan), dan programmer masih perlu menginvestasikan waktu untuk menghapus atau bekerja di sekitar 'perlindungan'.
Delioth
sumber
3
Sangat tidak mungkin bagi seorang programmer untuk menambahkan pemeriksaan overflow yang efisien jika suatu bahasa tidak menyediakannya. Jika suatu fungsi menghitung nilai yang diabaikan, kompiler dapat mengoptimalkan perhitungan. Jika fungsi menghitung nilai yang meluap-diperiksa tapi sebaliknya diabaikan, compiler harus melakukan perhitungan dan perangkap jika meluap, bahkan jika overflow akan tidak mempengaruhi output program dan dapat diabaikan dengan aman.
supercat
1
Anda tidak dapat beralih dari INT_MAXke INT_MINdengan mengalikan dengan -1.
David Conrad
Solusinya jelas untuk memberikan cara bagi programmer untuk mematikan cek di blok kode atau unit kompilasi yang diberikan.
David Conrad
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.
Bernhard Hiller
1
@Delioth: Jika iadalah 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 terbesar intjika 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.
supercat