Skip to content
Kembali ke Blog
Tutorial

Presisi Floating Point: Mengapa 0.1 + 0.2 ≠ 0.3

Mengapa presisi floating point membuat 0.1 + 0.2 meleset, cara IEEE 754 membulatkan, dan cara membandingkan float dengan aman — konverter online gratis.

12 menit membaca

Presisi Floating Point: Mengapa 0.1 + 0.2 ≠ 0.3

0.1 + 0.2 menghasilkan 0.30000000000000004 karena baik 0.1 maupun 0.2 sama sekali tidak ada di dalam bilangan floating-point biner. Sebuah double hanya sanggup menampung nilai berbentuk m × 2ⁿ, sedangkan sepersepuluh adalah pecahan berulang tak hingga dalam basis 2, jadi perangkat keras menyimpan tetangga terdekat yang bisa diwakili. Double di balik 0.1 persisnya adalah 0.1000000000000000055511151231257827021181583404541015625. Yang di balik 0.2 adalah 0.200000000000000011102230246251565404236316680908203125. Jumlahkan kedua nilai tersimpan itu, dan hasil persisnya jatuh di antara dua double yang dapat diwakili. IEEE 754 membulatkan ke tetangga yang lebih dekat, dan tetangga itu duduk setipis rambut di atas 0.3.

Itulah seluruh jawabannya. Presisi floating point selalu terbatas: pecahan desimal jarang pas di kisi biner, lalu setiap operasi membulatkan sekali lagi.

Ini bukan keanehan JavaScript, bukan pula cacat CPU. Hasil yang sama muncul di Python, Java, C, Go, Rust, Swift, dan kolom FLOAT milik SQL, karena semuanya berjalan di atas IEEE 754. Tempelkan nilai apa pun ke konverter IEEE 754 untuk melihat bit persisnya dan nilai tersimpannya, digit demi digit.

Sisanya menyangkut sebab galat itu, alasan bahasa Anda kadang menyembunyikannya, cara membandingkan float tanpa ==, dan tipe numerik yang sebaiknya Anda pilih ketika presisi menyangkut uang.

Presisi Floating Point dalam 60 Detik

Tiga fakta menjelaskan hampir semua kejutan:

  1. Floating point biner hanya mampu mewakili bilangan berbentuk m × 2ⁿ, yaitu penjumlahan dari pangkat-pangkat dua.
  2. Pecahan desimal seperti 0.1, 0.2, dan 0.3 tidak berbentuk demikian, jadi nilainya sudah membulat sejak masuk.
  3. Setiap operasi aritmetika kembali membulatkan hasilnya ke nilai terdekat yang dapat diwakili.
DesimalBisa diwakili persis?Alasannya
0.5Ya2⁻¹
0.25Ya2⁻²
0.75Ya2⁻¹ + 2⁻²
2.5Ya2 + 2⁻¹
100YaSemua bilangan bulat di bawah 2⁵³
0.1Tidak1/10 — penyebutnya punya faktor 5
0.2Tidak1/5 — masalah yang sama
0.3Tidak3/10 — masalah yang sama

Patokan praktisnya: sederhanakan pecahan itu, dan kalau penyebutnya murni pangkat dua, nilainya eksak. Faktor 5 yang masih tersisa berarti nilainya hanya tersimpan sebagai hampiran.

Mengapa 0.1 Tidak Bisa Tersimpan Persis

Bit-bit setelah titik biner membawa bobot 1/2, 1/4, 1/8, 1/16, 1/32, dan seterusnya. Coba susun 0.1 dari bobot-bobot itu; Anda tidak akan pernah tepat mendarat di sana. Anda dapat 1/16 = 0.0625, tambah 1/32 = 0.09375, tambah 1/256 = 0.09765625, makin dekat setiap kali, tetapi tidak pernah persis. Dalam biner, sepersepuluh adalah 0.0001100110011… dengan 0011 berulang selamanya.

