Bilangan Floating-Point

Diperbarui 20 menit baca Menengah Read in English

Komputasi Sistem bilangan Python JavaScript

Jawaban singkat

Bilangan floating-point adalah bilangan biner dalam notasi ilmiah, disimpan sebagai tanda, eksponen, dan sejumlah tetap bit pecahan, sehingga rentangnya sangat besar tetapi hanya sekitar 16 angka desimal (untuk 64 bit). Bilangan tidak berjarak sama: jaraknya berlipat dua di setiap pangkat dua, setiap operasi membulatkan ke nilai terdekat yang muat, dan galat kecilnya dapat menumpuk, menghapus angka, atau terserap.

Poin penting

  • Sebuah double 64 bit adalah (-1)^s×(1.f)_2×2^e: 1 bit tanda, 11 bit eksponen, dan 52 bit pecahan. Angka 1 di depan tidak disimpan, sehingga ketelitiannya 53 bit, sekitar 15,95 angka desimal.
  • Bilangannya tidak berjarak sama. Di antara 2^k dan 2^k+1 selalu ada jumlah yang sama, sehingga jaraknya berlipat dua di setiap pangkat dua; mulai 2^53 jarak pada double adalah 2, dan bilangan bulat ganjil tidak ada lagi.
  • Setiap operasi dasar dibulatkan ke bilangan terdekat yang dapat disajikan, seri ke genap, sehingga \mathrmfl(a\circ b)=(a\circ b)(1+\delta) dengan |\delta|\le u=2^-53≈1,1×10^-16. Jarak di atas 1 adalah epsilon mesin 2^-52.
  • Pola khusus: ±0, ±∞, dan NaN, yang tidak sama bahkan dengan dirinya sendiri; bilangan subnormal mengisi celah antara 0 dan bilangan normal terkecil, sehingga underflow berlangsung bertahap.
  • Galat pembulatan menumpuk ketika kamu menjumlahkan banyak bilangan, hilang ke dalam bilangan besar (penyerapan), dan membuat pengurangan dua bilangan yang hampir sama kehilangan angka (pembatalan). Jumlah berkompensasi, penulisan ulang aljabar, dan fungsi pustaka seperti expm1 membatasi kerusakannya.
  • Bandingkan float dengan toleransi relatif dan mutlak, jangan dengan == setelah perhitungan; pakai bilangan bulat atau tipe desimal untuk uang; dan pilih float16, bfloat16, float32, atau float64 menurut rentang dan angka yang diperlukan soal.

Apa itu bilangan floating-point?

Bilangan floating-point adalah bilangan yang ditulis dalam notasi ilmiah biner dengan jumlah angka tetap: tanda, signifikan (angkanya), dan eksponen yang menyatakan letak titiknya. Dalam desimal, notasi ilmiah menulis -6,02×10^23 atau 1,6×10^-19: beberapa angka signifikan dan pangkat sepuluh. Komputer melakukan hal yang sama dalam basis 2: x=(-1)^s×(1.f_1f_2f_3… f_m)_2×2^e. Titiknya "mengambang" karena eksponen e menggesernya, sehingga format yang sama memuat ukuran alam semesta dan ukuran atom. Harganya adalah jumlah angka m+1 tetap, sehingga setiap bilangan hanya memiliki angka signifikan yang terbatas, sebesar atau sekecil apa pun ia.

Mengapa bukan titik tetap? Format titik tetap menjaga titik di satu tempat, misalnya selalu 16 angka biner setelahnya. Maka ia tidak dapat memuat 10^30 dan 10^-30 sekaligus; floating-point mengorbankan sebagian ketelitian untuk menjangkau seluruh rentang itu.

Standarnya. Hampir semua perangkat keras mengikuti IEEE 754. Ia mendefinisikan beberapa format biner, yang berbeda dalam banyaknya bit untuk eksponen dan untuk pecahan:

FormatBitTanda + eksponen + pecahanBit signifikanAngka desimalNilai terbesar
half, float16161 + 5 + 1011sekitar 3,365504
bfloat16161 + 8 + 78sekitar 2,4sekitar 3,4×10^38
single, float32321 + 8 + 2324sekitar 7,2sekitar 3,4×10^38
double, float64641 + 11 + 5253sekitar 15,95sekitar 1,8×10^308

Banyaknya angka desimal adalah banyaknya bit signifikan dikali \log_102≈0,301. Di Python float adalah double, di JavaScript setiap Number adalah double, dan di C dan C++ double adalah double dan float adalah single. Artikel ini membahas apa yang dilakukan format-format ini, dengan cara yang penting ketika kamu menulis kode; bilangan real menjelaskan versi singkatnya, dan bilangan biner menjelaskan mengapa 0,1 tidak punya ekspansi biner berhingga.

Bagaimana bit sebuah double disusun?

