Skip to content
Bloga Dönün
Eğitimler

Kayan Nokta Hassasiyeti: 0.1 + 0.2 Neden 0.3 Değil?

Kayan nokta hassasiyeti 0.1 + 0.2'yi neden bozar, IEEE 754 nasıl yuvarlar, float değerlerini güvenle nasıl karşılaştırırsınız — ücretsiz çevrimiçi dönüştürücü.

12 dakika okuma

Kayan Nokta Hassasiyeti: 0.1 + 0.2 Neden 0.3 Değil?

0.1 + 0.2 işlemi 0.30000000000000004 döndürür, çünkü ne 0.1 ne de 0.2 ikilik bir kayan nokta sayısının içinde var olur. Bir double yalnızca m × 2ⁿ biçimindeki değerleri tutabilir ve onda bir, taban 2’de sonsuza kadar tekrar eden bir kesirdir; bu yüzden donanım onun yerine temsil edilebilir en yakın komşuyu saklar. 0.1 değerinin arkasındaki double tam olarak 0.1000000000000000055511151231257827021181583404541015625 sayısıdır. 0.2 değerinin arkasındaki ise 0.200000000000000011102230246251565404236316680908203125. Saklanan bu iki değeri toplayın; tam sonuç, temsil edilebilir iki double’ın arasına düşer. IEEE 754 daha yakın olana yuvarlar ve o da 0.3 sayısının kıl payı üstünde durur.

Cevabın tamamı bu. Kayan nokta hassasiyeti sonludur ve onluk kesirler ikilik ızgaraya nadiren oturur; üstelik her işlem sonucunu yeniden yuvarlar.

Bu bir JavaScript tuhaflığı ya da bir CPU kusuru değildir. Aynı sonuç Python, Java, C, Go, Rust, Swift ve SQL’in FLOAT sütunlarında da çıkar, çünkü hepsi IEEE 754 üzerinde çalışır. Herhangi bir değeri ücretsiz IEEE 754 kayan nokta dönüştürücüsüne yapıştırın; tam bitleri ve saklanan tam değeri basamak basamak görün.

Geri kalan bölümler hatanın nereden çıktığını ve dilinizin onu neden bazen gizlediğini açıklıyor; sonra da float değerlerini == olmadan nasıl karşılaştıracağınıza ve hassasiyetin bedeli para olduğunda hangi sayısal türe geçeceğinize geliyor.

60 saniyede kayan nokta hassasiyeti

Neredeyse her sürprizi üç olgu açıklar:

  1. İkilik kayan nokta yalnızca m × 2ⁿ biçimindeki sayıları, yani ikinin kuvvetlerinin toplamlarını temsil edebilir.
  2. 0.1, 0.2 ve 0.3 gibi onluk kesirler bu biçimde değildir, dolayısıyla girişte yuvarlanırlar.
  3. Her aritmetik işlem sonucunu yeniden en yakın temsil edilebilir değere yuvarlar.
OnlukTam temsil edilebilir mi?Neden
0.5Evet2⁻¹
0.25Evet2⁻²
0.75Evet2⁻¹ + 2⁻²
2.5Evet2 + 2⁻¹
100Evet2⁵³ altındaki her tam sayı
0.1Hayır1/10 — paydada 5 çarpanı var
0.2Hayır1/5 — aynı sorun
0.3Hayır3/10 — aynı sorun

Pratik kural: kesri sadeleştirin; payda ikinin saf bir kuvvetiyse değer tamdır. Geriye kalan her 5 çarpanı, değerin yaklaşık olarak saklandığı anlamına gelir.

0.1 neden tam olarak saklanamaz

İkilik noktadan sonraki bitler 1/2, 1/4, 1/8, 1/16, 1/32 ve devamı ağırlıkları taşır. 0.1 sayısını bunlardan kurmayı deneyin, asla tam üstüne düşemezsiniz. 1/16 = 0.0625, artı 1/32 = 0.09375, artı 1/256 = 0.09765625 elde edersiniz; her adımda biraz daha yakın, hiçbir zaman tam. İkilikte onda bir, 0011 sonsuza kadar tekrarlanacak şekilde 0.0001100110011… olur.

