Chapter sebelumnya menunjukkan bahwa 0,1 tidak punya representasi biner persis; chapter ini membahas akibat langsungnya di ruang perhitungan — presisi. Karena mantissa berhenti di 23 bit (float) atau 52 bit (double), jarak antara dua nilai floating-point yang bisa dibedakan (spacing) tidak seragam: rapat di sekitar nol dan makin renggang seiring besarnya angka. Akibatnya ada dua kejutan yang muncul berulang-ulang di kode nyata: penjumlahan kecil ke angka besar bisa hilang tanpa jejak, dan perbandingan == antara dua hasil perhitungan yang secara matematis sama sering bernilai false. Kita buktikan keduanya dengan program teruji, lalu membangun fungsi perbandingan yang sehat menggunakan std::numeric_limits<double>::epsilon() dengan toleransi relatif.

Kasus Klasik: 0.1 + 0.2 Tidak Sama dengan 0.3 #

Contoh paling terkenal di seluruh ilmu komputasi adalah 0.1 + 0.2 == 0.3 yang bernilai false — bukan bug compiler, melainkan konsekuensi format biner yang sudah kita bedah bitnya. Program berikut mengukur selisihnya, menunjukkan makna epsilon, membuktikan hilangnya angka kecil di sekitar 2²⁴, lalu menguji toleransi absolut melawan toleransi relatif:

cpp
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <limits>

// Perbandingan floating-point yang sehat: toleransi relatif terhadap
// besaran yang dibandingkan, bukan kesetaraan == yang kaku.
bool hampirSama(double a, double b) {
    double eps = std::numeric_limits<double>::epsilon();
    double selisih = std::fabs(a - b);
    return selisih <= eps * std::max(std::fabs(a), std::fabs(b));
}

int main() {
    std::cout << std::setprecision(std::numeric_limits<double>::max_digits10);

    // 1. Kasus klasik: 0.1 + 0.2 tidak persis 0.3
    double jumlah = 0.1 + 0.2;
    std::cout << "0.1 + 0.2          = " << jumlah << "\n";
    std::cout << "0.1 + 0.2 == 0.3   = "
              << (jumlah == 0.3 ? "sama" : "berbeda") << "\n";
    std::cout << "selisih            = " << std::fabs(jumlah - 0.3) << "\n";

    // 2. epsilon: jarak antara 1.0 dan double terdekat berikutnya
    double eps = std::numeric_limits<double>::epsilon();
    std::cout << "epsilon            = " << eps << "\n";

    // 3. Jarak antar nilai membesar seiring besarnya angka (float)
    float besar = 16777216.0f;   // 2^24
    std::cout << "2^24               = " << besar << "\n";
    std::cout << "2^24 + 1.0f        = " << besar + 1.0f << " (hilang!)\n";
    std::cout << "2^24 + 2.0f        = " << besar + 2.0f << "\n";

    // 4. Toleransi absolut gagal pada skala besar; relatif menolong
    double p = 1e16;
    double q = p + 2.0;
    std::cout << "|p - q|            = " << std::fabs(p - q) << "\n";
    std::cout << "abs <= eps?        = "
              << (std::fabs(p - q) <= eps ? "sama" : "berbeda") << "\n";
    std::cout << "hampirSama(p, q)   = "
              << (hampirSama(p, q) ? "sama" : "berbeda") << "\n";
    std::cout << "hampirSama(jumlah, 0.3) = "
              << (hampirSama(jumlah, 0.3) ? "sama" : "berbeda") << "\n";

    return 0;
}

Output yang Dihasilkan:

text
0.1 + 0.2          = 0.30000000000000004
0.1 + 0.2 == 0.3   = berbeda
selisih            = 5.5511151231257827e-17
epsilon            = 2.2204460492503131e-16
2^24               = 16777216
2^24 + 1.0f        = 16777216 (hilang!)
2^24 + 2.0f        = 16777218
|p - q|            = 2
abs <= eps?        = berbeda
hampirSama(p, q)   = sama
hampirSama(jumlah, 0.3) = sama