Sebuah double memiliki tiga bidang: 1 bit tanda, 11 bit eksponen, dan 52 bit pecahan, dan nilainya mengikuti bidang eksponen.

  • Bilangan normal (bidang eksponen 1 sampai 2046): nilainya (-1)^s×\left(1+(F)/(2^52)\right)×2^E-1023, dengan E bidang eksponen yang dibaca sebagai bilangan bulat, F bidang pecahan, dan 1023 adalah bias. Angka 1 di depan tersirat, itulah sebabnya 52 bit tersimpan memberi ketelitian 53 bit. Bias memungkinkan eksponen disimpan tanpa tandanya sendiri.
  • Bidang eksponen 0: nilai ±0 jika pecahannya 0, dan selain itu bilangan subnormal (-1)^s×(F)/(2^52)×2^-1022 tanpa angka 1 tersirat.
  • Bidang eksponen 2047 (berisi satu semua): ±∞ jika pecahannya 0, dan selain itu NaN.

Dua contoh. Bilangan -2,5=-1,25×2^1 bertanda 1, bidang eksponen 1+1023=1024=10000000000_2 dan pecahan 0100…0 (bagian 0,25 dari 1,25), sehingga polanya dalam heksadesimal adalah C004000000000000. Bilangan 0,1 adalah 1,6000000000000000888…×2^-4: tanda 0, bidang eksponen 1019, dan pecahan yang berulang 1001 1001… dan dibulatkan ke atas di ujungnya, menghasilkan 3FB999999999999A. A terakhir itu adalah pembulatannya, dan itulah sebabnya nilai yang tersimpan adalah 0,1000000000000000055511151231257827021181583404541015625, sedikit lebih dari 0,1. Untuk single, bilangannya sama kecuali bias 127 dan ada 8 dan 23 bit.

Ketik sebuah bilangan di bawah dan pilih format. Widget menunjukkan tiga bidang, rumus yang diwakilinya, nilai tepat yang tersimpan, galat pembulatannya, dan tetangga di atas dan di bawah.

Interaktif: bit sebuah bilangan floating-point

Ketik sebuah bilangan dan format (float16, bfloat16, float32, float64) dan lihat bit tanda, eksponen, dan pecahannya, rumus yang diwakilinya, nilai tepat yang tersimpan, galat pembulatan, dan jarak ke tetangganya.

Mengapa bilangan floating-point tidak berjarak sama?

Di antara dua pangkat dua berurutan, 2^k dan 2^k+1, suatu format selalu memiliki jumlah nilai yang sama (2^52 untuk double), sehingga jarak antartetangga adalah 2^k-52 dan berlipat dua di setiap pangkat dua. Di dekat 1 jaraknya 2^-52≈2,2×10^-16; di dekat 1000 jaraknya 1,1×10^-13; di dekat 10^16 jaraknya 2. Jarak relatif, jarak dibagi bilangannya, tetap di antara 2^-53 dan 2^-52 di mana-mana, dan itulah arti "sekitar 16 angka". Jarak antara sebuah bilangan dan bilangan berikutnya disebut ulp, unit pada tempat terakhir.

Format dengan sangat sedikit bit memperlihatkannya dengan jelas. Ambil 1 bit tanda, 3 bit eksponen (bias 3), dan 2 bit pecahan. Bilangan tak negatifnya adalah 0, \tfrac116, \tfrac18, \tfrac316, \tfrac14, \tfrac516, \tfrac38, \tfrac716, \tfrac12, \tfrac58, \tfrac34, \tfrac78, 1, \tfrac54, \tfrac32, \tfrac74, 2, …, 14. Di antara \frac14 dan \frac12 ada empat bilangan, berjarak seperempat celah, seperti di antara 1 dan 2, tetapi di sana jaraknya empat kali lebih besar. Bilangan di bawah \frac14, bilangan subnormal, berbagi satu jarak, \frac116, yaitu jarak binade pertama: itulah yang membuat format mencapai 0 secara bertahap, bukan melompat dari \frac14 ke 0.

Dua akibat penting dalam kode. Pertama, bilangan bulat tepat hanya sampai 2^53 pada double (9 007 199 254 740 992) dan sampai 2^24=16 777 216 pada single: di atasnya jaraknya 2, sehingga 2^53+1 dibulatkan kembali ke 2^53. Kedua, bilangan besar menyerap bilangan kecil: 1e16 + 1 == 1e16 bernilai benar.

Pilih banyaknya bit di bawah dan geser melewati bilangannya. Tabel memberi jarak tiap pangkat dua.

Interaktif: semua bilangan pada format mini

Pilih format dengan 2 sampai 4 bit eksponen dan 1 sampai 4 bit pecahan dan lihat semua bilangannya pada satu garis: rapat di dekat nol, dua kali lebih renggang di setiap pangkat dua, dengan bilangan subnormal mengisi celah di sebelah nol.

Bagaimana komputer membulatkan, dan apa itu epsilon mesin?

IEEE 754 mewajibkan + − × ÷ dan akar kuadrat menghasilkan hasil yang dibulatkan dengan tepat: jawaban eksak yang dibulatkan ke bilangan format terdekat, dan pada seri ke yang bit pecahan terakhirnya genap. Pembulatan ke genap terdekat menghindari bias: dengan seri selalu dibulatkan ke atas, perhitungan panjang akan bergeser ke atas. Mode lain (ke arah 0, ke arah +∞, ke arah -∞) ada untuk aritmetika selang dan kasus khusus.