Onluk sistemin de farklı kesirlerle aynı sorunu vardır. 1/3 sayısını taban 10’da yazmak 0.333… verir ve sonlu hiçbir basamak dizisi onu tam yakalayamaz. Kimse buna onluk sistemin hatası demiyor. Taban 2 sınırı sadece başka bir yere çiziyor ve 1/10 da tesadüfen o sınırın yanlış tarafına düşüyor. Uzun anlatımını isterseniz, Python belgelerindeki Floating Point Arithmetic: Issues and Limitations bölümü aynı açılımı adım adım gösteriyor.

İkilik yalnızca taban 2’dir ve aynı konumsal aritmetik hex ile sekizliği de yürütür. Sayı tabanı dönüştürücü bir değerin tabanlar arasında nasıl yer değiştirdiğini gösterir; sayı tabanı dönüşümü rehberimiz ise tam sayı tarafını derinlemesine ele alır. Taban 2’nin canını yakmaya başladığı yer kesirlerdir.

Bir double aslında neyi saklar

64 bitlik bir double üç alana ayrılır: 1 işaret biti, 11 üs biti ve 52 mantis biti. Mantis, hiçbir zaman saklanmayan örtük bir baştaki 1 taşır; böylece 53 bitlik bir anlamlı kısım, yani kabaca 15.95 onluk basamaklık kayan nokta hassasiyeti elde edersiniz. Üs ise 1023 kaydırmasıyla saklanır.

Herhangi bir dilden normalde yazdırdığından fazla basamak isteyin, yaklaşıklık kendini gösterir:

>>> 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) dürüst görünümdür: elinizde zaten bulunan double’ı yeniden yuvarlamadan dönüştürür ve 55 basamağın tamamını yazdırır. IEEE 754 dönüştürücüsündeki doğruluk paneli, yazdığınız her sayı için aynı açılımı, girdiğiniz değere göre işaretli yuvarlama hatasının yanında yazdırır.

0.30000000000000004 sayısının arkasındaki tam aritmetik

Peki sonuç neden başka bir yakın ıskalama değil de tam olarak o sayı?

Saklanan iki değeri hiç yuvarlamadan, tam olarak toplayın; şunu elde edersiniz:

0.1 →  0.1000000000000000055511151231257827021181583404541015625
0.2 →  0.200000000000000011102230246251565404236316680908203125
sum →  0.3000000000000000166533453693773481063544750213623046875

Bu toplamın kendisi temsil edilebilir bir double değildir. İkilik ızgarada iki komşunun arasında durur:

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

Şimdi aralıkları ölçün. Tam toplam, alttaki komşunun 2⁻⁵⁵2.776e-17 üstünde, üstteki komşunun da 2⁻⁵⁵ altında kalıyor: ikisine tam eşit uzaklıkta, kusursuz bir beraberlik.

En yakına yuvarlamanın burada çalışacak bir mesafesi kalmaz, bu yüzden IEEE 754 beraberlik bozucusunu devreye sokar: yarımı çifte yuvarlama, yani son mantis biti 0 olan adayı seçme. Alttaki komşunun anlamlı kısmı 5404319552844595, yani tek. Üsttekininki 5404319552844596, yani çift. Çift değer kazanır ve 0x3FD3333333333334 bit desenini verir; bu da 0.3 sabitinin eşlendiği double’ın bir ULP üstüdür, ekrana 0.30000000000000004 olarak düşer.

Peki neden çift tercih edilir? Yarımları hep yukarı yuvarlamak uzun toplamları tek yönde yanlı hale getirirdi. Dönüşümlü olarak çift komşuya yaklaşmak, çok sayıda işlem boyunca bu kaymayı sıfıra yakın tutar. Bu, muhasebecilerin kullandığı bankacı yuvarlamasının silikona standartlaştırılmış hali. İnternetin en meşhur kayan nokta yuvarlama hatasının ...04 ile bitmesinin doğrudan nedeni de bu.

