6 Nisan 2011 Çarşamba

İlkel Türler-Kayan Noktalı Sayılar

İlkel türler hakkındaki dizimizin üçüncü yazısında, matematikteki gerçel sayıların temsil edilmesinde kullanılan kayan noktalı sayı türlerine göz atacağız. Tamsayı türlerinin anlatıldığı yazıdan🔎 da tanıdık gelecek çoğul eki bu amaçla kullanılabilecek iki türe işaret ediyor: float ve double.

float ve double türlü değerlerin bellek gösterimi, gerçel sayıların temsili için tanımlanan ve pratikte tüm donanımlar (ve programlama dilleri) tarafından benimsenmiş olan IEEE754 standardına göre oluşturulur. Önerilen gösterimin ayrıntısına girmeden önce, anılan türlerin kullanımına dair şu uyarı yerinde olacaktır: tanım aralığındaki tüm değerlerin kusursuz bir biçimde temsil edildiği tamsayı türlerinin aksine, kayan noktalı sayı türleri tanım aralıklarındaki gerçel sayıların sadece bazılarını kusursuz olarak temsil edebilir, diğer sayılar ancak yaklaşık olarak temsil edilebilirler. Bu, tanım aralığındaki gerçel sayılar ile kayan noktalı sayılar arasında bire-bir bir ilişki olmadığı anlamına gelir. Bir diğer deyişle, aynı kayan noktalı sayı birden çok—aslında, sonsuz—gerçel sayıyı temsil eder. Dolayısıyla, aşağıdaki kod parçasının üreteceği çıktı bizi şaşırtmamalıdır. 3.14'ün yaklaşık temsil edilmesi nedeniyle 9.8596 olması gereken çarpma sonucu 9.859601 olarak hesaplanacak ve son satırdaki eşitlik denetimi beklenmedik bir yanıt üretecektir.1
float pi = 3.14f;
float piKare = 9.8596f;
System.out.println(pi * pi == piKare); // ⇒ false
Değişkenlerin ilklenmesinde kullanılan ilk değerlerin sonundaki fF de olabilir—sabitlerin float türünden ele alınması gerektiğini ifade etmek için kullanılır. Bu niteleyicinin yokluğunda, sabitin türü double olarak hesaba katılacaktır.2

Kayan noktalı tanımlayıcılara değer sağlamakta kullanılan sabitler değişik şekillerde yazılabilir.
double bütçeAçığı = 8000000000000000;
bütçeAçığı = 8e15; // 8.0e15 olarak da yazılabilir
bütçeAçığı = 80e14; // Yukarıdakiler ile aynı.
final float üç = 3.f; // 3.0f yazmak daha iyi bir fikir.

double dSayı = 0x49.0p0; // dSayı ← 73 (2^0(4*16^1+9*16^0))
dSayı = 0x4.9p4; // dSayı ↞ 73 (2^4(4*16^0+9*16^-1))
dSayı = 0x1.24p6; // dSayı ↞ 73 (2^6(1*16^0+2*16^-1+4*16^-2))
dSayı = 0xA.5p3; // dSayı ↞ 82.5 (2^3(10*16^0+5*16^-1))
Yukarıda verilen seçeneklerin kullanımında şu noktaların akılda tutulması yararlı olacaktır.
  • Sayının kesir kısmının var olması durumunda, 16'lı taban sadece bilimsel gösterimle birlikte kullanılabilir. Buna göre, Java derleyicisi 0x49 ve 0x49.0p0 seçeneklerini kabul ederken 0x49.0'ı reddedecektir.
  • Bilimsel gösterim sayılarda ölçekleme yapılırken, çarpan olarak 16'lı tabanda 2, 10'lu tabanda ise 10 kullanılır.

İlk kod parçasındaki eşitlik denetiminde ortaya çıkan beklenmedik durumun nedenini daha iyi anlayabilmek için IEEE754'ün koyduğu kurallara bakalım. Öncelikle, gerçel sayıların temsilinde kullanılan kayan noktalı değerlerin içeriklerinin yorumlanmasında ortaokul yıllarından hatırlayacağınız (normalize edilmiş) bilimsel gösterimin temel alındığını söyleyerek başlayalım: kayan noktalı değerlerin içerikleri ∓i0.i-1i-2...x2n şeklinde yorumlanır. Dolayısıyla, normalize edilmiş bilimsel gösterimde i0'ın 0 olamayacağı düşünüldüğünde, kayan noktalı sayı bulunduran bellek bölgelerinin aşağıdaki gibi hesaplanan bir sayıyı tuttuğunu söyleyebiliriz.