Desimal punya masalah serupa, hanya pada pecahan yang berbeda. Menulis 1/3 dalam basis 10 memberi 0.333… dan tidak ada rangkaian digit berhingga yang benar-benar mengenainya. Tidak ada yang menyebut itu bug pada sistem desimal. Basis 2 sekadar menarik garisnya di tempat lain, dan 1/10 kebetulan jatuh di sisi yang salah. Dokumentasi Python, Floating Point Arithmetic: Issues and Limitations, menelusuri ekspansi yang sama kalau Anda ingin versi panjangnya.

Biner hanyalah basis 2, dan aritmetika posisional yang sama juga menggerakkan heksadesimal dan oktal. Konverter basis bilangan memperlihatkan bagaimana sebuah nilai berpindah antarbasis, dan panduan konversi basis bilangan kami mengupas sisi bilangan bulatnya secara mendalam. Pecahanlah yang membuat basis 2 mulai terasa merepotkan.

Apa yang sebenarnya disimpan sebuah double

Double 64-bit terbagi menjadi tiga medan: 1 bit tanda, 11 bit eksponen, dan 52 bit mantissa. Mantissa membawa angka 1 di depan yang bersifat implisit dan tidak pernah ikut tersimpan, sehingga Anda mendapat 53 bit signifikan, kira-kira 15.95 digit desimal presisi floating point. Eksponennya tersimpan dengan bias 1023.

Minta digit lebih banyak daripada yang biasa dicetak sebuah bahasa. Hampirannya langsung menyembul:

>>> 0.1
0.1
>>> f"{0.1:.20f}"
'0.10000000000000000555'
>>> from decimal import Decimal
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
(0.1).toFixed(20);      // '0.10000000000000000555'
(0.1).toPrecision(20);  // '0.10000000000000000555'

Decimal(0.1) memberi tampilan yang jujur: ia mengonversi double yang sudah Anda pegang, tanpa membulatkan ulang, lalu mencetak seluruh 55 digitnya. Panel akurasi pada konverter IEEE 754 mencetak ekspansi yang sama untuk angka apa pun yang Anda ketik, berdampingan dengan galat pembulatan bertanda terhadap nilai yang Anda masukkan.

Aritmetika Persis di Balik 0.30000000000000004

Kenapa hasilnya justru angka itu, bukan meleset-dekat yang lain?

Jumlahkan kedua nilai tersimpan tadi secara persis, tanpa pembulatan sama sekali. Hasilnya:

0.1 →  0.1000000000000000055511151231257827021181583404541015625
0.2 →  0.200000000000000011102230246251565404236316680908203125
sum →  0.3000000000000000166533453693773481063544750213623046875

Jumlah itu sendiri bukan double yang dapat diwakili. Nilainya duduk di antara dua tetangga pada kisi biner:

below:  0.299999999999999988897769753748434595763683319091796875   (prints as 0.3)
above:  0.3000000000000000444089209850062616169452667236328125     (prints as 0.30000000000000004)

Kalau jaraknya Anda ukur, muncul sesuatu yang tidak lazim. Jumlah persis itu berada 2⁻⁵⁵2.776e-17 di atas tetangga bawah dan 2⁻⁵⁵ di bawah tetangga atas, jadi tidak lebih dekat ke salah satu pun: persis di tengah, seri sempurna.

Pembulatan ke terdekat tidak punya selisih jarak untuk dipakai di sini, jadi IEEE 754 memakai aturan pemecah serinya: round-half-to-even, yaitu pilih kandidat yang bit mantissa terakhirnya 0. Signifikan tetangga bawah adalah 5404319552844595, ganjil. Milik tetangga atas 5404319552844596, genap. Nilai genap menang, menghasilkan pola bit 0x3FD3333333333334, satu ULP di atas double yang dipetakan dari literal 0.3, dan tercetak sebagai 0.30000000000000004.

Kenapa harus genap? Selalu membulatkan setengah ke atas akan membuat penjumlahan panjang condong ke satu arah. Berganti-ganti menuju tetangga genap menjaga penyimpangan itu tetap mendekati nol sepanjang banyak operasi. Prinsipnya sama dengan banker’s rounding (pembulatan bankir) yang dipakai para akuntan, hanya saja standar ini menanamkannya di dalam silikon, dan itulah alasan langsung mengapa galat pembulatan floating point paling terkenal di internet berakhir dengan ...04.