Hiç yuvarlama gerektirmeyen bir toplamla karşılaştırın:

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

0.5, 0.25 ve 0.75 sırasıyla 2⁻¹, 2⁻² ve 2⁻¹ + 2⁻² değerleridir. Üçü de tamdır, toplamları da tamdır ve eşitlik okul aritmetiğinin öngördüğü gibi davranır. Dönüştürücünün komşular paneli, girdiğiniz değerin çevresindeki önceki ve sonraki temsil edilebilir değerleri ULP aralığıyla birlikte gösterir; böylece 0.3 çevresindeki ızgarayı kendiniz inceleyebilirsiniz.

Diliniz hatayı neden bazen gizler

Konuyu asıl tekinsiz kılan şey şu: 0.1 değeri 0.1 olarak yazdırılır ama 0.1 + 0.2 on yedi basamak yazdırır. Aynı saklama biçimi, taban tabana farklı çıktı.

Modern çalışma zamanları en kısa gidiş-dönüş biçimlendirmesi kullanır: aynı double’a geri ayrıştırılan en kısa onluk karakter dizisini üretirler. "0.1" zaten 0.1 için saklanan değere benzersiz biçimde gidip döner, gördüğünüz de budur. 0.1 + 0.2 toplamı ise "0.3" karakter dizisinin eşlendiğinden farklı bir double’dır; dolayısıyla biçimlendirici, karakter dizisi tek anlama gelene kadar basamak eklemek zorundadır ve bu da on yedi basamağın tamamını alır. Bu soyağacını David Gay’in 1990 tarihli doğru yuvarlama çalışması başlattı; Grisu ve ardından Ryu bunu her standart kütüphane için yeterince hızlı hale getirdi. Diliniz yalan söylemiyor. Değeri benzersiz biçimde tanımlayan en kısa etiketi gösteriyor.

Dil dil davranış

DilVarsayılan float türü0.1 + 0.2 çıktısıTam onluk seçeneği
JavaScriptnumber (yalnızca double)0.30000000000000004Yerleşik yok — tam sayı cent ya da bir kütüphane
Pythonfloat (double)0.30000000000000004decimal.Decimal, fractions.Fraction
Javadouble0.30000000000000004BigDecimal (karakter dizisinden kurun)
C#double0.30000000000000004decimal — 128 bit, taban 10
Gofloat640.30000000000000004 (değişkenler üzerinden)math/big.Rat
Rustf640.30000000000000004rust_decimal crate’i
SQLFLOAT / DOUBLE PRECISION0.30000000000000004NUMERIC / DECIMAL

Bu satırlardan ikisi bir tuzak saklıyor.

Go sabitleri keyfi hassasiyetle katlanır. Sabit bir ifade derleme zamanında tam olarak değerlendirilir ve ancak ondan sonra dönüştürülür; böylece 0.1 + 0.2 sabiti, daha float64 olmadan önce tam olarak 0.3 haline gelir:

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
}

Java’nın BigDecimal sınıfı ise, ona bir double verirseniz hatayı devralır. Yapıcı, kendisine uzatılan bitleri sadakatle dönüştürür:

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

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

SQL ise lehçeye göre değil, sütun türüne göre ayrışır:

-- 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)

Şu SQLite ikilisine bir an bakın. Yazdırılan sonuç 0.3 diyor, karşılaştırma tersini söylüyor ve ikisi de doğru.

Float karşılaştırma: == neden başarısız olur ve Number.EPSILON neden bir tolerans değildir

0.1 + 0.2 === 0.3 yanlıştır, çünkü sol taraf farklı bir double’dır. Alışıldık tavsiye bunun yerine bir toleransla karşılaştırmaktır ve bu tavsiyenin en çok tekrarlanan sürümü yanlıştır:

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

Number.EPSILON değeri 2.220446049250313e-16, yani 2⁻⁵². MDN bunu 1 ile 1’den büyük en küçük double arasındaki fark olarak tanımlar. Yani evrensel bir hata bütçesi değil, 1.0 noktasındaki ızgara aralığı. Float aralığı ikinin her kuvvetinde iki katına çıkar, dolayısıyla sabit bir eşik, ölçüldüğü komşuluğun dışında her yerde yanlıştır.