∓1.i-1i-2...x2n = ∓(1 + i-12-1 + i-22-2 + ...)2n

Bir diğer deyişle, gerçel sayılar işaret bilgisi, kesir kısmındaki ikili basamaklar ve ölçeklemek amacıyla kullanılan üs değeri kullanılarak temsil edilirler. İşaret bilgisinin bir ikil ile gösterildiği float ve double türleri arasındaki fark, diğer iki özellik için ayrılan alanın büyüklüğünden kaynaklanır. Bu alanların büyüklüğü aşağıdaki tabloda verilmiştir.

Kayan noktalı sayı türleri ve özellikleri
Özellikdoublefloat
Uzunluk (ikil)İşaret11
Üs118
Kesir5223
Duyarlık (Ondalık)167
Merkez1023127

Temsil edilebilecek gerçel sayılara dair yoruma girişmeden önce, tablodaki özelliklerin sayının değerini nasıl etkilediğine bir bakalım.
  • İşaret ikilinin 0 olması sayının artı, 1 olması ise eksi olduğunu gösterir. Bu ikilin 1'e tümlenmesi—yani, 1 iken 0 veya 0 iken 1 yapılması—temsil edilen sayıyı -1 ile çarpma etkisini yaratır. Bunun doğal bir sonucu olarak, artı ve eksi sıfırdan bahsedilmesi de mümkündür.
  • Kesir kısmının en küçük değeri—tüm ikillerin 0 olması durumu—0 olabilirken en büyük değeri—tüm ikillerin 1 olması durumu—1-2-kesir uzunluğu olabilir. Bu değerlerin, başta varsayılan 1 değerine eklenmesi mantis değerini [1..2-2-kesir uzunluğu] aralığına taşıyacaktır. Ayrıca, 52 ve 23 ikili basamak duyarlığında temsil edilen sayılarımızın ondalık basamak türünden en yüksek duyarlığı, sırasıyla, 16 ve 7 olacaktır.3
  • [1..2-2-kesir uzunluğu] aralığındaki mantisin eksi olmayan üs değerleriyle ölçeklenmesi sonucunda mutlak değerce 1'den küçük sayıların temsil edilemeyecek olması nedeniyle, üs kısmı tablodaki Merkez adlı maddedeki değerin sıfır noktası seçildiği bir tamsayı çizgisine eşlenir. Örneğin, float türlü bir sayının üs kısmının 130 olması, ölçeklemekte kullanılan üs değerinin 3 (130-127) olarak alınmasına neden olacaktır. Ayrıca, üs kısmının 1'lerle dolu gösterimi ∓∞ ve temsil edilemeyecek mutlak değerce büyük sayıları göstermek için kullanıldığından, en büyük üs değeri 2üs uzunluğu-1-merkez olacaktır. Benzer şekilde, üs kısmının 0'larla dolu gösterimi ∓0 ve temsil edilemeyecek mutlak değerce küçük sayıları göstermekte kullanıldığından, en küçük üs değeri de 1-merkez olacaktır.


Buna göre, double ve float türlerince temsil edilebilecek en büyük değer, sırasıyla, (2-2-52)22046-1023 ve (2-2-23)2254-127 olarak hesaplanacaktır.4 Temsil edilebilecek mutlak değerce en küçük sayılar ise, her iki tür için de 21-merkez formülü kullanarak bulunabilir. Bu değerler ve Java programları içinden bu değerlere atıfta bulunmak için kullanılabilecek simgesel sabit adları aşağıdaki tabloda verilmiştir.

Kayan noktalı sayıların değer sınırları
Sınır türüdoublefloat
En büyük21024-2971Double.MAX_VALUE2128-2104Float.MAX_VALUE
En küçük2-10222-126
En küçük (özel)2-1075Double.MIN_VALUE2-150Float.MIN_VALUE

Yukarıdaki tablonun son sırasında verilen değerler, üs kısmının tümüyle 0 olduğu gösterimde kullanılan özel hesaplamanın üreteceği değerlerdir. Bu özel gösterime sahip bir kayan noktalı sayının temsil ettiği değerin hesaplanması esnasında kesir kısmına 1 eklenmez ve sonuç kesir kısmı ile 2-merkez'in çarpılması yoluyla elde edilir. Bu özel uygulama sayesinde, mutlak değerce daha küçük sayıların temsil edilmesi olanaklı kılınır.

