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:
#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:
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) = samaBedah baris-per-baris:
- Baris 9-13 (
bool hampirSama(double a, double b)): fungsi perbandingan dengan toleransi relatif — selisih dianggap nol bila lebih kecil dariepsilondikali 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. - Baris 16 (
std::cout << std::setprecision(...max_digits10)): presisi dicetak dinaikkan ke 17 digit (nilaimax_digits10untukdouble) agar selisih yang sebenarnya terlihat, tidak tertutup pembulatan tampilan default 6 digit. - Baris 19-23 (
double jumlah = 0.1 + 0.2; ...): hasil0.1 + 0.2tercetak0.30000000000000004. Penjumlahan dua pembulatan terdekat menghasilkan pembulatan baru yang sedikit melebihi 0,3; selisihnya terukur5.55e-17— kecil, tetapi cukup untuk membuat==bernilaiberbeda. - 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. - Baris 30-33 (
float besar = 16777216.0f; ...): pada 2²⁴ = 16777216, float sudah kehabisan bit sehingga jarak antar nilai berikutnya adalah 2. Menambah1.0ftidak mengubah apa pun (dibulatkan kembali ke 16777216), menambah2.0fbaru terlihat. Inilah spacing yang renggang — dan untuk double, fenomena serupa mulai di sekitar 2⁵³ ≈ 9 × 10¹⁵. - Baris 36-40 (
double p = 1e16; ...):pdanq = p + 2.0berjarak 2 secara absolut — nilai itu melebihiepsilon(2,22 × 10⁻¹⁶) sehingga uji absolut menyatakanberbeda, padahal secara relatif selisihnya hanya 2 × 10⁻¹⁶, lebih kecil dari satu epsilon pada skala 1e16. - Baris 41-44 (
hampirSama(p, q) ...): fungsi toleransi relatif menyatakanpdanqsama (selisih 2e-16 ≤ eps × 1e16 ≈ 2,2), dan juga menyatakan0.1 + 0.2sama dengan0.3— dua kasus yang sehat keduanya, tanpa menyesuaikan konstanta per kasus.
Memilih Toleransi: Absolut vs Relatif #
| Metode | Rumus inti | Cocok untuk | Titik lemah |
|---|---|---|---|
Kesetaraan == | a == b | nilai yang dijamin bit-identik (0.5, 0.25) | hampir semua hasil perhitungan |
| Absolut | fabs(a-b) <= 1e-9 | perbandingan terhadap nol atau skala tetap | gagal 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.