Bandingkan dengan penjumlahan yang sama sekali tidak butuh pembulatan:

0.5 + 0.25 === 0.75;   // true
0.1 + 0.2 === 0.3;     // false

0.5, 0.25, dan 0.75 adalah 2⁻¹, 2⁻², dan 2⁻¹ + 2⁻². Ketiganya eksak, jumlahnya eksak, dan uji kesamaan berperilaku persis seperti yang diajarkan aritmetika sekolah. Panel tetangga di konverter menampilkan nilai representabel sebelum dan sesudah angka yang Anda masukkan, lengkap dengan jarak ULP-nya, jadi Anda bisa memeriksa sendiri kisi di sekitar 0.3.

Mengapa Bahasa Anda Kadang Menyembunyikan Galat Ini

Di sinilah topik ini mulai terasa ganjil: 0.1 tercetak sebagai 0.1, tetapi 0.1 + 0.2 tercetak dengan tujuh belas digit. Format penyimpanannya sama, keluarannya jauh berbeda.

Runtime modern memakai pemformatan shortest round-trip (round-trip terpendek): cetak string desimal terpendek yang bila terurai kembali menghasilkan double yang identik. "0.1" sudah menempuh round-trip secara unik ke nilai yang tersimpan untuk 0.1, jadi itulah yang Anda lihat. Jumlah 0.1 + 0.2 adalah double yang berbeda dari yang dipetakan "0.3", sehingga pemformat harus terus menambah digit sampai stringnya tidak lagi ambigu, dan itu menelan seluruh tujuh belas digit. Karya David Gay pada 1990 tentang pembulatan yang benar memulai garis keturunan ini; Grisu lalu Ryu membuatnya cukup cepat untuk setiap pustaka standar. Yang Anda lihat adalah label terpendek yang secara unik menandai nilai tersebut.

Perilaku per bahasa

BahasaTipe float default0.1 + 0.2 tercetakOpsi desimal eksak
JavaScriptnumber (hanya double)0.30000000000000004Tidak ada bawaan — sen bilangan bulat atau pustaka
Pythonfloat (double)0.30000000000000004decimal.Decimal, fractions.Fraction
Javadouble0.30000000000000004BigDecimal (bangun dari string)
C#double0.30000000000000004decimal — 128-bit, basis 10
Gofloat640.30000000000000004 (lewat variabel)math/big.Rat
Rustf640.30000000000000004crate rust_decimal
SQLFLOAT / DOUBLE PRECISION0.30000000000000004NUMERIC / DECIMAL

Baris Go dan baris Java menyimpan jebakan.

Go melipat konstanta pada presisi sembarang. Kompilernya mengevaluasi ekspresi literal secara persis pada waktu kompilasi dan baru sesudah itu mengonversinya, sehingga konstanta 0.1 + 0.2 menjadi persis 0.3 sebelum sempat berubah menjadi float64:

package main

import "fmt"

func main() {
	fmt.Println(0.1 + 0.2) // 0.3   — constant folded exactly, then converted
	a, b := 0.1, 0.2
	fmt.Println(a + b)      // 0.30000000000000004
	fmt.Println(a+b == 0.3) // false
}

BigDecimal milik Java mewarisi galat itu kalau Anda menyuapinya sebuah double. Konstruktornya dengan setia mengonversi bit apa pun yang ia terima:

System.out.println(new BigDecimal(0.1));
// 0.1000000000000000055511151231257827021181583404541015625

System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
// 0.3

SQL membelah persoalan ini menurut tipe kolom, bukan menurut dialek:

-- PostgreSQL: a bare decimal literal is NUMERIC, which is exact
SELECT 0.1 + 0.2 = 0.3;                          -- t

-- Cast to binary floating point and equality fails
SELECT 0.1::float8 + 0.2::float8 = 0.3::float8;  -- f

-- SQLite: REAL is a double, and the printed value hides it
SELECT 0.1 + 0.2;        -- 0.3
SELECT 0.1 + 0.2 = 0.3;  -- 0  (false)

Perhatikan pasangan SQLite itu: hasil cetaknya bilang 0.3, perbandingannya bilang sebaliknya, dan keduanya sama-sama benar.

