C, PHP, VB, .NET

Дневникът на Филип Петров


* Числа с плаваща запетая

Публикувано на 21 юли 2015 в раздел Информатика.

От математиката в училище знаете за реалните числа – обединението на множествата на рационалните и ирационалните числа. Би трябвало да знаете, че рационалните числа се представят като крайна или безкрайна десетична дроб, а ирационалните са винаги безкрайни непериодични дроби. Важното в случая е, че в определени случаи става дума за „безкрайна“ дроб. Това е един от основните проблеми в информационните технологии – ние нямаме безкрайно количество памет, в което да запишем двоичното представяне на една безкрайна десетична дроб. Поради ограничената си памет компютрите нямат възможност да записват реалните числа с абсолютна точност. Ще се досетите, че имаме същия проблем дори при целите числа – ние не можем да записваме безкрайно големи цели числа, а винаги се съобразяваме с определен брой отпуснати за целта байтове.

Най-популярният стандарт за представяне на приближения на реални числа при компютрите е стандартът IEEE 754. При него едно число „float“ се определя от 32 бита и всяка една комбинация от тези битове определя точно едно реално число. Това означава, че в едно float число можем да запишем не повече от 232 реални числа. Това може би ще ви се стори прекалено малко, при положение, че само между числата 0,001 и 0,002 знаем, че има безкрайно много други числа… Даже не само това – между всеки две различни реални числа има безкрайно много други. Значи пред нас стои въпросът за оптималното използване на тези отпуснати битове така, че да можем да покрием максимално ефективно числата, които се използват в практиката.

Преди да продължим трябва да се запознаем с термина „стандартна научна форма“ (scientific notation). Много често в различни статии (а и при сметки с много големи или много малки числа на най-обикновените калкулатори) ще видите числа от типа 9е-3 или 10e+5 и т.н. Без да даваме строга дефиниция, ще обясним как се пресмятат чрез примери – числото в ляво от „e“ се умножава по 10 на степен числото вдясно от „e“. Тоест ±Ae±B = ±A*10±B. Например:

  • 9е-3 = 9*10-3
  • 10e+5 = 10*105
  • -4е+8 = -4*108
  • -12е-3 = -12*10-3

Понеже работим с компютри, за нас ще е по-удобно да използваме експонента с основа 2 вместо 10. Тоест когато говорим за „scientific notation при компютрите“, то записът 2e+3 ще е 2*23, а не 2*103. Освен това числата A и B ще е удачно да се превърнат от десетични в бинарни. И тук стандартът IEEE754, по който работят повечето съвременни езици за програмиране, ни предлага оптимизации, чрез които да разходим минимално количество памет. За да запишем едно число в този вид ни трябват три компонента – знаков бит s (sign – определя се с един бит), експонента e (exponent – определя се от 8 бита) и мантиса M (manitssa – определя се от оставащите 23 бита). Формулата за определяне на число N по този стандарт е:

Nf = (-1)s * (1+M) * 2(e – 127)

Нека ги разгледаме поотделно:

  • s се записва като най-старши бит (31-ви, защото 32 бита се броят с индекси от 0 до 31 включително). Припомняме, че това е най-левият бит при записа на числото в двоичен вид;
  • е се записва между битове 30 и 23 включително. Както знаем от статията за целите числа, 8 бита (които са без специално определен знаков бит) могат да представят десетично число от 0 до 255. Виждате, че от това число във формулата вадим 127 (това се нарича „bias 127“). Тоест ние можем да представяме степени от -127 до 128. Така например ако ни трябва степен 10, ние трябва в числото „е“ да запишем 127+10 = 137 или в двоичен вид 10001001. Ако ни трябва числото -35, ще трябва да запишем 127-35 = 92 или в двоичен вид 01011100;
  • M се записва в оставащите битове между 22 и 0 включително. Мантисата е нормализирана в интервала 0 до 1 (без 1). Може да се докаже, че тази нормализация е постижима винаги чрез увеличаване или намаляване на експонентата – ще покажем метод за това с практически пример без доказателство. Освен това понеже тя винаги има десетична част 0, не пазим десетичната ѝ част (освен в един частен случай, за който ще пишем в края на статията).