Model standar. Asalkan tidak ada overflow atau underflow, \mathrmfl(a\circ b)=(a\circ b)(1+\delta), |\delta|\le u, dengan \circ operasi apa pun dan u=2^-53≈1,11×10^-16 untuk double adalah pembulatan satuan. Setiap operasi seteliti yang diizinkan format, dan seluruh cerita galat numerik adalah bagaimana galat relatif kecil ini bergabung.

Epsilon mesin. Jarak antara 1 dan double berikutnya adalah 2^-52≈2,22×10^-16, disebut epsilon mesin (di Python sys.float_info.epsilon, di JavaScript Number.EPSILON); ia dua kali pembulatan satuan. Pada single nilainya 2^-23≈1,19×10^-7 dan pada half 2^-10≈9,8×10^-4.

Apa yang tepat. Hasil sebuah operasi tepat bila muat: bilangan bulat sampai 2^53, pecahan dengan pangkat dua di penyebut seperti 0,5 dan 0,375, dan apa pun dikali pangkat dua (kecuali overflow atau underflow). Itulah sebabnya 0,5+0,25=0,75 benar dalam floating point sedangkan 0,1+0,2=0,3 tidak: jumlah 0,1+0,2 adalah 0,30000000000000004, satu ulp di atas double yang terdekat dengan 0,3.

Apa itu tak hingga, NaN, nol negatif, dan bilangan subnormal?

IEEE 754 mencadangkan beberapa pola untuk nilai yang bukan bilangan biasa, sehingga setiap operasi punya jawaban dan perhitungan panjang tidak berhenti pada masalah pertama.

  • Tak hingga. Bidang eksponen berisi satu semua dengan pecahan 0 adalah ±∞. Ia hasil overflow, seperti Number.MAX_VALUE * 10, dan dari membagi bilangan tak nol dengan nol di JavaScript atau C. Python memunculkan ZeroDivisionError untuk 1 / 0.0, tetapi float('inf') ada, dan NumPy mengembalikan inf dengan peringatan.
  • NaN, "bukan bilangan". Bidang eksponen berisi satu semua dengan pecahan tak nol. Ia hasil 0/0, ∞-∞, dan √(-1) (bila ini tidak memunculkan galat). NaN tak terurut: ia tidak sama dengan apa pun, bahkan tidak dengan dirinya sendiri, sehingga x != x adalah uji untuk NaN (atau isnan, Number.isNaN, Object.is), dan setiap perbandingan dengan NaN salah. Ia menular: operasi apa pun dengan NaN menghasilkan NaN.
  • Nol negatif. Tanda 1 dengan sisanya 0. Ia sama dengan 0 dalam perbandingan, tetapi \frac1+0=+∞ dan \frac1-0=-∞, yang menjaga tanda limit yang mendekati 0 dari bawah.
  • Bilangan subnormal. Tanpa mereka, double positif terkecil adalah bilangan normal terkecil 2^-1022≈2,2×10^-308, dengan celah ke 0 jauh lebih besar daripada celah di dekatnya. Bilangan subnormal (bidang eksponen 0) mengisi celah itu secara merata sampai 2^-1074≈4,9×10^-324, sehingga underflow berlangsung bertahap: x-y=0 hanya terjadi ketika x=y.

Overflow dan underflow. Hasil di atas double terbesar, 1,7976931348623157×10^308, dibulatkan ke ∞; hasil di bawah setengah 2^-1074 dibulatkan ke 0. Dalam praktik ini muncul pada hasil kali dan eksponensial, dan obatnya adalah bekerja dengan logaritma, atau menskalakan: panjang vektor √(x^2+y^2) overflow untuk x=10^200 padahal jawabannya tidak, dan hypot(x, y) menghindarinya.

Mengapa galat pembulatan menumpuk, dan bagaimana membatasinya?

Setiap penjumlahan membulatkan, dan galat pembulatan pada jumlah yang panjang tidak saling menghapus: ia menumpuk, dan galat jumlah naif tumbuh kira-kira sebanding dengan banyaknya suku. Jumlahkan 0,1 sepuluh kali pada double, satu per satu, dan hasilnya 0,9999999999999999; pada single hasilnya 1,0000001192092896. Jumlahkan sejuta kali dan double memberi 100000,00000133288, meleset 1,3×10^-6, sekitar 90 000 ulp. Batas untuk n suku sekitar n· u kali jumlah nilai mutlaknya.

Jumlah parsial yang besar juga menyerap suku kecil: begitu totalnya 2^53 jaraknya 2, sehingga menambah 1 tidak mengubah apa pun, berapa kali pun kamu melakukannya.

Tiga obat. 1. Penjumlahan Kahan menyimpan bilangan kedua c yang memuat bagian yang dibulatkan pada penjumlahan terakhir, dan memasukkannya kembali pada penjumlahan berikutnya: y=x_i-c, t=s+y, c=(t-s)-y, s=t. Galatnya lalu tidak lagi tumbuh bersama n: ia tetap dalam satu atau dua ulp. Kompilator yang menyusun ulang aritmetika floating-point dengan bebas dapat menghilangkan kompensasinya, sehingga opsi untuk aritmetika ketat penting. 2. Penjumlahan berpasangan menjumlahkan dalam pohon, sehingga suku-sukunya berukuran mirip dan galat tumbuh seperti \log n, bukan n. NumPy melakukannya. 3. Penjumlahan tepat. math.fsum di Python mengembalikan jumlah nilai eksak yang dibulatkan dengan tepat dengan menyimpan jumlah parsial yang tidak kehilangan apa pun. Ia acuan yang dipakai dalam uji.