Membandingkan Float: Mengapa == Gagal dan Number.EPSILON Bukan Toleransi

0.1 + 0.2 === 0.3 bernilai false karena ruas kirinya adalah double yang berbeda. Saran yang biasa muncul: bandingkan dengan toleransi. Sayangnya, versi yang paling sering diulang dari saran itu justru keliru:

Math.abs(a - b) < Number.EPSILON;   // ⚠️ not a general-purpose comparison

Number.EPSILON bernilai 2.220446049250313e-16, yaitu 2⁻⁵². MDN mendefinisikannya sebagai selisih antara 1 dan double terkecil yang lebih besar dari 1. Baca sekali lagi: itu jarak kisi di titik 1.0, bukan anggaran galat universal. Jarak antar-float berlipat dua di setiap pangkat dua, jadi ambang yang konstan akan salah di mana-mana kecuali di lingkungan tempat orang mengukurnya.

Di dekat 1.0 kebetulan cara itu berhasil:

Math.abs((0.1 + 0.2) - 0.3);                  // 5.551115123125783e-17
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON; // true

Naikkan skala perhitungan yang sama satu miliar kali, dan cara itu runtuh:

const a = (0.1 + 0.2) * 1e9;
const b = 0.3 * 1e9;

a === b;                          // false
Math.abs(a - b);                  // 5.960464477539063e-8
Math.abs(a - b) < Number.EPSILON; // false — yet a and b are adjacent doubles

Di sekitar 1e9, jarak antara dua double bertetangga kira-kira 1.19e-7. Karena 1e9 jatuh di [2²⁹, 2³⁰), ULP di sana adalah 2²⁹⁻⁵² = 2⁻²³ ≈ 1.19e-7, sekitar 500 juta kali lebih besar daripada Number.EPSILON. Galat pembulatan sungguhan pada besaran itu jauh melampaui ambangnya, jadi pemeriksaan tersebut selamanya mengembalikan false. Turun ke 1e-20, konstanta yang sama justru terlalu longgar dan menyatakan dua nilai yang jelas berbeda sebagai sama.

Toleransi absolut, relatif, dan ULP

Masing-masing menangani situasi yang berbeda:

  • Toleransi absolut, |a − b| <= atol, tepat ketika Anda sudah tahu besarannya di muka. Ini juga satu-satunya yang berfungsi terhadap nol, karena toleransi relatif di sekitar 0 selalu bernilai 0.
  • Toleransi relatif, |a − b| <= rtol × max(|a|, |b|), ikut menyesuaikan diri dengan data lintas besaran, tetapi merosot di dekat nol.
  • Jarak ULP menghitung berapa banyak nilai representabel yang duduk di antara dua pola bit. Paling presisi, tetapi paling sulit dibaca.

Gabungkan dua yang pertama untuk mendapat bentuk yang dipakai setiap pustaka numerik serius:

function nearlyEqual(a, b, rtol = 1e-9, atol = 1e-12) {
  if (a === b) return true;            // handles Infinity === Infinity
  const diff = Math.abs(a - b);
  return diff <= Math.max(rtol * Math.max(Math.abs(a), Math.abs(b)), atol);
}

nearlyEqual(0.1 + 0.2, 0.3);                  // true
nearlyEqual((0.1 + 0.2) * 1e9, 0.3 * 1e9);    // true
nearlyEqual(0, 1e-15);                        // true
nearlyEqual(1, 1.0001);                       // false

Python menyediakannya langsung di pustaka standar, dan allclose milik NumPy memakai rumus yang sama:

import math
math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9, abs_tol=1e-12)  # True
math.isclose(1e9, 1e9 + 1e-7, rel_tol=1e-9)                # True

Untuk versi yang paling ketat, petakan bit ke urutan bilangan bulat yang monoton lalu kurangkan:

const view = new DataView(new ArrayBuffer(8));

function toOrdinal(x) {
  view.setFloat64(0, x);
  const i = view.getBigInt64(0);
  return i >= 0n ? i : -(1n << 63n) - i;   // make negatives order correctly
}

function ulpDistance(a, b) {
  const d = toOrdinal(a) - toOrdinal(b);
  return d < 0n ? -d : d;
}