Bedah baris-per-baris:

  1. Baris 9-13 (bool hampirSama(double a, double b)): fungsi perbandingan dengan toleransi relatif — selisih dianggap nol bila lebih kecil dari epsilon dikali besaran angka yang dibandingkan. Pendekatan ini otomatis menyesuaikan skala: toleransi untuk nilai sekitar 1e16 jadi sekitar 2, tidak lagi seribu kali lebih kecil dari jarak minimum yang masih bisa dibedakan.
  2. Baris 16 (std::cout << std::setprecision(...max_digits10)): presisi dicetak dinaikkan ke 17 digit (nilai max_digits10 untuk double) agar selisih yang sebenarnya terlihat, tidak tertutup pembulatan tampilan default 6 digit.
  3. Baris 19-23 (double jumlah = 0.1 + 0.2; ...): hasil 0.1 + 0.2 tercetak 0.30000000000000004. Penjumlahan dua pembulatan terdekat menghasilkan pembulatan baru yang sedikit melebihi 0,3; selisihnya terukur 5.55e-17 — kecil, tetapi cukup untuk membuat == bernilai berbeda.
  4. Baris 26-27 (double eps = ...epsilon(); std::cout << ...): epsilon = 2,22 × 10⁻¹⁶ adalah jarak antara 1.0 dan double terdekat berikutnya. Angka ini adalah satuan pengukuran presisi mesin, bukan “batas aman semua skala” — lihat bukti di bagian 4.
  5. Baris 30-33 (float besar = 16777216.0f; ...): pada 2²⁴ = 16777216, float sudah kehabisan bit sehingga jarak antar nilai berikutnya adalah 2. Menambah 1.0f tidak mengubah apa pun (dibulatkan kembali ke 16777216), menambah 2.0f baru terlihat. Inilah spacing yang renggang — dan untuk double, fenomena serupa mulai di sekitar 2⁵³ ≈ 9 × 10¹⁵.
  6. Baris 36-40 (double p = 1e16; ...): p dan q = p + 2.0 berjarak 2 secara absolut — nilai itu melebihi epsilon (2,22 × 10⁻¹⁶) sehingga uji absolut menyatakan berbeda, padahal secara relatif selisihnya hanya 2 × 10⁻¹⁶, lebih kecil dari satu epsilon pada skala 1e16.
  7. Baris 41-44 (hampirSama(p, q) ...): fungsi toleransi relatif menyatakan p dan q sama (selisih 2e-16 ≤ eps × 1e16 ≈ 2,2), dan juga menyatakan 0.1 + 0.2 sama dengan 0.3 — dua kasus yang sehat keduanya, tanpa menyesuaikan konstanta per kasus.

Memilih Toleransi: Absolut vs Relatif #

MetodeRumus intiCocok untukTitik lemah
Kesetaraan ==a == bnilai yang dijamin bit-identik (0.5, 0.25)hampir semua hasil perhitungan
Absolutfabs(a-b) <= 1e-9perbandingan terhadap nol atau skala tetapgagal pada angka besar (contoh 1e16 di atas)
Relatif`fabs(a-b) <= eps * max(a,

Dua pilihan terakhir saling melengkapi: kode produksi sering menggabungkan keduanya — fabs(a-b) <= tolAbsolut || fabs(a-b) <= tolRelatif * max(fabs(a), fabs(b)) — agar perbandingan dengan nol tetap bisa lewat. Untuk aplikasi Anda sendiri, nilai toleransi juga pertanyaan domain, bukan hanya matematika: pengukuran sensor milimeter mungkin puas dengan selisih 1e-6, sementara simulasi orbit butuh jauh lebih ketat.

Setelah memahami kehilangan presisi pada satu operasi, pertanyaan berikutnya menanti: bagaimana nasib akurasi ketika ratusan ribu operasi dijumlahkan beruntun? Selisih kecil yang “aman” di satu penjumlahan bisa menumpuk menjadi galat besar — topik Chapter Rounding Errors berikutnya, lengkap dengan teknik Kahan summation untuk menahannya.

Rujukan resmi untuk topik Chapter ini: std::numeric_limits dan fungsi matematika <cmath> di cppreference.