1.0 civarında tesadüfen işe yarar:

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

Aynı hesabı bir milyarla ölçekleyin, çöker:

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

1e9 civarında komşu double’lar arasındaki aralık kabaca 1.19e-7 kadardır. 1e9 sayısı [2²⁹, 2³⁰) aralığına düştüğü için oradaki ULP değeri 2²⁹⁻⁵² = 2⁻²³ ≈ 1.19e-7 olur; bu da Number.EPSILON değerinden yaklaşık 500 milyon kat büyüktür. O büyüklükteki gerçek bir yuvarlama hatası eşiği fersah fersah aşar, dolayısıyla kontrol sonsuza kadar yanlış döner. 1e-20 seviyesinde ise aynı sabit fazlasıyla cömerttir ve apaçık farklı değerleri eşit ilan eder.

Mutlak, bağıl ve ULP toleransı

Bu iş için üç ayrı araç var:

  • Mutlak tolerans (|a − b| <= atol). Büyüklüğü önceden bildiğinizde doğrudur ve sıfıra karşı işe yarayan tek seçenektir, çünkü 0 civarında bağıl tolerans her zaman 0’dır.
  • Bağıl tolerans (|a − b| <= rtol × max(|a|, |b|)). Büyüklükler arasında veriyle birlikte ölçeklenir ama sıfıra yaklaştıkça bozulur.
  • ULP uzaklığı, yani iki bit deseni arasında kaç temsil edilebilir değer bulunduğu. En kesin olanı, en az okunabilir olanı.

İlk ikisini birleştirin; her ciddi sayısal kütüphanenin kullandığı biçimi elde edersiniz:

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 bunu standart kütüphanede sunar; NumPy içindeki allclose da aynı formülü kullanır:

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

Katı sürüm için bitleri monoton bir tam sayı sıralamasına eşleyin ve çıkarın:

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

Bu arada, tam == her zaman yanlış değildir. 2⁵³ altındaki tam sayı değerli double’larda gayet uygundur; atadığınız ama üzerinde hesap yapmadığınız sabitlerde ve bir değerin tam olarak sıfır olup olmadığını kontrol ederken de öyle.

Hassasiyetin bedeli para olduğunda: doğru sayısal türü seçmek

Para birimi ile ikilik float’lar kötü bir eşleşmedir; üstelik tek bir hata büyük olduğu için değil. Hatalar sistematik ve yeniden üretilebilir olduğu hâlde bir denetçi bulana kadar görünmez kalır:

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

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

İkincisi her yıl birilerini yakalıyor. toFixed yanlış yuvarlamadı. Kendisine verilen değer zaten 1.005 sayısının altındaydı.

Tam sayı alt birimler

Dolar değil, cent saklayın. 1999 demek $19.99 demektir, aritmetik tam sayılarla yürür ve yalnızca gösterim anında bölersiniz.

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

İki uyarı. Number.MAX_SAFE_INTEGER (9007199254740991, yani 2⁵³ − 1) üzerinde JavaScript tam sayıları tam olmaktan çıkar, bu yüzden BigInt kullanın. Ayrıca tam sayılar yuvarlama politikanızı sizin yerinize belirlemez: bölme, yüzdeler ve vergi paylaşımları için artan cent’in nereye gideceğine dair açık bir kural yine gerekir.

Decimal türleri

Bir decimal türü taban 10 basamaklarını saklar, dolayısıyla onluk kesirler tam oturur. Onu bir karakter dizisinden kurun, asla bir float’tan değil; yoksa daha başlamadan ikilik hatayı devralırsınız:

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’da BigDecimal, C#‘ta yerleşik 128 bitlik bir decimal, SQL’de NUMERIC(12,2) var. Hepsi onluk kesirleri düzeltir. Hiçbiri 1/3 sayısını düzeltmez, çünkü decimal türü de hâlâ bir kayan nokta biçimidir; sadece tabanı 2’den 10’a taşımıştır.