ulpDistance(0.1 + 0.2, 0.3);                // 1n
ulpDistance((0.1 + 0.2) * 1e9, 0.3 * 1e9);  // 1n
ulpDistance(-0, 0);                         // 0n

Omong-omong, == yang eksak tidak selalu salah. Perbandingan itu aman untuk double bernilai bulat di bawah 2⁵³, untuk konstanta yang Anda tetapkan sendiri dan tidak pernah masuk perhitungan, serta untuk memeriksa apakah sebuah nilai persis nol.

Ketika Presisi Menyangkut Uang: Memilih Tipe Numerik yang Tepat

Mata uang dan float biner adalah pasangan yang buruk, meski bukan karena ada satu galat berukuran besar. Galatnya sistematis dan selalu berulang sama, lalu tetap tak terlihat sampai seorang auditor menemukannya:

19.99 * 100;              // 1998.9999999999998
Math.round(19.99 * 100);  // 1999

(1.005).toFixed(2);       // '1.00'  — 1.005 is stored as 1.00499999999999989...

Yang kedua itu menjebak tim baru setiap tahun. toFixed tidak salah membulatkan. Nilai yang sampai ke tangannya memang sudah berada di bawah 1.005.

Satuan terkecil dalam bilangan bulat

Simpan sen, bukan dolar. 1999 berarti $19.99, aritmetika berjalan di atas bilangan bulat, dan Anda membagi hanya pada detik terakhir saat menampilkannya.

const priceCents = 1999;             // $19.99
const subtotal = priceCents * 3;     // 5997 — exact

const totalCents = 1000;             // $10.00 split three ways
const share = Math.floor(totalCents / 3);   // 333
const remainder = totalCents - share * 3;   // 1 cent to allocate

Ada dua batasan yang perlu Anda ingat. Di atas Number.MAX_SAFE_INTEGER (9007199254740991, atau 2⁵³ − 1) bilangan bulat JavaScript berhenti eksak, jadi pakailah BigInt. Bilangan bulat juga tidak menentukan kebijakan pembulatan Anda: pembagian, persentase, dan pemecahan pajak tetap butuh aturan eksplisit soal ke mana sisa satu sen itu pergi.

Tipe desimal

Tipe desimal menyimpan digit basis 10, jadi pecahan desimal mendarat persis. Bangun nilainya dari sebuah string, jangan pernah dari float, kalau tidak Anda sudah mewarisi galat biner bahkan sebelum mulai:

from decimal import Decimal

Decimal("0.1") + Decimal("0.2") == Decimal("0.3")   # True
Decimal(0.1)                                        # 0.1000000000000000055511151231257827021181583404541015625
Console.WriteLine(0.1m + 0.2m);          // 0.3
Console.WriteLine(0.1m + 0.2m == 0.3m);  // True

Java punya BigDecimal, C# punya decimal 128-bit bawaan, SQL punya NUMERIC(12,2). Semuanya membereskan pecahan desimal. Tidak satu pun membereskan 1/3, karena tipe desimal tetaplah format floating point; ia cuma menggeser basisnya dari 2 ke 10.

Matriks keputusan

Kasus penggunaanTipe yang disarankanAlasannya
Uang, penagihan, pajakSatuan terkecil bilangan bulat atau desimalAritmetika desimal eksak, pembulatan yang bisa diaudit
Sains, fisikadouble (FP64)15–16 digit sudah lebih dari cukup; dukungan pustaka terbaik
Geometri, grafisfloat atau double + toleransiSudah bersifat hampiran; bandingkan dengan epsilon
Pelatihan MLbfloat16Jangkauan FP32 dengan separuh memori
Inferensi ML, penyimpananFP16Bit mantissa lebih banyak saat nilai sudah ternormalisasi
Pencacah, ID, kunciBilangan bulat 64-bit atau BigIntSejak awal memang bukan urusan float

Akumulasi Galat dan Pembatalan Katastrofik

Satu galat pembulatan bernilai 1e-17 dan tidak berbahaya. Sepuluh ribu galat semacam itu berubah menjadi tiket dukungan pelanggan:

