Tipe floating point tidak bisa merepresentasikan setiap bilangan real secara tepat. Karena jumlah bit terbatas, sebagian besar bilangan desimal—termasuk 0.1—tidak memiliki representasi biner yang persis sama. Akibatnya, perhitungan floating point selalu mengandung kesalahan pembulatan yang sangat kecil, dan kesalahan ini bisa terakumulasi dalam loop ribuan iterasi. Pada Chapter ini kita akan melihat fenomena klasik 0.1 + 0.2 != 0.3, bagaimana nilai kecil bisa “hilang” saat ditambahkan ke nilai besar, serta cara membandingkan dua nilai floating point dengan benar menggunakan toleransi epsilon.

Fenomena 0.1 + 0.2 != 0.3 #

Salah satu kejutan paling terkenal bagi pemula floating point: pada C, Python, JavaScript, dan hampir semua bahasa yang memakai IEEE 754, ekspresi 0.1 + 0.2 tidak sama persis dengan 0.3. Penyebabnya adalah 0.1 dan 0.2 tidak memiliki representasi biner yang tepat—keduanya dibulatkan ke pecahan biner terdekat, dan pembulatan ini menyebabkan akumulasi selisih sangat kecil.

c
#include <stdio.h>

int main(void) {
    /* 0.1 + 0.2 bukan 0.3 pada floating point */
    double a = 0.1;
    double b = 0.2;
    double c = a + b;

    printf("0.1 = %.20f\n", a);
    printf("0.2 = %.20f\n", b);
    printf("sum = %.20f\n", c);
    printf("0.3 = %.20f\n", 0.3);

    if (c == 0.3) {
        printf("sum == 0.3 (true)\n");
    } else {
        printf("sum != 0.3 (false, padahal matematis sama)\n");
    }
    return 0;
}

Output yang Dihasilkan:

text
0.1 = 0.10000000000000000555
0.2 = 0.20000000000000001110
sum = 0.30000000000000004441
0.3 = 0.29999999999999998890
sum != 0.3 (false, padahal matematis sama)

Mari kita telaah baris-baris kode di atas:

  1. Baris 6 (double a = 0.1;): nilai 0.1 disimpan sebagai 0.10000000000000000555... karena pembulatan ke pecahan biner terdekat.
  2. Baris 7 (double b = 0.2;): nilai 0.2 disimpan sebagai 0.20000000000000001110....
  3. Baris 8 (double c = a + b;): penjumlahan menghasilkan 0.30000000000000004441....
  4. Baris 12 (%.20f): mencetak dengan 20 digit desimal untuk memperlihatkan selisih kecil di belakang koma.
  5. Baris 14 (0.3): literal 0.3 sendiri disimpan sebagai 0.29999999999999998890..., berbeda dari hasil penjumlahan a + b.
  6. Baris 16 (if (c == 0.3)): perbandingan persis == mengembalikan false karena selisih sangat kecil di bit terakhir.

Absorpsi Nilai Kecil dan Cancellation #

Presisi double sekitar 15-17 digit desimal. Saat kita menambahkan nilai yang sangat besar dengan nilai yang sangat kecil, nilai kecil bisa “hilang” karena tidak muat di digit presisi yang tersedia. Fenomena ini disebut absorption. Sebaliknya, saat mengurangkan dua bilangan yang hampir sama besar, digit-digit paling depan saling menghilangkan sehingga digit-digit sisa yang sebelumnya tidak signifikan menjadi dominan—fenomena ini disebut catastrophic cancellation.

c
#include <stdio.h>
#include <float.h>
#include <math.h>

int main(void) {
    /* Absorpsi nilai kecil oleh nilai besar */
    double besar = 1e16;
    double kecil = 1.0;
    double hasil = besar + kecil;
    printf("besar = %.1f\n", besar);
    printf("kecil = %.1f\n", kecil);
    printf("besar + kecil = %.1f (kecil hilang!)\n", hasil);
    printf("(harapan matematis: 10000000000000001.0)\n");

    /* Loss of precision pada pengurangan */
    double x = 1.0000001;
    double y = 1.0000000;
    double diff = x - y;
    printf("x = %.10f\n", x);
    printf("y = %.10f\n", y);
    printf("x - y = %.10f (cancellation)\n", diff);

    /* Epsilon sebagai batas perbandingan */
    double eps = DBL_EPSILON;
    printf("DBL_EPSILON = %.20e\n", eps);
    return 0;
}

Output yang Dihasilkan:

text
besar = 10000000000000000.0
kecil = 1.0
besar + kecil = 10000000000000000.0 (kecil hilang!)
(harapan matematis: 10000000000000001.0)
x = 1.0000001000
y = 1.0000000000
x - y = 0.0000001000 (cancellation)
DBL_EPSILON = 2.22044604925031308085e-16

Pada baris 9, nilai 1e16 + 1 seharusnya menghasilkan 10.000.000.000.000.001, tetapi hasil aktual adalah 10.000.000.000.000.000 karena 1.0 terlalu kecil untuk memengaruhi digit presisi double pada magnitudo 1e16. Pada baris 17, selisih 1.0000001 - 1.0000000 = 0.0000001 seharusnya punya 7 digit signifikan, tetapi karena kedua operan hanya punya 7 digit presisi yang signifikan, hasilnya hanya punya sekitar 1 digit presisi yang bisa dipercaya. Pada baris 22, DBL_EPSILON adalah selisih terkecil antara 1.0 dan double berikutnya yang lebih besar dari 1.0; nilai ini sekitar 2.22e-16 dan menjadi patokan presisi mesin.

Perbandingan dengan Toleransi Epsilon #

Untuk membandingkan dua nilai floating point secara matematis, gunakan toleransi epsilon alih-alih persamaan ==. Strateginya: hitung selisih absolut kedua nilai, lalu bandingkan dengan ambang batas yang besarnya bergantung pada magnitudo nilai yang dibandingkan. Untuk nilai mendekati nol, gunakan absolute epsilon; untuk nilai besar, gunakan relative epsilon yang diskalakan dengan magnitudo.

c
#include <stdio.h>
#include <math.h>
#include <float.h>

/* Bandingkan floating point dengan toleransi epsilon */
int approx_equal(double a, double b) {
    double diff = fabs(a - b);
    if (diff < DBL_EPSILON) return 1;
    /* Skala epsilon berdasarkan magnitudo */
    double larger = (fabs(a) > fabs(b)) ? fabs(a) : fabs(b);
    return diff < larger * DBL_EPSILON * 10.0;
}

int main(void) {
    double sum = 0.1 + 0.2;
    double target = 0.3;

    printf("0.1 + 0.2 = %.20f\n", sum);
    printf("0.3       = %.20f\n", target);

    if (sum == target) {
        printf("Perbandingan langsung: sama\n");
    } else {
        printf("Perbandingan langsung: beda\n");
    }

    if (approx_equal(sum, target)) {
        printf("Perbandingan epsilon: sama (dalam toleransi)\n");
    } else {
        printf("Perbandingan epsilon: beda\n");
    }
    return 0;
}

Output yang Dihasilkan:

text
0.1 + 0.2 = 0.30000000000000004441
0.3       = 0.29999999999999998890
Perbandingan langsung: beda
Perbandingan epsilon: sama (dalam toleransi)

Pada baris 6-12, fungsi approx_equal membandingkan dua double dengan strategi dua langkah. Baris 8: jika selisih absolut kurang dari DBL_EPSILON, dianggap sama (cocok untuk nilai mendekati nol). Baris 11-12: untuk nilai besar, epsilon diskalakan dengan magnitudo nilai yang lebih besar; faktor 10.0 memberi margin tambahan untuk kesalahan pembulatan akumulatif. Pada baris 25, perbandingan langsung sum == target mengembalikan false karena selisih bit terakhir. Pada baris 30, perbandingan epsilon mengembalikan true karena selisih sangat kecil relatif terhadap magnitudo nilai.

Strategi Mengurangi Kesalahan Akumulasi #

Beberapa strategi untuk mengurangi kesalahan floating point:

  • Pakai double alih-alih float untuk seluruh perhitungan kecuali ada alasan memori. double punya 15 digit presisi vs float 6 digit.
  • Pakai Kahan summation saat menjumlahkan ribuan elemen; algoritma ini melacak bagian hilang dari penjumlahan untuk dikompensasi pada iterasi berikutnya.
  • Hindari pengurangan dua bilangan hampir sama. Jika memungkinkan, susun ulang formula matematika untuk menghindari catastrophic cancellation.
  • Urutkan operasi dari magnitudo terkecil ke terbesar saat menjumlahkan banyak bilangan; ini mengurangi absorption.

Untuk aplikasi finansial atau kriptografi yang membutuhkan presisi tak terbatas, gunakan pustaka arbitrary precision seperti GMP (libgmp) alih-alih tipe bawaan C.

Dengan pemahaman keterbatasan presisi floating point dan strategi perbandingan epsilon, kita siap menulis kode numerik yang lebih robust. Selanjutnya, kita akan meninjau seluruh type qualifiers di C secara komprehensif.