Karar matrisi

Kullanım senaryosuÖnerilen türNeden
Para, faturalama, vergiTam sayı alt birimler veya decimalTam onluk aritmetik, denetlenebilir yuvarlama
Bilimsel hesap, fizikdouble (FP64)15–16 basamak fazlasıyla yeter; en iyi kütüphane desteği
Geometri, grafikfloat veya double + toleransZaten yaklaşık; epsilon ile karşılaştırın
ML eğitimibfloat16FP32’nin aralığı, yarı bellekle
ML çıkarımı, saklamaFP16Değerler normalize edildiğinde daha fazla mantis biti
Sayaçlar, ID’ler, anahtarlar64 bitlik tam sayı veya BigIntBunların float içinde hiç işi yoktu

Hata birikmesi ve yıkıcı sadeleşme

Tek bir yuvarlama hatası 1e-17’dir ve zararsızdır. Bunlardan on bin tanesi bir destek kaydıdır:

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

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

Kayma kabaca işlem sayısıyla birlikte büyür. Bu boyutta bir döngüde görünmezdir; milyonlarca satır üzerinde çalışan gecelik bir toplulaştırmada değildir.

Yıkıcı sadeleşme

Daha kötü olan başarısızlık çıkarmadır. Neredeyse eşit iki sayıyı çıkarın, örtüşen baştaki basamakları birbirini götürür ve geriye o ana kadar birikmiş hatanın baskın olduğu bir sonuç kalır. Mutlak hata büyümez. Bağıl hata patlar, çünkü sonuç artık minicikken hata aynı büyüklükte durmaktadır. İngilizce literatürde bu duruma catastrophic cancellation denir.

Ders kitaplarındaki tek geçişli varyans formülü, E[x²] − (E[x])², doğrudan bu tuzağın içine yürür:

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

Gerçek anakütle varyansı 2/3’tür. Tek geçişli formül tam olarak sıfır döndürür; biraz sapmış bir sonuç değil, bambaşka bir cevap. İki işlenen de 1e18 civarındaydı ve aralarındaki fark, saklanan son bitin altında yaşıyor. Welford’un çevrimiçi algoritması bunu, ortalamayı ve karesel sapmaların toplamını artımlı olarak güncelleyerek önler.

İkinci dereceden denklem formülü b² ≫ 4ac olduğunda aynı zaafı taşır ve düzeltmesi de aynı tarzdadır:

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

İki satır da aynı kökü hesaplar. İlki neredeyse eşit iki sayıyı çıkarır; ikincisi cebiri, bunu hiç yapmak zorunda kalmayacak şekilde yeniden düzenler. Goldberg’in What Every Computer Scientist Should Know About Floating-Point Arithmetic makalesi, sadeleşme ve hata sınırları konusunda hâlâ başvuru metnidir.

Kahan toplaması

Kahan’ın telafili toplaması, her toplamanın attığı düşük dereceli bitlerin kaydını tutar ve bunları bir sonraki yinelemede geri besler:

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 bunu size bedavaya verir. math.fsum([0.1] * 10_000) ifadesi 1000.0 döndürür ve CPython 3.12’den beri yerleşik sum() de Neumaier telafisi kullanır; böylece sum([0.1] * 10_000) tamdır, oysa açıkça yazılmış bir += döngüsü hâlâ 1000.0000000001588 değerine kayar.

Uzun toplamlarda, finansal toplulaştırmalarda ve sayısal integrasyonda telafiye başvurun. Birkaç değer için ya da zaten tam bir türe geçtiyseniz atlayın.

Varsayımları bozan özel değerler

IEEE 754, beklediğiniz kurallara uymayan değerler için bit desenleri ayırır:

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, tasarım gereği kendisi dahil her şeye eşitsiz karşılaştırılır; böylece başarısız bir hesap asla geçerli bir sonuç kılığına giremez. Onu Number.isNaN() ya da Python’un math.isnan() işleviyle sınayın.