let total = 0;
for (let i = 0; i < 10000; i++) total += 0.1;

total;         // 1000.0000000001588
total - 1000;  // 1.588205122970976e-10

Penyimpangan tumbuh kira-kira sebanding dengan jumlah operasi. Dalam perulangan sekecil ini penyimpangan itu tak kasatmata; dalam agregasi malam hari atas jutaan baris, ceritanya lain.

Pembatalan katastrofik

Kegagalan yang lebih licik datang dari pengurangan. Kurangkan dua bilangan yang nyaris sama, digit-digit depan yang cocok saling meniadakan (catastrophic cancellation), dan yang tersisa adalah hasil yang didominasi galat yang sudah menumpuk sebelumnya. Galat absolutnya tidak bertambah besar. Galat relatif-nya yang meledak, karena hasilnya kini mungil sementara galatnya tetap sebesar semula.

Rumus varians satu-lintasan yang ada di buku teks, E[x²] − (E[x])², melangkah lurus ke dalam jebakan itu:

const data = [1e9, 1e9 + 1, 1e9 + 2];
const mean = data.reduce((s, x) => s + x, 0) / data.length;

// One-pass: E[x²] − (E[x])²
data.reduce((s, x) => s + x * x, 0) / data.length - mean * mean;  // 0

// Two-pass: centre the data first
data.reduce((s, x) => s + (x - mean) ** 2, 0) / data.length;      // 0.6666666666666666

Varians populasi yang sebenarnya adalah 2/3. Rumus satu-lintasan mengembalikan tepat nol, bukan sekadar meleset sedikit, karena kedua operandnya berada di sekitar 1e18 dan selisih di antara keduanya hidup di bawah bit terakhir yang tersimpan. Algoritme daring Welford menghindari hal ini dengan memperbarui rerata dan jumlah kuadrat simpangan secara inkremental.

Rumus kuadrat punya kelemahan yang sama ketika b² ≫ 4ac, dan perbaikannya pun bergaya sama:

const a = 1, b = 1e8, c = 1;
const disc = Math.sqrt(b * b - 4 * a * c);

(-b + disc) / (2 * a);   // -7.450580596923828e-9  — about 25% wrong
(2 * c) / (-b - disc);   // -1e-8                  — correct

Kedua baris menghitung akar yang sama. Baris pertama mengurangkan dua bilangan yang nyaris sama; baris kedua menata ulang aljabarnya supaya tidak pernah perlu melakukan itu. Tulisan Goldberg, What Every Computer Scientist Should Know About Floating-Point Arithmetic, masih menjadi rujukan utama soal pembatalan dan batas galat.

Penjumlahan Kahan

Penjumlahan terkompensasi ala Kahan menyimpan catatan berjalan atas bit-bit orde rendah yang terbuang di setiap penjumlahan, lalu mengumpankannya kembali pada iterasi berikutnya:

function kahanSum(values) {
  let sum = 0;
  let compensation = 0;
  for (const value of values) {
    const y = value - compensation;
    const t = sum + y;
    compensation = (t - sum) - y;   // the bits that fell off
    sum = t;
  }
  return sum;
}

kahanSum(new Array(10000).fill(0.1));   // 1000  — exactly

Python memberi Anda ini cuma-cuma. math.fsum([0.1] * 10_000) mengembalikan 1000.0, dan sejak CPython 3.12 fungsi bawaan sum() juga memakai kompensasi Neumaier, sehingga sum([0.1] * 10_000) eksak sementara perulangan += yang eksplisit tetap menyimpang ke 1000.0000000001588.

Pakailah kompensasi untuk penjumlahan panjang, agregat keuangan, dan integrasi numerik. Lewati saja untuk segelintir nilai atau ketika Anda sudah pindah ke tipe yang eksak.

Nilai Khusus yang Mematahkan Asumsi

IEEE 754 mencadangkan sejumlah pola bit untuk nilai yang tidak menuruti aturan yang Anda harapkan:

NaN === NaN;          // false — mandated by the standard
Number.isNaN(NaN);    // true
[NaN].indexOf(NaN);   // -1   (uses ===)
[NaN].includes(NaN);  // true (uses SameValueZero)