Sejak Python versi 3.12 sum bawaan untuk float juga memakai algoritma berkompensasi, sehingga sum([0.1] * 10) memberi 1.0 di sana, sedangkan perulangan biasa tetap memberi 0.9999999999999999.

Cobalah di bawah: pilih nilai, jumlah, dan ketelitian.

Interaktif: galat yang menumpuk

Jumlahkan satu nilai n kali, satu per satu, dengan kompensasi Kahan, dan secara tepat, dalam float32 atau float64, dan lihat galat tumbuh bersama n; atau tambahkan satu-satu pada 2^53 dan perhatikan mereka terserap.

Apa itu pembatalan katastrofik?

Ketika dua bilangan yang hampir sama dikurangkan, angka terdepan saling menghapus dan yang tersisa kebanyakan galat pembulatan lama: hasilnya eksak, tetapi informasi di dalamnya hilang. Ambil dua bilangan hasil hitung a≈1,0000000000000002 dan b≈1. Masing-masing membawa galat relatif sampai 10^-16, dan selisihnya 2,2×10^-16 dihitung tanpa galat baru, tetapi ia sebesar galat yang sudah dimiliki a dan b, sehingga selisihnya mungkin tidak memiliki satu pun angka benar. Galat relatif pengurangan diperbesar sekitar (|a|+|b|)/(|a-b|).

Contoh. - (1-\cos x)/(x^2) untuk x=10^-8: \cos x=1-5×10^-17 dibulatkan ke 1, sehingga rumus naif memberi 0, padahal nilai sebenarnya \frac12. Memakai 1-\cos x=2\sin^2\frac x2 memberi 0,5. - (e^x-1)/(x) untuk x=10^-15: e^x dibulatkan ke 1,000000000000001, dan rumus naif memberi 1,1102230246251565, bukan 1. Fungsi pustaka expm1(x) menghitung e^x-1 langsung, dan memberi 1,0000000000000004 (angka terakhir adalah pembulatan). - √(x^2+1)-x untuk x besar: untuk x=10^8 rumus naif memberi 0; bentuk stabil \dfrac1√(x^2+1)+x memberi 5×10^-9. - Akar kecil x^2-bx+1=0 untuk b=10^8: (b-√(b^2-4))/(2) memberi 7,45×10^-9, sedangkan bentuk stabil (2)/(b+√(b^2-4)) memberi 10^-8 yang benar.

Obatnya hampir tidak pernah ketelitian lebih besar: ia menghindari pengurangan dengan aljabar (sekawan, identitas), dengan deret untuk x kecil, atau dengan fungsi pustaka yang dibuat untuknya (expm1, log1p, hypot). Pembatalan juga menjelaskan mengapa hasil bagi selisih dengan h sangat kecil pada diferensiasi numerik justru memburuk, bukan membaik, di bawah h≈10^-8.

Geser x pada tiap empat kasus di bawah dan baca banyaknya angka benar.

Interaktif: ketika pengurangan menghancurkan angka

Empat ungkapan yang ditulis dengan dua cara, yang lazim dan yang stabil: geser x dan lihat berapa angka benar yang dipertahankan tiap bentuk ketika pengurangan menghapus angka terdepan.

Bagaimana membandingkan dua bilangan floating-point?

Bandingkan float hasil hitung dengan toleransi yang memiliki bagian relatif dan bagian mutlak: |a-b|\le\max\left(rel·\max(|a|,|b|), abs\right). Inilah yang dilakukan math.isclose(a, b, rel_tol=1e-09, abs_tol=0.0) di Python, dan yang kamu tulis sendiri di JavaScript dan C++.

  • Toleransi mutlak murni seperti 10^-9 salah untuk bilangan besar: di dekat 10^16 dua tetangga berjarak 2, sehingga tidak ada dua bilangan berbeda yang berjarak kurang dari 10^-9, dan di dekat 10^-12 semuanya "sama".
  • Toleransi relatif murni salah di dekat nol: 10^-300 dan 0 tidak dekat secara relatif, padahal keduanya "nol" bagi soalnya. Jadi beri bagian mutlak, sekecil yang dibenarkan skala datanya, bila nilai sebenarnya dapat nol.
  • Bagian relatif sebaiknya beberapa kali pembulatan satuan dikali banyaknya operasi: 10^-9 pilihan umum untuk double, dan sekitar 10^-5 untuk single.
  • ulp. Dalam uji kode numerik jarak dalam ulp, banyaknya float di antara kedua bilangan, adalah ukuran yang menyesuaikan dengan besarnya: 0.1 + 0.2 dan 0.3 berjarak 1 ulp.
  • Jangan pernah mengulang berdasarkan kesamaan. while x != 1.0: x += 0.1 tidak pernah berhenti, karena x melangkahi 1,0 dan terus berjalan. Hitung dengan bilangan bulat, atau ulangi selama x < 1.0 - 1e-9.
  • NaN tidak pernah dekat dengan apa pun, termasuk dirinya; ujilah secara terpisah.

