22 Ağustos 2017 Salı

IEEE 754 - Float

Giriş
Float'un bir diğer ismi ise Single-precision floating-point format. Bu isimdeki single kelimesini gördükten sonra double tipinin niçin böyle isimlendirildiğini anlamak daha kolay.

Float 32 bitlik yer kullanırken double 64 bitlik (iki katı) yer kullanıyor. Bu yüzden ismi double olarak geçiyor.

Float Niçin Halen Var?
Double tipi float tipinin yaptığı her şeyi yapıyor. Öyleyse niçin float halen var. Sebepleri şöyle olabilir.
1. float daha az bellek harcar. Gömülü ortamlarda bu önemli olabilir.
2. GPU'da float işlemleri double işlemlerine göre çok daha hızlı. Oyun kodlarında bu önemli olabilir.

Precision
Not : Bir çok yazıda precision yerine "significant figure" kelimesi de kullanılıyor. Ben bu yazıda precision kelimesini kullandım, çünkü float yapısı içindeki significant kavramı ile karışmasın istedim.

Precision toplam hane sayısı anlamına gelir. Bu veri tipi ile toplam 7 hane büyüklüğüne (precision) kadar sayıları saklamak mümkün. Yani şöyle bir sayıyı saklayabiliriz.
3.141592 - Toplam 7 hane
Ya da şöyle
float: 0.3333333 - Toplam 7 hane, sıfır hariç
Not : Bir çok yazıda precision yerine "significant figure" kelimesi de kullanılıyor. Ben bu yazıda precision kelimesini kullandım, çünkü float yapısı içindeki significant kavramı ile karışmasın istedim.

En Küçük Sayı
C Dili
Açıklaması şöyle
FLT_MIN (normalized minimal positive value) = 1.175494351e-38F
FLT_TRUE_MIN (denormalized minimal positive value) = 1.401298464e-45F
FLT_TRUE şöyle hesaplanır.
float float_min()
{
  int exp_bit = CHAR_BIT * sizeof(float) - FLT_MANT_DIG;
  float exp = 2 - pow(2, exp_bit - 1);

  float m = pow(2, -(FLT_MANT_DIG - 1));

  return m * pow(2, exp);
}

int main()
{
    printf("%g\n", float_min());
}
Java Dili
Açıklaması şöyle
Even if the name is misleading, Float.MIN_VALUE is a positive value, higher than 0:
...
The real minimum float value is, in fact: -Float.MAX_VALUE.
Decimal Precision
Kaç tane ondalık hane saklanabileceği anlamına gelir. Bu veri tipi ile noktanın sol tarafında en fazla 6 tam sayı hanesi tutulabilir. Yani tüm 7 haneli tam sayılar float'a çevrilip tekrar tam sayıya çevrilemezler. Bazıları kaybolur. Ama tüm 6 haneli tam sayılar float'a çevrilip tekrar tam sayı haline gelebilir.
cout << numeric_limits<float>::digits10 <<endl;//6 verir
Yapısı
Float için kullanılan 32 bit aşağıdaki gibi bölümleniyor

   1 bit|7bit    |23 bit
    sign|exponent|mantissa veya significant
Yatay olarak bakarsak şöyle
sign : 1 bit
exponent : 8 bits
fraction : 23 bits
Türkçeleri şöyle: Sign = Yön, Exponent = Üst, Mantissa = Kök

Sign
Sayının artı veya eksi olduğunu belirtir. 1 ise eksi sayıdır.

Exponent
Exponent alanı 127'ye göre referans alınır. Örneğin exponent olarak 1 elde etmek istiyorsak 7 bitlik bu alana 128 yazmak gerekiyor çünkü 128- 127 = 1 .

Eğer exponent 255 ise, sayı infinity veya NaN'dır. Mantissa'ya göre karar verilir
If the exponent field is 255 then the number is infinity (if the mantissa is zero) or a NaN (if the mantissa is non-zero)
Eğer exponent 1- 254 arasında ise, normalized sayıdır ve şöyle hesaplanır
(1.0 + mantissa-field / 0x800000) * 2^(exponent-field-127)
Eğer exponent 0 ise, denormalized sayıdır ve şöyle hesaplanır
(mantissa-field / 0x800000) * 2^-126

Mantissa
Float veri tipinin mantissa alanı sadece 23 bit büyüklüğünde. Mantissa'nın solunda her zaman gizli 1 biti olduğu kabul edilir. Bu durumda 2^24'ten büyük (16,777,216) yani 16 milyondan biraz büyük tam sayıları float'a atamak hataya sebep oluyor. Mümkünse float yerine her zaman double kullanılmalı.

Buradaki açıklamada da mantissa'dan daha büyük bir int değer, float'a atandıktan sonra karşılaşılan bir hata açıklanmış

Alanlara erişmek
Şöyle yaparız.
float x = ...;
// split the given bits of sign exponent and fraction