Тук е важно да припомним как точно се записват дробните числа от 0 до 1 в бинарен вид (или как въобще се записват дробни числа в бинарен вид). Това вече беше разгледано в статията за преобразуване на двоични в десетични числа. Нека обаче се пренесем от света на научната информатика в практическото приложение в информационните технологии като вземем примера от посочената статия – там видяхме, че не всички десетични дробни числа могат да се представят в с краен брой двоични цифри, например видяхме, че 12,310 = 1100,0(1001)2. Записаното число в скобите беше период, т.е. то се повтаря до безкрайност. А ние вече знаем, че нямаме безкрайно много памет. Тоест на нас ни се налага да „отрежем и закръглим“ двоичното представяне на числото до определена цифра (в зависимост от това колко битове са ни дадени). В такъв случай казваме, че сме загубили точност.

А защо казваме, че числата са с „плаваща“ запетая? Това идва от фактът, че нямаме строго фиксиране на броя цифри преди и след десетичната запетая – в този смисъл тя се мести и при едни числа е по-наляво, при други по-надясно.

Нека разгледаме един практически пример. Искаме да представим десетичното число 0,085 като число с плаваща запетая. Искаме да го представим в научна форма с експонента с основа 2 и мантисата трябва да е между 0 и 1, т.е. искаме да представим числото точно както е по формулата в IEEE754. За улеснение нека направим преобразуване на формулата по следния начин:

[math]0,085 = (1+M) * 2^{e – 127}[/math]

[math]=> \frac{0,085}{2^{e – 127}} = 1+M[/math]

Търсим каква е степента на двойката в знаменателя, при която 1+M да е в интервала от 1 до 2. Лесно може да намерите, че това се получава при степен -4 или в крайна сметка при e = 123. При такава степен ще получим M = 0,36. Тоест получихме следното представяне:

  • s очевидно е 0, защото знакът на мантисата е положителен;
  • e е 12310 = 011110112;
  • M е 0.3610. Започваме преобразуванията, за да го намерим в двоичен вид. Цялата част е 0. За дробната част имаме:
    0,36 * 2 = 0,72 (0)
    0,72 * 2 = 0,44 (1)
    0,44 * 2 = 0,88 (0)
    0,88 * 2 = 0,76 (1)
    0,76 * 2 = 0,52 (1)
    0,52 * 2 = 0,04 (1)
    0,04 * 2 = 0,08 (0)
    0,08 * 2 = 0,16 (0)
    0,16 * 2 = 0,32 (0)
    0,32 * 2 = 0,64 (0)
    0,64 * 2 = 0,28 (1)
    0,28 * 2 = 0,56 (0)
    0,56 * 2 = 0,12 (1)
    0,12 * 2 = 0,24 (0)
    0,24 * 2 = 0,48 (0)
    0,48 * 2 = 0,96 (0)
    0,96 * 2 = 0,92 (1)
    0,92 * 2 = 0,84 (1)
    0,84 * 2 = 0,68 (1)
    0,68 * 2 = 0,36 (1)
    0,36 * 2 = 0,72 (0)
    Тоест получихме числото 0.(010111000010100011110)2. Премахваме „0.“ – мантисата винаги започва с нула и както казахме по-горе това не се пази. Да, но продължаваме да имаме безкрайна периодична дроб, а ние разполагаме със само 23 бита. Значи закръгляваме точно 23 бита от тази поредица (закръгляването винаги е „нагоре“, т.е. ако 24-тата цифра е 1, то прибавяме 1 към 23-тата) или в крайна сметка получихме мантисата M = 010111000010100011110112