-0 === 0;             // true
Object.is(-0, 0);     // false
1 / -0;               // -Infinity

1e308 * 10;           // Infinity — overflow never throws
Infinity - Infinity;  // NaN
Number.MIN_VALUE;     // 5e-324 — the smallest subnormal double

NaN tidak setara dengan apa pun, termasuk dengan dirinya sendiri, dan itu memang disengaja, supaya perhitungan yang gagal tidak pernah bisa menyamar sebagai hasil yang sah. Ujilah dengan Number.isNaN() atau math.isnan() di Python.

Overflow (limpahan) jauh lebih senyap dan lebih berbahaya: ia mengembalikan ±Infinity lalu jalan terus, merambat ke hilir sampai ada yang sadar grafiknya penuh ruang kosong. Nol negatif adalah pola bit tersendiri yang tetap setara dengan +0 di bawah ===; bentuk itu lahir dari underflow sebuah nilai negatif atau dari -1 * 0, dan hanya pembagian atau Object.is yang menyingkapnya.

Subnormal mengisi celah antara nol dan bilangan normal terkecil. Ketika medan eksponennya nol semua, angka 1 implisit di depan menghilang dan mantissanya menyusut perlahan menuju nol, yang menjamin selisih dua float yang tidak sama tidak pernah membulat menjadi tepat nol. Harganya: presisi yang merosot dan, pada sebagian perangkat keras, jurang performa yang tajam. Chip nilai khusus di konverter IEEE 754 memuat ±0, ±Infinity, NaN, dan subnormal terkecil sehingga Anda bisa memeriksa setiap pola bit secara langsung.

float vs double vs FP16 vs bfloat16

Standar yang sama, anggaran yang berbeda untuk jangkauan dan presisi floating point:

FormatTotal bitEksponenMantissaPerkiraan digit desimalMaksimum berhinggaPenggunaan umum
binary16 (FP16)16510~3.365504Inferensi GPU, penyimpanan ringkas
bfloat161687~2.4~3.39 × 10³⁸Pelatihan ML
binary32 (float)32823~7.2~3.40 × 10³⁸Grafis, sensor, GPU
binary64 (double)641152~15.9~1.80 × 10³⁰⁸Default untuk sisanya

Bit eksponen membeli jangkauan; bit mantissa membeli presisi. FP16 membelanjakan anggarannya pada presisi dan mentok di 65504, cukup dekat dengan nilai gradien nyata sehingga limpahan menjadi Infinity adalah bahaya rutin. bfloat16 mengambil pertukaran sebaliknya, mempertahankan delapan bit eksponen milik FP32 dan rela hidup dengan tujuh bit mantissa, sehingga nilai yang akan meluap di FP16 lewat dengan mulus dan pelatihan jarang membutuhkan loss scaling.

Jadikan double sebagai pilihan default. Turun ke format yang lebih sempit setelah Anda benar-benar mengukur adanya hambatan memori atau bandwidth, lalu periksa berapa ongkos penyempitan itu dengan memasukkan nilai yang sama pada tiap format di konverter.

Aturan Praktis untuk Presisi Floating Point

  • Jangan pernah pakai == pada float hasil perhitungan. Pakailah toleransi gabungan absolut-plus-relatif yang ukurannya pas dengan data Anda.
  • Jangan perlakukan Number.EPSILON sebagai ambang. Angka itu menggambarkan kisi di titik 1.0 dan tidak lebih.
  • Simpan uang dalam satuan terkecil bilangan bulat atau tipe desimal, dan selalu bangun nilai desimal dari string.
  • Kompensasi penjumlahan yang panjang dengan Kahan, Neumaier, atau math.fsum.
  • Tata ulang rumus yang mengurangkan besaran nyaris sama, alih-alih memperketat toleransi di sekitarnya.
  • Format angka secara eksplisit untuk manusia dengan toFixed, f-string, atau printf, dan jangan pernah mengirim repr default ke pengguna.
  • Kirim angka lintas batas layanan sebagai string atau bilangan bulat. Perjalanan bolak-balik JSON lewat sebuah double berlangsung senyap dan merusak presisi.
  • Pilih NUMERIC, bukan FLOAT, untuk kolom mata uang. Memperbaikinya setelah rilis berarti migrasi plus rekonsiliasi.
  • Berpikirlah dalam bit ketika sebuah nilai tampak mustahil. Kebiasaan yang sama menggerakkan panduan operasi bitwise kami, dan kebiasaan itu mengubah misteri floating-point menjadi aritmetika yang bisa Anda periksa sendiri.

