Merhaba. Bir önceki yazıda ondalıklı sayıların bilgisayarda nasıl tutulduğuna değindim ve bilgisayar mimarisine ait kısıtların ilginç davranışlara neden olduğundan bahsettim. Bu yazıda ilginç davranışlara birkaç örnek vereceğim.
Bu ilginçliklerin ilki, bir önceki yazıda söz ettiğim nedenlerden ötürü özellikle sıfıra yakın sonuçları sıfırla karşılaştırmak yerine sıfıra kabul edilebilir uzaklıkta olup olmadığına bakma gereksinimidir. Bununla ilgili bir örneğe bazı ara konulara değindikten sonra yanında yıldız imiyle (*) yer vereceğim.
Önceki yazıda verdiğim örnekler çoğunlukla tek değerlikli (single) sayılarlaydı. Buradan, çift değerlikli (double) sayılarla hesaplamada sorunlar olmadığı anlamı çıkmamalı. Double'da katsayı daha çok bitle temsil edildiğinden (önceki yazıda) hesaplamadaki hata onyedinci ondalıkta ortaya çıkıyordu. Single için hata dokuzuncu ondalıktaydı. Önceki yazının ilk örneğinde Excel'deki sonuçta onaltıncı ondalıkta hata var (hata on kat büyüdü). Ben örneklerde tek duyarlıklı sayıları, hesaplamalar ve hataları göstermek kolay olsun diye seçtim.
Hatalar yalnız sıfıra yakın sayılarda ortaya çıkmaz. Katsayı bitlerinin sınırlı olması büyük sayılarda da hataya neden olur. Katsayı için 23 bit ayrılmış olması 23'ten fazla sayıda kesirle ifade edilebilen sayılarda sorun çıkarır. Hexpert'te bir deney yaptım: 00H ofsetine gidip FP32 hücresine 16 777 215, 04H ofsetine 16 777 216 vb. 16 777 220'ye kadar olan sayıları girdim.
İlk sayıda tüm katsayı bitlerini tükettim. 16 777 215, 23 kesrin toplamı olarak yazılıyor (yukarıda). Ardışığı, 1*224 olarak ifade ediliyor. Ama üs o kadar büyüdü ki 224+1'in 1'ini yazacak bit kalmadı. Dikkat edilirse bir fazlası olsa bile 04H'daki sayıyla 08H'daki sayı ve 0CH'daki sayıyla 10H'daki sayılar aynı. 224 ile 225 arasındaki sayılar bilgisayarda tek değerlikli olarak saklanırken tek sayılar yazılamıyor. Benzer şekilde 225 ile 226 arasındaki sayılarda dördün katı olmayanlar yazılamıyor. Yeterince büyük sayılardaki küçük artımların anlamsız olduğu durumda sorun yok ama problemine göre 224 çok büyük bir sayı olmayabilir. Bu arada 224'ü 32-bit'lik tamsayıyla (integer) hala kesin bir şekilde gösterebilirim.
Sonuçta bitlerin sonlu olmasından ötürü sayılar belli bir bitte kesilmektedir. Bu konudaki hata örnekleri çoğaltılabilir. Örn. π ve e gibi rasyonel sayılar kümesinde olmayan, sonlu sayıda ondalıkla gösterilemeyen sayılar hiçbir zaman bilgisayarda tam olarak ifade edilemez. Üstelik sayılamaz sonsuzlukta olan gerçel sayılar kümesinin, sonlu sayıda bitle gösterilmesi teoride zaten olanaksızdır. Bunun pratiğe dökülebilmesinin tek nedeni mühendislik hesaplarındaki sonuçların yuvarlandığında "yeterince" tutarlı olmasıdır. Bu tür hatalara kesme veya yuvarlama hatası denir. Sayısal analiz'de bu iki hata aynı değilse de mikroişlemcinin davranışı bu tür sayılarda yuvarlama yapmak olduğundan bu anlamda birbirlerinin yerine kullanılabilirler.
Kayar noktalı sayıları karşılaştırırken sıfır değil sıfıra yeterince yakın olmasıyla ilgili olarak bir örnek ele alalım*. sin(30) = sin(π / 6) = 0.5 olduğunu biliyoruz, doğal olarak sin(π / 6) - 0.5 = 0 olur. printf("%e\n", sin( M_PI / 6.0 )); ekrana 5.000000e-01 yazar. Yalnız yukarıda yazdığım gibi pi sayısında bir yaklaşıklık var. Sinüs fonksiyonunda da MacLaurin serisinin kesilmesinden dolayı bir yaklaşıklık olmalı. Bunun sonucunda hata birikmesinden ötürü gördüğümüz 5.000000e-01'in aslında 2-1 olmadığı tahmin edilebilir. printf("%e\n", sin( M_PI / 6.0 ) - 0.5); ifadesi ekrana -5.551115e-17 yazar. Farkın sıfır olması değil sıfıra "yeterince" yakın olması dikkate alınmalıdır. Örneğin sonucun sıfıra olan uzaklığının, makina epsilonu gibi yeterince küçük bir sayı kadar olup olmadığına bakılabilir. Yani:
if(fabs(sin(M_PI / 6.0) - 0.5) < DBL_EPSILON)
Üssün 00h ve 0FFh olmasının özel anlamları vardır. Üs 0FFh ve katsayı sıfırsa işaret bitine göre sayı, pozitif veya negatif sonsuz değerini alır. Çok büyük sayıların çok küçük sayılara bölünmesiyle çıkacak sonuç, yazmaca sığmıyorsa sonsuz değeri döner. Üs 0FFh ve katsayı sıfırdan farklıysa bu NaN yani "Not a Number" değeridir. Bu da matematikteki belirsiz işlemlerin sonucunda (Örn. 0/0, 00, ∞ - ∞) veya FPU'da yapılmak istenen işlem tanımsızsa ortaya çıkar ve NaN'la yapılan bütün işlemlerin sonucu da NaN'dır.
Üssün sıfır olması ikinci özel durumdur. Üs sıfır ve katsayı da sıfırsa (yani 00 00 00 00) sayı sıfırdır ama üssün sıfır olması durumu bundan daha karışıktır. Şöyle ki, daha önce her katsayıya 1 eklendiğini söylemiştim. Katsayı her zaman 1'e eşit veya 1'den büyükse hiçbir zaman sıfır elde edilemez. O nedenle üs sıfır olduğunda kural bozulur ve üsten 127 çıkarılmaz. Bunun yerine üs -126 alınıp katsayıya 1 eklenmesi kuralı bozulur yani d0 = 0 olur (d0'ın ne olduğu bir önceki yazıda bulunabilir). Üssün -126 olduğu sayılarla çıkartma yapılırken aradaki fark 2-127 mertebesinde olacaktır:
1.5 * 2-126 - 1.375 * 2-126 = 0.125 * 2-126 = 1.25 * 2-127
Yukarıdaki işlemin taşmaya (aslında doğru terim overflow değil underflow) neden olmaması, aradaki farkın bilgisayarda gösterilebilmesi için küçük sayılara ihtiyaç vardır. Bu nedenle [0, 1] aralığı ±2-126 için ondalıklı sayı standardına eklenmiştir. Bu tür sayılara subnormal sayılar denir (normalize olmayan). Subnormal sayılar üzerinde işlem yapılırken FPU, DE bayrağına 1 verir. Bunun anlamı artık DEnormal (subnormal) sayılar üzerinde işlem yapıldığı ve en küçük değerli ondalıkların kaybolabileceğidir. Başka bir deyişle işlemci, subnormal sayıların fark ve bölüm işlemlerinde sonucun kesinliğini garanti edemez. Yukarıda ** olarak işaretlediğim cümlede, çift duyarlıklı sayılarda iki farklı en küçük değer yazmıştım. Büyük olan normal sayıların en küçüğü, diğeri de subnormal sayıların en küçüğüdür.
Örnek Kod:
Subnormal sayılarda işlemler için şu kodu yazdım:
Bir union'la, double değere bir işaretsiz tamsayı veya 8 byte olarak eriştim. d1, d2 ve d3 olarak union uSayi türünde üç değişken tanımlayıp normal ondalıklı sayı sınırının en altına yakın değerler verdim ve birbirlerinden çıkarttım. Ara sonuçları t1, t2 ve t3 değişkenlerinde tuttum. Kodu debugger'la inceleyeceğim için aşağıdaki gibi derledim:
gcc -mfpmath=387 -g kod2.c
gdb -tui a.out
gcc, ondalıklı sayıları normalde SSE komutlarıyla işler. Ben FPU'yu görmek istediğim için 387 komutlarına zorladım.
Programı gdb'nin TUI'siyle açtım. assembly'de intel yazımını tercih ettiğim için şu iki komutu verdim:
(gdb) set disassembly-flavor intel
(gdb) layout asm
Yukarıdaki ekran görüntüsünde d1, d2 ve d3'e yapılan atamalar 0x400574 adresinden itibaren açıkça görülebiliyor. İlk çıkarma işlemi 0x4005a4'te, ikincisi 0x4005af'de, son çıkarma da 0x4005b7 adresindeki fsub ile yapılıyor. break *main+71 komutuyla ilk çıkarmaya bir breakpoint koyup run komutuyla kodu çalıştırdım. Programın durduğu yerde info float komutuyla FPU stack'e baktım. Elle girdiğim sayılar FPU'da işleme hazır :
nexti komutunu verip çıkarmayı gerçekleştirdim. Tekrar info float yazıp stack'te çıkartmanın sonucuna baktım. Diğer çıkartma da aynı şekilde gerçekleşecek. Bu nedenle until *main+90 komutuyla son çıkarmaya ilerledim. Bu adımda info float yazınca FPU Status Word'de önceden 0x3800 olan değer 0x3802 oldu. DE yani Denormalized bayrağı bir olarak ayarlandı. nexti komutuyla son çıkartmayı da yaptım. Sonuç +2,781342323134001729e-309 bulundu. Son olarak cont komutuyla programı tamamladım. Dökümanlarda ve Wikipedia'da, komutların denormalized değerlerle, normal değerlerle olduğundan daha yavaş çalıştığı belirtiliyor. Bu nedenle hızın, sonuçların kesinliğinden daha önemli olduğu uygulamalarda denormalize değerlerin kullanımın kapatılması öneriliyor.
https://blogs.oracle.com/d/subnormal-numbers adresinde derleyicinin -fns parametresinin, çalışma sırasında oluşacak subnormal sonuçları sıfıra yuvarlamasını sağlayacağından söz edilmiş ancak ben gcc'de böyle bir parametre bulamadım.
Bu ilginçliklerin ilki, bir önceki yazıda söz ettiğim nedenlerden ötürü özellikle sıfıra yakın sonuçları sıfırla karşılaştırmak yerine sıfıra kabul edilebilir uzaklıkta olup olmadığına bakma gereksinimidir. Bununla ilgili bir örneğe bazı ara konulara değindikten sonra yanında yıldız imiyle (*) yer vereceğim.
Önceki yazıda verdiğim örnekler çoğunlukla tek değerlikli (single) sayılarlaydı. Buradan, çift değerlikli (double) sayılarla hesaplamada sorunlar olmadığı anlamı çıkmamalı. Double'da katsayı daha çok bitle temsil edildiğinden (önceki yazıda) hesaplamadaki hata onyedinci ondalıkta ortaya çıkıyordu. Single için hata dokuzuncu ondalıktaydı. Önceki yazının ilk örneğinde Excel'deki sonuçta onaltıncı ondalıkta hata var (hata on kat büyüdü). Ben örneklerde tek duyarlıklı sayıları, hesaplamalar ve hataları göstermek kolay olsun diye seçtim.
Hatalar yalnız sıfıra yakın sayılarda ortaya çıkmaz. Katsayı bitlerinin sınırlı olması büyük sayılarda da hataya neden olur. Katsayı için 23 bit ayrılmış olması 23'ten fazla sayıda kesirle ifade edilebilen sayılarda sorun çıkarır. Hexpert'te bir deney yaptım: 00H ofsetine gidip FP32 hücresine 16 777 215, 04H ofsetine 16 777 216 vb. 16 777 220'ye kadar olan sayıları girdim.
Kayar noktalı sayıları karşılaştırırken sıfır değil sıfıra yeterince yakın olmasıyla ilgili olarak bir örnek ele alalım*. sin(30) = sin(π / 6) = 0.5 olduğunu biliyoruz, doğal olarak sin(π / 6) - 0.5 = 0 olur. printf("%e\n", sin( M_PI / 6.0 )); ekrana 5.000000e-01 yazar. Yalnız yukarıda yazdığım gibi pi sayısında bir yaklaşıklık var. Sinüs fonksiyonunda da MacLaurin serisinin kesilmesinden dolayı bir yaklaşıklık olmalı. Bunun sonucunda hata birikmesinden ötürü gördüğümüz 5.000000e-01'in aslında 2-1 olmadığı tahmin edilebilir. printf("%e\n", sin( M_PI / 6.0 ) - 0.5); ifadesi ekrana -5.551115e-17 yazar. Farkın sıfır olması değil sıfıra "yeterince" yakın olması dikkate alınmalıdır. Örneğin sonucun sıfıra olan uzaklığının, makina epsilonu gibi yeterince küçük bir sayı kadar olup olmadığına bakılabilir. Yani:
if(fabs(sin(M_PI / 6.0) - 0.5) < DBL_EPSILON)
doğru ifadedir. Makina epsilonunun 1 komşuluğunda anlamlı olduğunu yazmıştım. Bu nedenle aslında bu yaklaşım da yeterince doğru değildir. Yapılmak istenen işlemin hassaslığı yanında makina epsilonu büyük kalabilir. Çift duyarlıklı sayılarda en küçük 2.225 * 10-308 (aslında 4.94 * 10-324 ama bu da ileride açıklamak üzere kenarda dursun**) olduğu halde epsilon 10-16 mertebesindedir. Doğru "yeterince küçük" değeri seçmek, bu değerin sayı doğrusunun her yerinde farklı olması nedeniyle zor bir sorundur. Örneğin, 224 ile 225 arasındaki sayılarda 3 yeterince küçükken 225 ile 226 arası sayılarda değildir.
Üssün sıfır olması ikinci özel durumdur. Üs sıfır ve katsayı da sıfırsa (yani 00 00 00 00) sayı sıfırdır ama üssün sıfır olması durumu bundan daha karışıktır. Şöyle ki, daha önce her katsayıya 1 eklendiğini söylemiştim. Katsayı her zaman 1'e eşit veya 1'den büyükse hiçbir zaman sıfır elde edilemez. O nedenle üs sıfır olduğunda kural bozulur ve üsten 127 çıkarılmaz. Bunun yerine üs -126 alınıp katsayıya 1 eklenmesi kuralı bozulur yani d0 = 0 olur (d0'ın ne olduğu bir önceki yazıda bulunabilir). Üssün -126 olduğu sayılarla çıkartma yapılırken aradaki fark 2-127 mertebesinde olacaktır:
1.5 * 2-126 - 1.375 * 2-126 = 0.125 * 2-126 = 1.25 * 2-127
Yukarıdaki işlemin taşmaya (aslında doğru terim overflow değil underflow) neden olmaması, aradaki farkın bilgisayarda gösterilebilmesi için küçük sayılara ihtiyaç vardır. Bu nedenle [0, 1] aralığı ±2-126 için ondalıklı sayı standardına eklenmiştir. Bu tür sayılara subnormal sayılar denir (normalize olmayan). Subnormal sayılar üzerinde işlem yapılırken FPU, DE bayrağına 1 verir. Bunun anlamı artık DEnormal (subnormal) sayılar üzerinde işlem yapıldığı ve en küçük değerli ondalıkların kaybolabileceğidir. Başka bir deyişle işlemci, subnormal sayıların fark ve bölüm işlemlerinde sonucun kesinliğini garanti edemez. Yukarıda ** olarak işaretlediğim cümlede, çift duyarlıklı sayılarda iki farklı en küçük değer yazmıştım. Büyük olan normal sayıların en küçüğü, diğeri de subnormal sayıların en küçüğüdür.
Örnek Kod:
Subnormal sayılarda işlemler için şu kodu yazdım:
#include<stdio.h>
#include<stdint.h>
union uSayi {
unsigned char bayt[8];
uint64_t q;
double s;
};
int main() {
union uSayi d1, d2, d3;
double t1, t2, t3;
d1.q = 0x0018000000000000;
d2.q = 0x0016000000000000;
d3.q = 0x0014000000000000;
//printf("%e %e %e\n", d1.s, d2.s, d3.s);
// 1.5 * 2^-1022 - 1.25 * 2^-1022 = 0.25 * 2^-1022
t1 = d1.s - d3.s;
// 1.5 * 2^-1022 - 1.375 * 2^-1022 = 0.125 * 2^-1022
t2 = d1.s - d2.s;
// 0.25 * 2^-1022 - 0.125 * 2^-1022 = 0.125 * 2^-1022
t3 = t1 - t2;
return 0;
}
#include<stdint.h>
union uSayi {
unsigned char bayt[8];
uint64_t q;
double s;
};
int main() {
union uSayi d1, d2, d3;
double t1, t2, t3;
d1.q = 0x0018000000000000;
d2.q = 0x0016000000000000;
d3.q = 0x0014000000000000;
//printf("%e %e %e\n", d1.s, d2.s, d3.s);
// 1.5 * 2^-1022 - 1.25 * 2^-1022 = 0.25 * 2^-1022
t1 = d1.s - d3.s;
// 1.5 * 2^-1022 - 1.375 * 2^-1022 = 0.125 * 2^-1022
t2 = d1.s - d2.s;
// 0.25 * 2^-1022 - 0.125 * 2^-1022 = 0.125 * 2^-1022
t3 = t1 - t2;
return 0;
}
Bir union'la, double değere bir işaretsiz tamsayı veya 8 byte olarak eriştim. d1, d2 ve d3 olarak union uSayi türünde üç değişken tanımlayıp normal ondalıklı sayı sınırının en altına yakın değerler verdim ve birbirlerinden çıkarttım. Ara sonuçları t1, t2 ve t3 değişkenlerinde tuttum. Kodu debugger'la inceleyeceğim için aşağıdaki gibi derledim:
gcc -mfpmath=387 -g kod2.c
gdb -tui a.out
gcc, ondalıklı sayıları normalde SSE komutlarıyla işler. Ben FPU'yu görmek istediğim için 387 komutlarına zorladım.
Programı gdb'nin TUI'siyle açtım. assembly'de intel yazımını tercih ettiğim için şu iki komutu verdim:
(gdb) set disassembly-flavor intel
(gdb) layout asm
Yukarıdaki ekran görüntüsünde d1, d2 ve d3'e yapılan atamalar 0x400574 adresinden itibaren açıkça görülebiliyor. İlk çıkarma işlemi 0x4005a4'te, ikincisi 0x4005af'de, son çıkarma da 0x4005b7 adresindeki fsub ile yapılıyor. break *main+71 komutuyla ilk çıkarmaya bir breakpoint koyup run komutuyla kodu çalıştırdım. Programın durduğu yerde info float komutuyla FPU stack'e baktım. Elle girdiğim sayılar FPU'da işleme hazır :
nexti komutunu verip çıkarmayı gerçekleştirdim. Tekrar info float yazıp stack'te çıkartmanın sonucuna baktım. Diğer çıkartma da aynı şekilde gerçekleşecek. Bu nedenle until *main+90 komutuyla son çıkarmaya ilerledim. Bu adımda info float yazınca FPU Status Word'de önceden 0x3800 olan değer 0x3802 oldu. DE yani Denormalized bayrağı bir olarak ayarlandı. nexti komutuyla son çıkartmayı da yaptım. Sonuç +2,781342323134001729e-309 bulundu. Son olarak cont komutuyla programı tamamladım. Dökümanlarda ve Wikipedia'da, komutların denormalized değerlerle, normal değerlerle olduğundan daha yavaş çalıştığı belirtiliyor. Bu nedenle hızın, sonuçların kesinliğinden daha önemli olduğu uygulamalarda denormalize değerlerin kullanımın kapatılması öneriliyor.
https://blogs.oracle.com/d/subnormal-numbers adresinde derleyicinin -fns parametresinin, çalışma sırasında oluşacak subnormal sonuçları sıfıra yuvarlamasını sağlayacağından söz edilmiş ancak ben gcc'de böyle bir parametre bulamadım.
Hiç yorum yok:
Yorum Gönder