Uang. Harga bukan bilangan real: ia bilangan bulat dari satuan terkecil. Simpan sen, atau pakai tipe desimal (decimal.Decimal di Python, BigDecimal di Java, decimal di C#), tempat 0,1+0,2=0,3 berlaku tepat karena basisnya 10.

Tipe apa yang dipakai: float16, float32, float64, atau desimal?

Pakai double kecuali ada alasan untuk tidak; pakai ketelitian lebih rendah bila memori dan kecepatan lebih penting daripada angka, dan tipe eksak bila jawabannya harus eksak.

  • float64 (double): bawaan untuk sains dan rekayasa dan untuk kebanyakan program. Sekitar 16 angka dan rentang sampai 10^308.
  • float32 (single): grafika, permainan, dan GPU, tempat 7 angka cukup dan separuh memori melipatduakan lajunya. Hati-hati dengan jumlah banyak suku dan dengan bilangan bulat di atas 2^24 (ia mencacah dari 16 777 216 ke atas dengan langkah 2).
  • float16 dan bfloat16: pembelajaran mesin. Half punya ketelitian 11 bit dan maksimum 65504; bfloat16 mempertahankan rentang single (8 bit eksponen) dengan ketelitian hanya 8 bit, yang cocok untuk rentang dinamis besar pelatihan jaringan saraf. Keduanya dipakai dengan hati-hati, sering dengan akumulasi dalam float32.
  • Desimal, bilangan bulat, pecahan: uang, hitungan, dan rasio eksak. fractions.Fraction dan decimal.Decimal di Python eksak untuk bilangan yang dapat mereka muat tetapi lebih lambat.
  • Ketelitian sembarang (mpmath, BigDecimal, GMP): ketika perhitungan memerlukan lebih banyak angka daripada yang dimiliki double, dengan biaya kecepatan.

Dua kegagalan terkenal. Pada Februari 1991 baterai rudal Patriot di Dhahran gagal mencegat sebuah Scud. Jamnya menghitung persepuluh detik dalam register 24 bit, dan 0,1 tidak eksak dalam biner: galatnya sekitar 0,000000095 detik per detakan, yang setelah sekitar 100 jam operasi menjadi sekitar 0,34 detik, cukup agar Scud, yang menempuh lebih dari 1,6 km setiap detik, berada setengah kilometer dari tempat sistem mencarinya. Pada Juni 1996 roket Ariane 5 pertama hilang sekitar 37 detik setelah peluncuran ketika sebuah nilai floating-point 64 bit diubah menjadi bilangan bulat bertanda 16 bit yang terlalu kecil untuknya: overflow, bukan galat pembulatan, tetapi dalam keluarga yang sama, konversi yang tidak pernah diperiksa.

Bagaimana memeriksa bilangan floating-point dalam kode?

Setiap bahasa memungkinkanmu melihat bit dan jaraknya: Python dengan struct, math.ulp, dan decimal.Decimal, JavaScript dengan DataView, dan C++ dengan perulangan yang mencari epsilon mesin. Program di bawah menunjukkan pola bit 0,1 dan -2,5, nilai tepat 0,1, epsilon dan ulp, tetangga 0.1 + 0.2, tiga jenis jumlah, penyerapan, perbandingan dengan toleransi, pembatalan, dan NaN.

Python: bit, jarak, jumlah, toleransi, dan pembatalan
import math
import struct
import sys
from decimal import Decimal


def bits(x):
    """The 64 bits of a double as sign, exponent and fraction."""
    b = format(struct.unpack('>Q', struct.pack('>d', x))[0], '064b')
    return b[0], b[1:12], b[12:]


def naive(xs):
    total = 0.0
    for x in xs:
        total += x
    return total


def kahan(xs):
    total = comp = 0.0
    for x in xs:
        y = x - comp
        t = total + y
        comp = (t - total) - y
        total = t
    return total


print(bits(0.1))
print(bits(-2.5))
print(Decimal(0.1))
print(sys.float_info.epsilon, math.ulp(1.0), math.ulp(2.0 ** 53))
print(math.nextafter(0.1 + 0.2, 0.0) == 0.3)
print(naive([0.1] * 10), kahan([0.1] * 10), math.fsum([0.1] * 10))
print(naive([0.1] * 1000000), kahan([0.1] * 1000000), math.fsum([0.1] * 1000000))
print(1e16 + 1 == 1e16, 2.0 ** 53 + 1 == 2.0 ** 53)
print(math.isclose(0.1 + 0.2, 0.3), 0.1 + 0.2 == 0.3)
x = 1e-8
print((1 - math.cos(x)) / x ** 2, 2 * math.sin(x / 2) ** 2 / x ** 2)
print((math.exp(1e-15) - 1) / 1e-15, math.expm1(1e-15) / 1e-15)
print(float('nan') == float('nan'), math.isnan(float('nan')), float('inf') - float('inf'))
print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3'))
# ('0', '01111111011', '1001100110011001100110011001100110011001100110011010')
# ('1', '10000000000', '0100000000000000000000000000000000000000000000000000')
# 0.1000000000000000055511151231257827021181583404541015625
# 2.220446049250313e-16 2.220446049250313e-16 2.0
# True
# 0.9999999999999999 1.0 1.0
# 100000.00000133288 100000.0 100000.0
# True True
# True False
# 0.0 0.5
# 1.1102230246251565 1.0000000000000004
# False True nan
# True
JavaScript: DataView, EPSILON, dan nilai khusus
const bits = (x) => {
  const v = new DataView(new ArrayBuffer(8))
  v.setFloat64(0, x)
  return v.getBigUint64(0).toString(2).padStart(64, '0')
}
bits(1)                                  // '0011111111110000000000000000000000000000000000000000000000000000'
bits(0.1)                                // '0011111110111001100110011001100110011001100110011001100110011010'
Number.EPSILON                           // 2.220446049250313e-16
;[Math.fround(0.1), 0.1]                 // [0.10000000149011612, 0.1]: the nearest single to 0.1
;[2 ** 53 + 1 === 2 ** 53, 2 ** 53 + 2]  // [true, 9007199254740994]
;[0 / 0, 1 / 0, 1 / -0]                  // [NaN, Infinity, -Infinity]
;[NaN === NaN, Object.is(NaN, NaN)]      // [false, true]
;[Object.is(0, -0), 0 === -0]            // [false, true]
;[Number.MAX_VALUE * 10, 5e-324 / 2]     // [Infinity, 0]: overflow, and underflow past the smallest subnormal
;[0.1 + 0.2, 0.1 + 0.2 === 0.3]          // [0.30000000000000004, false]
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true: a tolerance of one machine epsilon is fine near 1
;[Math.fround(16777217), 16777217]       // [16777216, 16777217]: a single cannot hold 2^24 + 1
let s32 = 0
for (let i = 0; i < 10; i++) s32 = Math.fround(s32 + Math.fround(0.1))
s32                                      // 1.0000001192092896: ten tenths in single precision
C++: epsilon mesin, sebuah jumlah, dan penyerapan
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;

int main() {
  double eps = 1;
  int count = 0;
  while (1 + eps / 2 > 1) {                              // halve until 1 + eps/2 is rounded back to 1
    eps = eps / 2;
    count = count + 1;
  }
  printf("%d %.17f\n", count, eps);                      // 52 halvings: eps = 2^-52
  double s = 0;
  for (int i = 0; i < 10; i++) s = s + 0.1;
  printf("%.17f %d\n", s, s == 1.0);                     // ten times 0.1 is not 1
  double big = 9007199254740992.0;                      // 2^53
  printf("%d %d\n", big + 1 == big, big + 2 == big);     // 1 0: the 1 is absorbed, the 2 is a whole gap
  return 0;
}

/* output:
52 0.00000000000000022
0.99999999999999989 0
1 0
*/

Dari mana floating-point berasal?

Floating-point lebih tua daripada komputer elektronik, dan selama puluhan tahun setiap mesin memiliki formatnya sendiri sampai sebuah standar membuat program dapat dipindahkan.

  • 1914. Leonardo Torres Quevedo mengusulkan aritmetika floating-point untuk kalkulator mekanis dalam Essays on Automatics.
  • 1930-an–1941. Mesin Konrad Zuse, Z1 dan Z3, memakai floating-point biner, dengan tanda, eksponen, dan pecahan.
  • 1954. IBM 704 memiliki aritmetika floating-point di perangkat keras, dan sepanjang 1960-an dan 1970-an setiap pembuat memilih basis, ukuran, dan pembulatannya sendiri, sehingga program yang sama memberi jawaban berbeda di komputer berbeda.
  • 1980-an. William Kahan, yang pernah mengerjakan koprosesor 8087 Intel, memimpin panitia yang menulis IEEE 754, terbit pada 1985: format, pembulatan tepat, tak hingga, NaN, dan underflow bertahap. Kahan menerima Penghargaan Turing pada 1989.
  • 1994. Galat FDIV Pentium, beberapa entri yang salah pada tabel yang dipakai pembagi, memberi hasil yang salah pada angka signifikan keempat atau kelima atau lebih jauh, dan menunjukkan bahwa perangkat keras floating-point harus diuji secermat perangkat lunak; Intel menarik kembali cip itu.
  • 2008 dan 2019. Standar direvisi: fused multiply-add (menghitung a× b+c dengan satu pembulatan), format desimal, half precision sebagai format pertukaran, dan aturan yang lebih jelas untuk NaN dan untuk operasi seperti minimum dan maksimum.
  • 2010-an. bfloat16, dengan rentang eksponen single dan ketelitian hanya 8 bit, dirancang untuk perangkat keras pembelajaran mesin, dan half precision menjadi umum pada GPU.

Kesalahan apa yang sering terjadi pada bilangan floating-point?

Kesalahan paling umum pada bilangan floating-point adalah sembilan hal berikut, masing-masing dengan pernyataan yang benar.

KesalahanYang benar
❌ Menguji float hasil hitung dengan ==Bandingkan dengan toleransi relatif dan mutlak, atau dengan jarak dalam ulp.
❌ Float berjarak samaJaraknya berlipat dua di setiap pangkat dua: 2^-52 di dekat 1, 2 di dekat 2^53.
❌ Float memuat semua bilangan bulatDouble tepat hanya sampai 2^53 dan single sampai 2^24.
❌ Float punya 16 angka setelah titikIa punya sekitar 16 angka signifikan, di mana pun titiknya.
❌ Menambah bilangan kecil ke yang besar dan memercayainyaIa dapat terserap: 1e16 + 1 == 1e16.
❌ Memakai ketelitian lebih besar untuk memperbaiki penguranganTulis ulang rumusnya agar bilangan yang hampir sama tidak dikurangkan.
❌ Mengulang dengan while x != 1.0Float melangkahi nilainya: hitung dengan bilangan bulat.
❌ NaN sama dengan NaNNaN tidak sama dengan apa pun, termasuk dirinya; pakai isnan.
❌ Memakai float untuk uangSimpan sen sebagai bilangan bulat atau pakai tipe desimal.

Latihan: uji pemahamanmu

Soal-soal ini mencampur semua yang dibahas di atas. Jika jawabanmu belum tepat, perhatikan petunjuk yang tersedia, lalu coba kembali.

Benar atau salah?

Tentukan tiap pernyataan Benar atau Salah.

  • Double menyimpan 52 bit pecahan tetapi memiliki ketelitian 53 bit.
  • Jarak antara double bertetangga sama di mana-mana.
  • NaN sama dengan dirinya sendiri.
  • -0 dan +0 sama dalam perbandingan, tetapi \frac1-0=-∞.
  • Mengurangkan dua bilangan yang hampir sama dapat kehilangan hampir semua angka benar.
  • Float adalah tipe yang baik untuk sejumlah uang.

Pilih semua yang benar

Mana dari ungkapan berikut yang benar untuk double? Pilih semua yang benar.

  • 2.0 ** 53 + 1 == 2.0 ** 53
  • 0.5 + 0.25 == 0.75
  • 0.1 + 0.2 == 0.3
  • 1e16 + 1 == 1e16
  • float('nan') == float('nan')

Bias

Berapa bidang eksponen E dari double 1,0, sebagai bilangan desimal?

Eksponen dari 8

Berapa bidang eksponen E dari double 8,0?

Berapa angka?

Kira-kira berapa angka desimal signifikan yang dimuat sebuah single (ketelitian 24 bit)?

  • sekitar 7, karena 24\log_102≈7,2
  • sekitar 16
  • sekitar 3
  • tepat 24

Bilangan bulat pertama yang hilang

Berapa bilangan bulat positif terkecil yang tidak dapat disimpan tepat oleh float presisi tunggal?

Penyerapan

Pada double, berapa (2.0 ** 53 + 1) + 1?

  • 2^53: setiap +1 terserap, karena jaraknya 2
  • 2^53+2
  • 2^53+1
  • NaN

Bilangan subnormal

Apa yang dilakukan bilangan subnormal?

  • mereka mengisi celah antara 0 dan bilangan normal terkecil, sehingga underflow bertahap
  • mereka menyimpan bilangan yang lebih besar daripada maksimum
  • mereka menyatakan tak hingga
  • mereka adalah bilangan negatif

Sebuah toleransi

Dengan math.isclose dan rel_tol=1e-9, abs_tol=0, dua bilangan berukuran 10^6 dekat jika berbeda paling banyak berapa?

Memperbaiki pengurangan

Untuk x besar, sqrt(x*x + 1) - x kehilangan semua angkanya. Bentuk mana yang teliti?

  • (1)/(√(x^2+1)+x), nilai yang sama tanpa pengurangan
  • pakai float32 bukan float64
  • √(x^2+1)-x+0
  • bulatkan x menjadi bilangan bulat

Tipe apa?

Mana cara terbaik menyimpan harga seperti 19,99 pada program toko?

  • sebagai bilangan bulat 1999 (sen), atau dengan tipe desimal
  • sebagai float64, karena ia memiliki 16 angka
  • sebagai float32 untuk menghemat memori
  • sebagai string, selalu

Epsilon mesin

Epsilon mesin sebuah double adalah 2^k. Berapa k?

Ringkasan: bilangan floating-point sekilas

  • Format: (-1)^s×(1.f)_2×2^e; double adalah 1+11+52 bit, bias 1023, ketelitian 53 bit, sekitar 16 angka.
  • Jarak: jaraknya berlipat dua di setiap pangkat dua; bilangan bulat tepat sampai 2^53 (single: 2^24).
  • Pembulatan: dibulatkan tepat, seri ke genap, galat relatif paling banyak u=2^-53; epsilon mesin 2^-52.
  • Nilai khusus: ±0, ±∞, NaN (tidak sama dengan dirinya), bilangan subnormal untuk underflow bertahap.
  • Galat: menumpuk pada jumlah, terserap oleh bilangan besar, dan pembatalan menghancurkan angka pada pengurangan bilangan yang hampir sama.
  • Obat: jumlah Kahan atau berpasangan atau tepat, penulisan ulang aljabar, expm1, log1p, hypot.
  • Bandingkan: dengan toleransi relatif dan mutlak, atau dalam ulp; jangan pernah mengulang dengan ==.
  • Pilih: float64 sebagai bawaan, float32 atau lebih rendah untuk memori dan kecepatan, bilangan bulat atau desimal untuk uang.
  • Kode: struct, math.ulp, math.fsum, math.isclose, decimal di Python; DataView dan Number.EPSILON di JavaScript.

Glosarium: istilah penting

Bilangan floating-point
Bilangan yang disimpan sebagai tanda, eksponen, dan sejumlah tetap bit signifikan, sehingga titiknya mengambang menurut eksponen.
IEEE 754
Standar yang mendefinisikan format floating-point biner, pembulatan tepat, tak hingga, NaN, dan underflow bertahap yang dipakai hampir semua perangkat keras.
Epsilon mesin
Jarak antara 1 dan bilangan berikutnya dari sebuah format: 2 pangkat minus 52 untuk double.
Ulp
Unit pada tempat terakhir: jarak antara sebuah bilangan floating-point dan bilangan berikutnya.
Bilangan subnormal
Bilangan di bawah yang normal terkecil, disimpan dengan bidang eksponen nol dan tanpa angka 1 di depan yang tersirat, yang membuat underflow bertahap.
NaN
Bukan bilangan: hasil operasi seperti nol per nol, tidak sama bahkan dengan dirinya sendiri.
Penyerapan
Hilangnya bilangan kecil ketika ditambahkan ke bilangan yang jauh lebih besar, karena jumlahnya dibulatkan kembali ke bilangan yang lebih besar.
Pembatalan katastrofik
Hilangnya sebagian besar angka benar ketika dua bilangan yang hampir sama dikurangkan, memperlihatkan galat pembulatan yang sudah mereka miliki.

Pertanyaan yang sering diajukan

Apa itu bilangan floating-point?

Itu adalah bilangan yang disimpan dalam notasi ilmiah biner: tanda, eksponen, dan sejumlah tetap bit pecahan. Eksponen menggeser titik biner, yang memberi rentang sangat besar, sedangkan jumlah bit tetap membatasi ketelitian.

Apa beda float dan double?

Float biasanya berarti single 32 bit dengan ketelitian 24 bit, sekitar 7 angka desimal, dan double adalah bilangan 64 bit dengan ketelitian 53 bit, sekitar 16 angka. Bilangan Python dan JavaScript adalah double.

Bagaimana bit sebuah double disusun?

Satu bit tanda, sebelas bit eksponen dengan bias 1023, dan lima puluh dua bit pecahan dengan angka 1 di depan yang tersirat. Bidang eksponen berisi nol semua dan satu semua dicadangkan untuk nol, bilangan subnormal, tak hingga, dan NaN.

Apa itu epsilon mesin?

Itu adalah jarak antara 1 dan bilangan berikutnya yang dapat disimpan sebuah format: 2 pangkat minus 52, sekitar 2,2 kali sepuluh pangkat minus 16, untuk double. Ia mengukur ketelitian relatif format itu.

Mengapa NaN tidak sama dengan dirinya sendiri?

IEEE 754 mendefinisikan setiap perbandingan dengan NaN sebagai salah, karena NaN menyatakan tiadanya nilai, dan dua nilai yang hilang tidak harus sama. Jadi menguji x tidak sama dengan x adalah cara mendeteksi NaN, seperti juga fungsi isnan.

Apa itu bilangan subnormal?

Itu adalah bilangan yang lebih kecil daripada bilangan normal terkecil, disimpan dengan bidang eksponen nol dan tanpa angka 1 di depan yang tersirat. Mereka mengisi celah di sebelah nol secara merata, sehingga underflow bertahap dan mengurangkan dua bilangan berbeda tidak pernah memberi nol.

Apa itu pembatalan katastrofik?

Itu adalah hilangnya sebagian besar angka benar ketika dua bilangan yang hampir sama dikurangkan. Angka terdepan saling menghapus dan hasilnya didominasi galat pembulatan yang sudah dimiliki bilangan itu, sehingga rumusnya harus ditulis ulang untuk menghindari pengurangan.

Tipe floating-point mana yang harus saya pakai?

Pakai double 64 bit sebagai bawaan. Pakai float 32 bit untuk grafika dan ketika memori penting, dan format 16 bit untuk pembelajaran mesin. Untuk uang pakai bilangan bulat atau tipe desimal, karena float biner tidak dapat memuat jumlah seperti 0,10 dengan tepat.

Referensi

  1. IEEE (2019). IEEE Standard for Floating-Point Arithmetic (IEEE 754-2019). Institute of Electrical and Electronics Engineers.
  2. David Goldberg (1991). What Every Computer Scientist Should Know About Floating-Point Arithmetic. ACM Computing Surveys 23(1).
  3. Nicholas J. Higham (2002). Accuracy and Stability of Numerical Algorithms (2nd ed.). SIAM.
  4. William Kahan (1965). Further remarks on reducing truncation errors. Communications of the ACM 8(1).