FAQ

Apakah matematika floating point rusak?

Matematika floating point tidak rusak; ia mengikuti IEEE 754 dengan persis. Standar itu mewakili bilangan riil dalam jumlah digit biner yang berhingga, dan pecahan desimal seperti 0.1 tidak punya bentuk biner yang eksak, jadi nilai representabel terdekatlah yang tersimpan. Yang rusak adalah harapan bahwa setiap desimal pasti muat.

Mengapa Python mencetak 0.1 sebagai 0.1 tetapi 0.1 + 0.2 sebagai 0.30000000000000004?

repr milik Python memakai pemformatan shortest round-trip: ia mencetak string desimal terpendek yang bila terurai kembali menghasilkan double yang identik. "0.1" sudah menandai double-nya secara unik. Jumlah 0.1 + 0.2 adalah double yang berbeda dari yang dipetakan "0.3", jadi pemformat harus mengeluarkan seluruh tujuh belas digit agar tetap tidak ambigu.

Mengapa saya sebaiknya tidak memakai Number.EPSILON untuk membandingkan dua float?

Number.EPSILON (sekitar 2.22e-16) adalah jarak antara 1 dan double berikutnya, bukan anggaran galat universal. Jarak antar-float berlipat dua di tiap pangkat dua, jadi di dekat 1e9 dua double bertetangga terpisah sekitar 1.19e-7. Galat pembulatan sungguhan di sana melampaui EPSILON, sehingga perbandingannya selalu false. Gunakan toleransi relatif atau toleransi gabungan.

Bisakah saya memakai floating point untuk uang kalau saya membulatkannya di akhir?

Membulatkan di akhir tidak menyelamatkan uang yang tersimpan sebagai floating point. Galat menumpuk lintas penjumlahan, perkalian, dan alokasi bertahap, dan momen Anda membulatkan ikut mengubah totalnya. 19.99 * 100 saja sudah menghasilkan 1998.9999999999998. Audit menuntut aritmetika eksak yang dapat direproduksi: sen bilangan bulat, decimal, BigDecimal, atau NUMERIC di SQL.

Bilangan desimal mana saja yang bisa tersimpan persis dalam sebuah double?

Hanya nilai yang bisa dinyatakan sebagai m / 2ⁿ untuk bilangan bulat m dan n dalam batas presisi formatnya. Itu mencakup 0.5, 0.25, 0.125, 0.75, dan 2.5, plus setiap bilangan bulat sampai 2⁵³. Sederhanakan pecahannya lebih dulu: sisa faktor 5 apa pun pada penyebut, seperti pada 0.1, 0.2, 0.3, atau 0.7, berarti nilai itu hanya tersimpan sebagai hampiran.

Mengapa 0.1 + 0.2 === 0.3 false tetapi 0.5 + 0.25 === 0.75 true?

Karena 0.5, 0.25, dan 0.75 adalah 2⁻¹, 2⁻², dan 2⁻¹ + 2⁻²: semuanya representabel secara persis, dan jumlahnya tidak butuh pembulatan. 0.1, 0.2, dan 0.3 masing-masing tersimpan sebagai hampiran, dan pembulatan atas jumlah dua yang pertama mendarat satu ULP di atas double yang dipetakan dari 0.3.

Apakah ini terjadi di setiap bahasa pemrograman?

Setiap bahasa yang berdiri di atas floating point biner IEEE 754 menunjukkan perilaku yang sama: JavaScript, Python, Java, C, C++, Go, Rust, Swift, dan FLOAT milik SQL. Yang berbeda hanyalah presisi cetak default dan apakah tipe desimal eksak sudah tersedia dari sononya, seperti decimal di C# atau modul decimal di Python.

Tag: floating-point ieee-754 javascript python numbers

Artikel Terkait

Lihat semua artikel