Taşma daha sessiz ve daha tehlikelidir: ±Infinity döndürür ve yoluna devam eder, birisi boşluklarla dolu bir grafik fark edene kadar aşağı doğru yayılır. Negatif sıfır ise === altında hâlâ +0 değerine eşit olan ayrı bir bit desenidir; negatif bir değerin alt taşmasından ya da -1 * 0 işleminden doğar ve yalnızca bölme veya Object.is onu açığa çıkarır.

Alt normal sayılar, sıfır ile en küçük normal sayı arasındaki boşluğu doldurur. Üs alanı tamamen sıfırken örtük baştaki 1 düşer ve mantis kademeli olarak sıfıra doğru küçülür; bu da eşit olmayan iki float’ın farkının asla tam sıfıra yuvarlanmamasını garanti eder. Bedeli, düşen hassasiyet ve bazı donanımlarda sert bir performans uçurumudur. IEEE 754 dönüştürücüsündeki özel değer çipleri ±0, ±Infinity, NaN ve en küçük alt normal sayıyı yükler; böylece her bit desenini doğrudan inceleyebilirsiniz.

float, double, FP16 ve bfloat16 karşılaştırması

Aynı standart, aralık ve kayan nokta hassasiyeti için farklı bütçeler:

BiçimToplam bitÜsMantisYaklaşık onluk basamakMaksimum sonluTipik kullanım
binary16 (FP16)16510~3.365504GPU çıkarımı, kompakt saklama
bfloat161687~2.4~3.39 × 10³⁸ML eğitimi
binary32 (float)32823~7.2~3.40 × 10³⁸Grafik, sensörler, GPU
binary64 (double)641152~15.9~1.80 × 10³⁰⁸Geri kalan her yerde varsayılan

Üs bitleri aralık satın alır, mantis bitleri hassasiyet. FP16 bütçesini hassasiyete harcar ve 65504’te tavan yapar; bu da gerçek gradyan değerlerine o kadar yakındır ki Infinity değerine taşma sıradan bir tehlike haline gelir. bfloat16 ise ters takası seçer: FP32’nin sekiz üs bitini korur ve yedi mantis bitiyle idare eder; böylece FP16’yı taşıracak değerler sorunsuz geçer ve eğitim nadiren kayıp ölçeklemesine ihtiyaç duyar.

Varsayılanınız double olsun. Bir bellek ya da bant genişliği darboğazını ölçtükten sonra daha dar bir biçime inin ve bu daralmanın neye mal olduğunu, aynı değeri dönüştürücüde her biçime girerek kontrol edin.

Kayan nokta hassasiyeti için pratik kurallar

  • Hesaplanmış float değerlerinde asla == kullanmayın. Verinize göre boyutlandırılmış, mutlak ve bağıl toleransı birleştiren bir ölçüt kullanın.
  • Number.EPSILON değerini eşik saymayın. O yalnızca 1.0 noktasındaki ızgara aralığını tarif eder.
  • Parayı tam sayı alt birimlerde ya da bir decimal türünde tutun ve decimal değerleri her zaman karakter dizilerinden kurun.
  • Uzun toplamları telafi edin; Kahan, Neumaier ya da math.fsum işinizi görür.
  • Neredeyse eşit nicelikleri çıkaran formülleri yeniden düzenleyin, çevrelerindeki toleransları sıkılaştırmak yerine.
  • İnsanlar için açıkça biçimlendirin; toFixed, bir f-string ya da printf kullanın. Kullanıcılara asla varsayılan bir repr göndermeyin.
  • Sayıları servis sınırları arasında karakter dizisi ya da tam sayı olarak gönderin. Bir double üzerinden geçen JSON gidiş-dönüşü sessiz ve kayıplıdır.
  • Para sütunları için FLOAT değil NUMERIC seçin. Bunu yayına aldıktan sonra düzeltmek bir veri göçü ve bir mutabakat çalışması demektir.
  • Bir değer imkânsız göründüğünde bit düzeyinde düşünün. Aynı alışkanlık bitwise işlemler rehberimizin de itici gücüdür ve kayan nokta gizemlerini kontrol edebileceğiniz aritmetiğe dönüştürür.

