Autor: | Wojciech Muła |
---|---|
Dodany: | 24.11.2001 |
Aktualizacja: | 23.11.2002 |
Treść
W artykule zostanie zaprezentowane wyświetlanie liczb typu single (32-bitowych). Jednak algorytm jest o tyle uniwersalny, iż nie powinno być problemów z jego adaptacją dla innych formatów zmiennoprzecinkowych.
Jak powszechnie wiadomo, najmniej znaczącą cyfrę liczby całkowitej, można otrzymać jako resztę z dzielenia przez podstawę systemu — w naszym przypadku podstawą jest 10. Najbardziej znaczącą cyfrę liczby ułamkowej (mniejszej od 1), uzyskujemy poprzez przemnożenie jej przez podstawę systemu.
Ponieważ wszelkie operacje na liczbach zmiennoprzecinkowych są zawsze obarczone błędem, przeto wspomniane dzielenie i mnożenie przez 10 należy wykonać na postaci stałoprzecinkowej.
Konwersja z postaci zmiennoprzecinkowej na stałoprzecinkową polega na przesunięciu bitowemu mantysy o wartość zapisaną w wykładniku. Ponieważ wykładnik jest rzędu +-128, więc liczba stałoprzecinkowa musi posiadać 2*128+24 bity, co daje 35 bajtów. W przykładowym kodzie, dla równego rachunku, przeznaczono po 20 bajtów na część całkowitą i ułamkową.
Jak zostało powiedziane, najmłodszą cyfrę można uzyskać poprzez podzielenie 10 — dokładniej cyfrą jest resztą z tego dzielenia. Proces ten powtarzany jest, aż do chwili, gdy dzielona liczba będzie równa zero; pseudokod przedstawia się następująco:
while x > 0: cyfra[i] = x mod 10 x = x div 10 i = i + 1
Jak jednak podzielić duże liczby stałoprzecinkowe? Można potraktować je, jako liczby o podstawie 256, wtedy każdy bajt reprezentuje cyfrę. Jeśli dana jest reszta z dzielenia ze starszej pozycji (ozn. r) oraz i-ta cyfra (ozn. a) to dzielenie przeprowadzamy na liczbie:
r ⋅ 256 + a
Dodatkowo, ponieważ dzielnik jest stały, wyniki dzielenia można prekalkulować — rozmiar dwóch tablic: wynik i reszta z dzielenia --- wyniesie 2560 bajtów (r = 0..9, a = 0..255, zatem liczba możliwych kombinacji jest równa 2559).
W przypadku tak dużych liczb stałoprzecinkowych, ilość iteracji może być dość duża. Ale ponieważ początkowo ilość bitów znaczących jest względnie niewielka więc można wyznaczyć przynajmniej najstarszy znaczący bajt. Gdy wynik dzielenia dla tego bajtu będzie równy 0, to zostanie zawężony przedział obliczeń.
Uzyskanie najstarszej cyfry wymaga przemnożenia przez 10, oczywiście możliwe jest prekalkulowanie wyników, ale obecnie mnożenie nie jest kosztowne. Uwagi podobne jak w punkcie 2.
Format IEEE-754 definiuje następujące wartości specjalne:
wykładnik | mantysa | wartość |
---|---|---|
00000000 | równa 0 | zero |
00000000 | różna od 0 | liczba zdenormalizowana |
11111111 | równa 0 | nieskończoność |
11111111 | różna od 0 | NaN: nie-liczba |
Jeśli liczba jest NaN, to najstarszy — 23 bit mantysy — służy rozróżnieniu pomiędzy QNaN i SNaN. Jeśli jest ustawiony to liczba jest QNaN (Quiet NaN) jej wystąpienie nie powoduje generacji wyjątku; w przeciwnym razie mamy do czynienia z SNaN (Significant NaN) — generuje wyjątek.
Wyjaśnienia wymaga jeszcze liczba zdenormalizowana. Mantysa liczby zdenormalizowanej jest mniejsza niż jeden, wobec czego nie uzupełnia się bitu jedności.
// -- float2string.cc -- #include <stdio.h> #include <string.h> #include <assert.h> typedef unsigned long long int qword; // 64 bity typedef unsigned int dword; // 32 bity typedef unsigned short int word; // 16 bity typedef unsigned char byte; // 8 bitów /* * byte quotient [10*256] = {for (i=0; i<2560; i++) element[i]=i/10}; * byte remainder[10*256] = {for (i=0; i<2560; i++) element[i]=i%10}; */ #include "precalc.c" /* * funkcja dzieli liczbę o podstawie 256 przez 10. * * 'start' i 'end' (zmienna 'end' może zostać zmieniona) * określają zakres tablicy 'number' na jakim przeprowadzane * są obliczenia * * zwracana jest reszta z dzielenia */ byte div_by_10(byte number[], int& start, int& end) { assert(end >= start); int i = end; word rem = 0; // reszta // dzielenie while (i >= start) { word digit = number[i]; word tmp = 256*rem + digit; number[i] = quotient[tmp]; // tmp/10; rem = remainder[tmp]; // tmp%10; i--; } // zawężenie przedziału obliczeń while (number[end]==0 && start <= end) end--; return rem; } /* * funkcja mnoży liczbę o podstawie 256 przez 10. * * 'start' i 'end' (zmienna 'start' może zostać zmieniona) * określają zakres tablicy 'number' na jakim przeprowadzane * są obliczenia * * zwracane jest przeniesienie */ byte mul_by_10(byte number[], int& start, int& end) { assert(end >= start); int i = start; dword carry = 0; // przeniesienie // mnożenie while (i <= end) { word digit = number[i]; word tmp = 10*digit + carry; number[i] = tmp & 0xff; carry = tmp >> 8; i++; } // zawężenie przedziału obliczeń while (number[start]==0 && start <= end) start++; return carry; } char* float2string(char* S, float x) { static byte fixedpoint[40]; // 0..19 - część ułamkowa // 20..39 - część całkowita #define bias 127 #define dot_position (20*8) /* pozycja kropki dziesiętnej (w bitach) */ // 1. wydzielenie pól bitowych dword bin = *(dword*)&x; dword sign = (bin >> 31); // znak dword exp = (bin >> 23) & 0xff; // wykładnik+bias dword mantissa = (bin & 0x007fffff); // mantysa // 2. wartości specjalne // 2a. zero if (exp==0x00 && mantissa==0) { if (sign) strcpy(S, "-0.0"); else strcpy(S, "+0.0"); return S; } // 2b. nieskończoność if (exp==0xff && mantissa==0) { if (sign) strcpy(S, "-inf"); else strcpy(S, "+inf"); return S; } // 2c. NaN -- rozróżnienie między QNaN i SNaN if (exp==0xff && mantissa!=0) { if (mantissa & 0x800000) strcpy(S, "QNaN"); else strcpy(S, "SNaN"); return S; } // 3. uzupełnienie części całkowitej // (jeśli liczba nie jest zdenormalizowana) if (exp==0x00 && mantissa!=0) ; else mantissa |= 0x800000; // 4. wstawienie znaku liczby if (sign) { S[0] = '-'; S[1] = '\0'; } else { //S[0] = '+'; S[1]='\0' S[0] = '\0'; } // 5. zamiana liczby zmiennoprzecinkowej na fixed-point memset(fixedpoint, sizeof(fixedpoint), 0); // zerowanie // exp-23, ponieważ mantysa jest przesunięta o 23 bity w prawo int position = (exp-23) + dot_position - bias; int shift = position & 0x7; // przesunięcie w obrębie bajtu int index = position >> 3; // pierwszy bajt // "wstawienie" mantysy do liczby stałoprzecinkowej // ponieważ shift=0..7, wobec czego liczba bitów znaczących // wynosi zaledwie 32 mantissa <<= shift; fixedpoint[index+0] = (mantissa >> 0x00) & 0xff; fixedpoint[index+1] = (mantissa >> 0x08) & 0xff; fixedpoint[index+2] = (mantissa >> 0x10) & 0xff; fixedpoint[index+3] = (mantissa >> 0x18) & 0xff; // 6. konwersja static char tmp[128]; if (exp >= 0+bias) // liczba posiada część całkowitą { memset(tmp, sizeof(tmp), 0); // zakres w którym zostaną przeprowadzone obliczenia int start = 20; int end = index+3; // int idx = sizeof(tmp)-1; while (1) { tmp[--idx] = div_by_10(fixedpoint, start, end) + '0'; if (start > end) break; } strcat(S, &tmp[idx]); } else strcat(S, "0"); strcat(S, "."); if (exp < 23+bias) // liczba posiada część ułamkową { memset(tmp, sizeof(tmp), 0); // zakres w którym zostaną przeprowadzone obliczenia int start = index; int end = 19; // int idx = 0; while (1) { tmp[idx++] = mul_by_10(fixedpoint, start, end) + '0'; if (start > end) break; } strcat(S, tmp); } else strcat(S, "0"); return S; }
Włączany jest plik precalc.c, który zawiera tablice quotient i remainder (wynik dzielenia i reszta). Oczywiście ich zawartość może być liczona na początku programu, co z oczywistych względów nie zostało pokazane. Są one wyznaczane wg algorytmu podanego w komentarzu; poniżej skrypt w bashu który generuje ten plik.
#!/bin/bash file="precalc.c" echo "byte quotient[2560] = {" > $file for ((i=0 ; i<256 ; i++)) do if [ $i -ne 255 ] then echo $i, $i, $i, $i, $i, $i, $i, $i, $i, $i, >> $file else echo $i, $i, $i, $i, $i, $i, $i, $i, $i, $i >> $file fi done echo "};" >> $file echo "" >> $file echo "byte remainder[2560] = {" >> $file for ((i=0 ; i<256 ; i++)) do if [ $i -ne 255 ] then echo "0, 1, 2, 3, 4, 5, 6, 7, 8, 9," >> $file else echo "0, 1, 2, 3, 4, 5, 6, 7, 8, 9" >> $file fi done echo "};" >> $file