Verilen sınırlar göz önünde bulundurularak, kayan noktalı sayı türlerinin hangi aralıktaki gerçel sayıları temsil edebileceği aşağıdaki şekille özetlenebilir. Şeklin incelenmesi sırasında temsil edilebilir aralıktaki sayıların bazıları dışında tümünün ancak yaklaşık olarak temsil edildiği unutulmamalıdır.


Kısaca açmak gerekirse;
  • ➃ 0 noktasını, ➁ ve ➅ nolu bölgeler ise 0 dışındaki temsil edilebilir gerçel sayı aralıklarını gösterir. ➁ nolu bölgenin sınırları, double türü için [-Double.MAX_VALUE..-Double.MIN_VALUE], float türü için [-Float.MAX_VALUE..-Float.MIN_VALUE] olarak belirlenirken, ➅ nolu bölgenin sınırları, sırasıyla, [Double.MIN_VALUE..Double.MAX_VALUE] ve [Float.MIN_VALUE..Float.MAX_VALUE] olarak belirlenmiştir.
  • ➀ ve ➆ nolu bölgeler mutlak değerce temsil edilemeyecek büyük gerçel sayıları, ➂ ve ➄ nolu bölgeler ise mutlak değerce temsil edilemeyecek küçük gerçel sayıları kapsar. ➀ ve ➆ nolu bölgelerdeki gerçel sayılar, sırasıyla, -∞ (Double.NEGATIVE_INFINITY veya Float.NEGATIVE_INFINITY) ve +∞ (Double.POSITIVE_INFINITY veya Float.POSITIVE_INFINITY) olarak ele alınırken, ➂ ve ➄ nolu bölgelere düşen gerçel sayılar, sırasıyla, -0 ve +0 olarak ele alınır.
Açıklık getirmek için aşağıdaki kod parçasını ele alalım. f1 değişkenini float tanımlayıcıların alabileceği en yüksek değerin 1 fazlasıyla ilklememiz sonrasında, ikinci satırda aynı değişkeni Float.MAX_VALUE ile karşılaştırıyoruz. Normalde yanlış olması gereken eşitlik denetiminin sonucu, söz konusu gerçel sayıların aynı kayan noktalı sayı ile temsil edilmesi nedeniyle, doğru sonucunu üretiyor. Sonraki iki satırda, en büyük float değer olarak hesaba katılan d1'in tersini bir kez daha d1 ile bölüyoruz. Sonuç, temsil edilemeyecek kadar küçük bir sayı olduğu için, 0.0 (ve -0.0) olarak ele alınıyor. Benzer bir durum, 5. ve 6. satırlar için de geçerli; bu sefer, işlemin sonucu temsil edilemeyecek kadar büyük olduğu için, f3 ve f4, sırasıyla, ∞ ve -∞ ile ilkleniyor. Kod parçasının son satırında ise, ∞'un -∞ ile bölünmesi, f5 değişkeninin "Sayı Değil" (İng., Not A Number) özel değeriyle ilklenmesi sonucunu doğuruyor.
float f1 = Float.MAX_VALUE + 1; // f1 ← 3.4028235E38
System.out.println(f1 == Float.MAX_VALUE); // ⇒ true
float f2 = 1 / f1 / f1; // f2 ← 0.0
f2 = -1 / f1 / f1; // f2 ↞ -0.0
float f3 = f1 * f1; // f3 ← Float.POSITIVE_INFINITY
float f4 = -f1 * f1; // f4 ← Float.NEGATIVE_INFINITY
float f5 = f3 / f4; // f5 ← Float.NaN

  1. Bazılarınız, böylesine can sıkıcı bir durumun varlığını IEEE754 komitesinin yaptığı bir gafa bağlayabilir. Ancak, bilgisayarların sonlu makine olma özelliği ve herhangi bir gerçel sayı çifti arasında sonsuz sayıda gerçel sayı olması birlikte düşünüldüğünde, bunun çok haksız bir yakıştırma olduğu kolaylıkla görülecektir.
  2. Aslına bakarsanız, f ekinin konmaması örneğimizde bir hataya neden olmayacaktır. Ancak, ilkleme sonrasında değişkenlerimize benzer bir şekilde niteleyicisiz bir şekilde kayan noktalı sayı sabitlerinin atanmaya çalışılması derleyici tarafından reddedilecektir.
  3. Duyarlığın hesaplanmasında yapılması gereken, ikili basamak sayısını log102 değeriyle çarpmak ve çıkan sonucu bir üst tamsayıya yuvarlamak.
  4. 1 + 1/2 + 1/4 + ... 1/2-n ifadesinin 2-2-n'e eşit olduğunu hatırlamanız, hesaplamanın doğruluğuna ikna olmanızı kolaylaştıracaktır.