SSS

Kayan nokta matematiği bozuk mu?

Kayan nokta matematiği bozuk değildir; tam olarak IEEE 754’ü izler. Standart, gerçek sayıları sonlu sayıda ikilik basamakla temsil eder ve 0.1 gibi onluk kesirlerin tam bir ikilik karşılığı yoktur; bu yüzden temsil edilebilir en yakın değer saklanır. Bozuk olan kısım, her onluk sayının bu düzene sığacağı beklentisidir.

Python neden 0.1 sayısını 0.1 olarak ama 0.1 + 0.2 sonucunu 0.30000000000000004 olarak yazdırır?

Python’un repr işlevi en kısa gidiş-dönüş biçimlendirmesini kullanır: aynı double’a geri ayrıştırılan en kısa onluk karakter dizisini yazdırır. "0.1" kendi double’ını zaten benzersiz biçimde tanımlar. 0.1 + 0.2 toplamı ise "0.3" karakter dizisinin eşlendiğinden farklı bir double’dır; dolayısıyla biçimlendirici, tek anlama gelmeyi sürdürmek için on yedi basamağın tamamını yazmak zorundadır.

İki float değerini karşılaştırmak için Number.EPSILON’u neden kullanmamalıyım?

Number.EPSILON (yaklaşık 2.22e-16), 1 ile bir sonraki double arasındaki boşluktur; evrensel bir hata bütçesi değildir. Float aralığı ikinin her kuvvetinde iki katına çıkar, dolayısıyla 1e9 civarında komşu double’lar arasında yaklaşık 1.19e-7 mesafe bulunur. Oradaki gerçek bir yuvarlama hatası EPSILON değerini aşar ve karşılaştırma daima yanlış olur. Bağıl ya da birleşik bir tolerans kullanın.

Sonda yuvarlarsam para için kayan nokta kullanabilir miyim?

Sonda yuvarlamak kayan noktalı parayı kurtarmaz. Hatalar toplamalar, çarpmalar ve çok adımlı paylaştırmalar boyunca birikir; ayrıca yuvarladığınız an toplamı değiştirir. 19.99 * 100 işlemi daha şimdiden 1998.9999999999998 veriyor. Denetim, yeniden üretilebilir ve tam bir aritmetik ister: tam sayı cent, decimal, BigDecimal ya da SQL NUMERIC.

Bir double içinde hangi onluk sayılar tam olarak saklanabilir?

Yalnızca, biçimin hassasiyeti dahilindeki m ve n tam sayıları için m / 2ⁿ olarak ifade edilebilen değerler. Bu, 0.5, 0.25, 0.125, 0.75 ve 2.5 ile birlikte 2⁵³ değerine kadar her tam sayıyı kapsar. Önce kesri sadeleştirin: paydada kalan her 5 çarpanı (0.1, 0.2, 0.3 ya da 0.7’de olduğu gibi) değerin yaklaşık tutulduğu anlamına gelir.

0.1 + 0.2 === 0.3 neden yanlışken 0.5 + 0.25 === 0.75 doğru?

Çünkü 0.5, 0.25 ve 0.75 sırasıyla 2⁻¹, 2⁻² ve 2⁻¹ + 2⁻² değerleridir; hepsi tam temsil edilebilir ve toplamları yuvarlama gerektirmez. 0.1, 0.2 ve 0.3 ise ayrı ayrı yaklaşık saklanır; ilk ikisinin toplamını yuvarlamak da 0.3 sayısının eşlendiği double’ın bir ULP üstüne düşer.

Bu her programlama dilinde oluyor mu?

IEEE 754 ikilik kayan nokta üzerine kurulu her dil aynı davranışı gösterir: JavaScript, Python, Java, C, C++, Go, Rust, Swift ve SQL’in FLOAT türü. Farklı olan, varsayılan yazdırma hassasiyeti ve C#‘ın decimal türü ya da Python’un decimal modülü gibi tam bir onluk türünün dille birlikte gelip gelmediğidir.

Etiketler: floating-point ieee-754 javascript python numbers