unsigned int sign = (x & 0x80000000) >> 31;
unsigned int expo = (x & 0x7F800000) >> 23;
unsigned int frac = (x & 0x007fffff);
"Section 7.4 Beej's Guide to Network Programming" sayfasında IEEE 754 kullanmayan platformlarda, IEEE 754'e göre encoding işlemini yapmak için şöyle bir kod parçası var. Kod bazı durumları ele almadığı içi eksik, ancak mantığı göstermek için not aldım.
uint64_t pack754(long double f, unsigned bits, unsigned expbits)
{
  long long sign = ...;
  
  // calculate the binary form (non-float) of the significand data
  long long significand = ...;
  // get the biased exponent
  long long exp = ...
  // return the final answer
  return (sign<<(bits-1)) | (exp<<(bits-expbits-1)) | significand;
}

int main(void)
{
    float f = 3.1415926;
    uint32_t fi;

    printf("float f: %.7f\n", f);

    fi = pack754(f, 32, 8);
    printf("float encoded: 0x%08" PRIx32 "\n", fi);

    return 0;
}
Bellekte Temsili
Örnek
5.2 bellek şöyle temsil edilir.
0 10000001 01001100110011001100110    
S    E               M
S = 0 olduğu için pozitif bir sayıdır. Exponent = 129 ancak bias çıkarılınca 2 kalır. Yani 10^2'dir.
Mantissanın başında gizli bir 1 vardır.
Dolayısıyla
101... şeklinde düşünülürse zaten 5 gibi bir rakam elde edilir. Fraction şöyledir.
bits in M: 0   1    0     0      1       ... 
weight:    0.5 0.25 0.125 0.0625 0.03125 ... (take the half of the previous in each step)
Bu da toplayınca 0.1999998 gibi bir değer verir. Yani kabaca 5.2'yi elde ederiz.

Örnek
Elimizde 1864.78 sayısı olsun. Fraction kısmı bellekte şöyle temsil edilir.
number   : 1864.78
float    : 1864.780029  (actual nearest representation in memory)
integer  : 1864
fraction : 0.780029

 2 * 0.780029 = 1.560059  =>  integer part (1) fraction (0.560059)  =>  '1'
 2 * 0.560059 = 1.120117  =>  integer part (1) fraction (0.120117)  =>  '1'
 2 * 0.120117 = 0.240234  =>  integer part (0) fraction (0.240234)  =>  '0'
 2 * 0.240234 = 0.480469  =>  integer part (0) fraction (0.480469)  =>  '0'
 2 * 0.480469 = 0.960938  =>  integer part (0) fraction (0.960938)  =>  '0'
 2 * 0.960938 = 1.921875  =>  integer part (1) fraction (0.921875)  =>  '1'
 2 * 0.921875 = 1.843750  =>  integer part (1) fraction (0.843750)  =>  '1'
 2 * 0.843750 = 1.687500  =>  integer part (1) fraction (0.687500)  =>  '1'
 2 * 0.687500 = 1.375000  =>  integer part (1) fraction (0.375000)  =>  '1'
 2 * 0.375000 = 0.750000  =>  integer part (0) fraction (0.750000)  =>  '0'
 2 * 0.750000 = 1.500000  =>  integer part (1) fraction (0.500000)  =>  '1'
 2 * 0.500000 = 1.000000  =>  integer part (1) fraction (0.000000)  =>  '1'
Böylece şu değere erişiriz. 1864 ikili tabanda 11101001000 olarak tutulur.
decimal  : 11101001000
fraction : 110001111011
sign bit : 0
integer ve fraction toplam 10 haneden oluştuğu ve float 1.XXX şeklinde tutulduğu için exponent 10 olur. Bu değer normalize edersek şöyle bir sonuca varırız.
11101001000.110001111011  =>  1.1101001000110001111011

     exponent bias: 10
 unbiased exponent: 127
 __________________+____

   biased exponent: 137
   binary exponent: 10001001
Nihayet şöyle bir gösterime geliriz.
IEEE-754 Single Precision Floating Point Representation

  0 1 0 0 0 1 0 0 1 1 1 0 1 0 0 1 0 0 0 1 1 0 0 0 1 1 1 1 0 1 1 0
 |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -|
 |s|      exp     |                  mantissa                    |
İkilik Sayıdan 10'luk Sayıya Çevirmek
Not : IEEE 754 tek standart değildir. Geçmişte başka standartlar da kullanılmış. MIL-STD-1750A çevrim örneği için buraya bakabilirsiniz.

Kökün başına her zaman "1." sayısı getirilir. Exponent ve Yön ile çarpılır.

Örnek 1
0|10000000|11000000000000000000000 sign|exponent| mantissa

Mantissa'nın başına 1 getirdiğimiz ve ikilik taban kullandığımız için çevirmek istediğimiz sayı şudur
+(1.11)base 2 x 2^(128-127)
Daha sonra şu hale gelir.
1.11 sağdan başyarak yazılır. Önce ilk bit bir olduğu için 2^0, solundaki bit te bir olduğu için 2^-1, solundaki bir bir olduğu için 2^-2. Bu sayıların toplamı * exponent
(2^0 + 2^-1 + 2^-2) x 2^1







Hiç yorum yok:

Yorum Gönder