Или в крайна сметка получихме, че числото 0,08510 се записва във формат число с плаваща запетая (тип данни float) като 00111101101011100001010001111011f. Имайте предвид, че след като сме „отрязали и закръглили“ мантисата, обратното преобразуване ще даде приближение на числото, а не точното десетично число (т.е. загубили сме точност). Нека проверим:

  • s се представя точно като 0 в десетичен вид (няма загуба);
  • e се представя точно като 123 в десетичен вид (няма загуба);
  • M (припомняме, че за нея трябва да добавим „0,“ в началото) e 0.36000001430511474609375 в десетичен вид.

Така по формулата имаме:

[math]N = (-1)^0*1.36000001430511474609375*2^{123-127}[/math]

Значи получихме, че в крайна сметка, че при записване на числото 0,085 във формат на плаваща запетая, ние изгубихме точност – при обратното му превръщане то става числото 0.085000000894069671630859375, което е близко, но все пак различно число (освен това компилаторите на езиците за програмиране ще го отрежат допълнително и ще го извеждат с прецизност до 17-тия знак). В следваща статия ще разгледаме какви проблеми възникват в приложното програмиране в следствие на тези грешки породени от закръгления.

В информационните технологии се срещат още един вид числа с плаваща запетая, които всъщност са по-често използвани ог програмистите спрямо досега разглежданите – това са числата с плаваща запетая с двойна точност (double). С тях се борави по същият начин, по който го правехме с float, но с тази разлика, че те заемат не 32, а 64 бита – експонентата заема 11 бита, а мантисата 52. Понеже експонентата вече „поема“ по-големи числа, това налага е промяна във формулата – вадим от експонентата (bias) 1023 вместо 127:

Nd = (-1)s * (1 +M) * 2(e – 1023)

Границите на числата от тип double са от ±4.94065645841246544e-324 до ±1.79769313486231570e+30. Няма да даваме конкретни примери, защото принципът е същия, а просто числата в двоичен вид са с много повече (двойно повече) цифри.

Представянето на числата с плаваща запетая имат една съществена разлика спрямо представянето на целите числа – числата с плаваща запетая не са разпределени равномерно по числовата ос. За да се изразим по-образно – те са по-сгъстени около нулата и са по-разредени в края. Тоест числата с плаваща запетая ще имат по-добра точност при по-малките числа и по-лоша точност при големите.

Освен всичко казано дотук, трябва да отбележим и четири специални числа с плаваща запетая. Техните представяния са резервирани и не се пресмятат по формулата:

  • Нула: изцяло нулеви битове на знак, експонента и мантиса;
  • Безкрайност: ако експонентата е само нули, а мантисата е само единици, се счита, че имаме положителна или отрицателна безкрайност (каква е зависи от знаковия бит);
  • Не числа: Когато експонентата е само единици, а мантисата е само нули, се счита, че това е невалидно число (not a number – NaN). Ако знакът му е 1, това е „тих“ NaN (QNaN – quiet not a number). Когато знакът е 0, това е „сигнализиращ“ NaN (SNaN – signaling not a number). QNaN се използват от програмните езици когато използваме невалидни математически операции (когато имаме недефиниран резултат – например 0.0/0.0 е такова недефинирано число). SNaN се използват, когато възникне програмна грешка (signal, exception) в софтуерния продукт и са за служебно ползване.

Има и още един специален случай – когато експонентата е само нули, а мантисата е ненулева казваме, че имаме денормализирано число. За тези числа знаем, че нямат водеща 1 в представянето на мантисата и затова при тях формулата се изменя на:

Nfd = (-1)s * M * 2(e – 126) за float или Ndd = (-1)s * M * 2(e – 1022) за double.

Заключение: Също както при целите числа, компютрърните програми записват числата в двоичен вид в съответен формат. Това, което трябва да знаем е, че често имаме загуба на точност. Затова например при числа с плаваща запетая например не е конектно да използваме сравнение с оператор равенство.

 



Добави коментар

Адресът на електронната поща няма да се публикува


*