Általános technológiai ökölszabály, hogy amikor egy technológiát kiválasztunk, akkor annak hatékonyan kell illeszkednie a megoldandó feladat gazdasági feltételeihez. Nem érdemes például nagy pontosságú szerszámgépes megmunkálást tervezni egy egyedi építőipari állvány elkészítéséhez a saját építkezésünkön. Így van ez a szoftvertechnológia esetében is, vagyis mindig tudnunk kell, hogy az adott technológiának hol vannak a határai, és azok hogyan illeszkednek a megoldandó feladathoz.
Ha assembler technológia alkalmazásával oldunk meg egy feladatot, mondjuk egy mikrokontroller alkalmazásával (ami egy ipari folyamat-vezérlésre specializált mikroprocesszor), nagyjából csak az eddigiekben ismertetett aritmetikai műveleteket, illetve az azon alapuló számításokat érdemes saját magunknak megírni. Azokban az ipari folyamatvezérlésekben, amikor a fizikai rendszerben valamilyen egyszerű idősoros vezérlést/szabályozást valósítunk meg, a szorzás, osztás, összeadás, kivonás általában elég a feladat megoldásához.
Ha a fentieknél összetettebb számítási igények adódnak, akkor azokat nem érdemes magunknak leprogramozni és mondjuk trigonometriai műveleteket írni, hanem ilyenkor kiegészítjük az assembler technológiát egy olyan készen kapható programcsomaggal, amely mindezt tudja. Persze ennek az az ára, hogy alkalmazkodnunk kell ennek egyedi szabályrendszeréhez. Egy ilyen – az aritmetikai pontosságot és a számítási lehetőségeket kibővítő – technológiai kiegészítés a lebegőpontos programcsomag, melynek elméletével és használatával foglalkozunk a továbbiakban.
Amikor nagyobb pontosságú számítást kell végeznünk, és nem elég, ha az eredmény valamilyen egészszám-reprezentációban keletkezik, vagy olyan nagy, vagy olyan kicsi, hogy az adott számítógép által kezelt 4-8-16-32-64 bites egész számábrázolásba nem fér el, akkor egy egészen másfajta megközelítést használunk, és ez az előbbiekben említett lebegőpontos ábrázolás.
Korábban láttuk, hogy fixpontos ábrázolásnál a fix pont jobbra helyezésével nőtt az ábrázolható szám, de csökkent a pontosság és fordítva, valamint léteztek olyan valós számok, melyeket nem lehetett ábrázolni. A lebegőpontos ábrázolás a számok ún. exponenciális alakjában történő felírásán alapszik, melyet a kis kézi kalkulátorainkon tudományos jelölésnek is neveznek (lásd ).
Hogy megértsük az exponenciális felírás lényegét, vegyünk egy számot, mondjuk az 12345,6789-et. Ez a szám két részből áll, van egy egész része és egy törtrésze, és arra is emlékszünk, hogy egy szám értéke nem változik, ha ugyanazzal a számmal elosztjuk és megszorozzuk. Ha tehát a fenti számot elosztanánk 10000-el és megszoroznánk 104-nel, akkor az értéke nem változna meg, viszont már így nézne ki:
1,23456789∙104.
Ez az alak az ún. exponenciális alak, mely lehetne 0,123456789∙105 vagy akár 123456789∙10-4 is. Ahhoz, hogy ilyen alakban írjunk fel egy számot, az ban szereplő jellemzőket kell megadni.
Az érték előjele (e) | +/- | Pozitív vagy negatív számot jelöl |
Érték/Mantissza (M) | szám | Az ábrázolandó mennyiség számalakja |
A kitevő előjele | +/- | Az adott alapon megadott kivető előjele |
Kitevő/Karakterisztika (k) | szám | Az adott hatvány hatványkitevőjének számalakja |
A hatvány alapja (a) | szám | Az adott hatvány alapja |
A törtrész-szeparátor helye | szám | Törtrész vessző jele |
Mindezeket a jellemzőket a fenti példára alkalmazva, írhatjuk,
hogy egy N = e ∙ M ∙ alakú szám az alábbi jellemzőkkel írható fel
pontosan:
A normalizálás vagy normál alakra hozás azt jelenti, hogy az eredetileg esetleg több egész helyi értéket tartalmazó exponenciális alakú adott számot olyan egységes formai alakra hozzuk, ahol csak egy egész helyi érték szerepel, melyet az adott számrendszerbeli törtrész követ úgy, hogy a hatványtaggal való szorzással az eredeti érték nem változik.
Vegyük egy lebegőpontos szám négybájtos, IEEE 754 szabvány szerinti reprezentációját[3]
Az előjel, mint korábban láttuk, az MSB (vagyis a legmagasabb helyi értékű bit). Az előjel bit miatt az első bájtban már csak hét bit marad a kitevő számára, mely azonos bájtban van, mint az előjel. A kitevő azonban lehet negatív és pozitív is, ezért ún. többletes alapértékes, vagy feszített előjeles alakját tároljuk (szokás még Excess-kódolásnak is nevezni).
Ez valójában csak annyit jelent, hogy pozitív számként tároljuk a kitevőt úgy, hogy egy adott konstanshoz hozzáadjuk. Például, ha ez az eltolás mondjuk 64, akkor a nulla kitevőt mint egy 64-es számot tároljuk. Ha −2 a kitevő, akkor már 62 lesz tárolva, ha pedig +3, akkor 67 a tárolandó kitevő. Ha úgy képzeljük el a feszített előjeles ábrázolást, mint a konstanssal növelt valódi értéket, akkor a kitevő előjelére nem kell külön jelet pazarolni, az egy kivonási algoritmus segítségével előállítható.
Később látni fogjuk, hogy a lebegőpontos műveletek elvégzéséhez szükséges az alap és a kitevő azonossága is, amely így igen könnyen megállapítható és tág határok között számolható.
A következő három bájt a mantissza, melynek ábrázolása, és főként mérete közvetlen hatással van a pontosságra, hiszen itt lesznek az ún. értékes számjegyek. Az belátható, hogy a mantisszában felesleges lenne a vezető nullákat ábrázolni, ezért követelmény, hogy a számokat normálformában, az első nullától különböző értékes jegytől, illetve az első értékes tizedestől tároljuk.
Fontos tárolási jellemző még a hatványalap, mely általában 2-es vagy 16-os számrendszerbeli lehet, az IBM nagygépein például 16-os alapú, míg a személyi számítógépeken általában 2-es alapot szoktak használni. Szokás az összes tárolási bájt szerint 4 bájtos (egyszeres) és 8 bájtos (kétszeres vagy double) pontosságú számábrázolásról beszélni.
Érdekes, még későbbi példáknál előkerülő lehetőség, hogy ha a tárolás 2-es alapra normált, akkor ez első nullától különböző jegy biztosan egy egyes, vagyis felesleges eltárolni, akkor is figyelembe vehetjük, ha nem tároljuk, és ezzel lehetőség nyílik a mantissza vagy a karakteriszika (IEEE 754) értékes jegyeinek, és végső soron a pontosságnak a növelésére. Ez az ún. rejtett egyes eljárás. Vegyük ez első példát (érdemes megfigyelni, hogy ez a meghatározás milyen bonyolult első látásra, pedig kis gyakorlattal gyerekjáték felírni):
Legyen az ábrázolandó decimális szám az 575,554. Állítsuk elő négy bájton, lebegőpontosan, kettes alapra normáltan, 7 bites karakterisztikával, 64 többletes előjeles formában, hexadecimális alakban.
Az első lépés hogy az, hogy a decimálisan adott számot átváltsuk kettes számrendszerbe. A kettes számrendszerbe történő átváltás (mely külön az egész rész és külön a törtrész átváltásával történik) eredményként a 1000111111,100012 bináris minta keletkezik (lásd ).
A következő lépésben egészre normalizálunk úgy, hogy az első értékes jegy mögé helyezzük a kettedesvesszőt, vagyis az exponenciális alak: 1,000111111100012×29.
A feladatkiírás szerint a feszített előjeles ábrázolásban az eltolási konstans 64, így a feszített karakterisztika 64+9=73=10010012 lesz. Mivel az előjelbit (pozitív számról lévén szó) 0, ezért a mantisszáig összeállíthatjuk lebegőpontos számunk első bájtját:
A következő három bájt lesz a mantissza tárolási helye, binárisan 000111111100012 rejtett egyessel, vagyis a vezető egyes elhagyásával. Az előzetesen már összeállított előjel és karakterisztika figyelembevételével a végső bináris alak az egyes „alkotórészek” eltérő színezésével:
Mivel a feladat kiírása úgy szólt, hogy az eredmény hexadecimális alakban kell megadnunk, alakítsuk át ezt az eredményt tetrádonként:
Ha szemléletesen úgy fogjuk fel a lebegőpontos szám mantisszáját, mint egy mindenképpen egész számmal (kettes számrendszer miatt ez 1) kezdődő, de a következő egésznél kisebb, véges kettedesjegyet tartalmazó számot, melynek lehetséges legnagyobb értéke jelent egy határértéket, akkor kiszámíthatjuk azt a tartományt, amelyben egyáltalán felvehetünk számokat, és azt, amelyben nem. Mivel ez egy kettes számrendszerben adott szám, ezért az egyest követő jegyek helyi értéke kettő hatványainak reciproka, valahogy így:
Vagyis van egy számtartományunk, amivel leírhatjuk két valós szám között lévő végtelen darab valós szám egy korlátos halmazát, és ez a korlát alulról és felülről is, sőt a tartományon belül is behatárolja a lehetőségeket. Ha ezt értjük, akkor belátjuk, hogy bizonyos valós számok, bizonyos pontosságok (bizonyos számok) eleve nem érhetők el a lebegőpontos ábrázolásban. Erre majd később készítünk egy mini lebegőpontos saját formátumot, hogy érzékelhető mennyiségeken mutassuk be a fenti elvi korlátokat, most fogadjuk el, hogy az elkerülhetetlen pontatlanságot az okozza, hogy egy pl. n bites mantisszával csak 2n darab valós szám ábrázolható pontosan, miközben végtelen számú valós szám van az adott tartományon belül.
Ha a feladatkiírás úgy szólt volna, hogy a karakterisztikát 8 biten kell megvalósítani, 127-es eltolással, akkor a feszített előjeles ábrázolás kilépett volna az első bájtból, emiatt az előjelet követő bitminta egy bittel eltolódott volna jobbra, valamint a 127-es eltolás miatt 127+9=136= 100010002 bináris alakja kerül az előjelbit mögé.
A megoldás bitmintája az alábbi lenne:
Vegyünk egy másik példát: alakítsuk át ötkettedes pontossággal, kettes alakra normáltan, 8 bites 127-el eltolt karakterisztikával az alábbi decimális számot és adjuk meg hexadecimális formában:
2012,052610
A normalizált kettes számrendszerbeli alak ötkettedes pontossággal a alapján:
1,111 1011 1000 00012×210
Az exponens 127-el vett eltolása 137-et ad, ami binárisan
, vagyis az összeállított rejtett egyessel
létrehozott ábrázolás:
Hexadecimális alakban kiírva:
44FB810016
A lebegőpontos számábrázolást szabványosították és megszületett az IEEE (Institute of Electrical and Electronics Engineers) 754/1985 (ejtsd: áj tripple í) szabvány, mely a következőkben rögzíti a lebegőpontos számok jellemzőit a következő jelölések szerint:
| signum vagy előjel |
| exponens vagy karakterisztika |
| mantissza vagy szignifikáns |
| offset vagy eltolás |
| fraction vagy törtrész |
A tartalmazza a lebegőpontos számok pontosság szerinti csoportosítását.
Széles-ség | Mantissza | s | e | m | o | Ábrázolható tartomány | |
Egyszeres pontosságú | 32 bit | kettes alapra normált (1.F) | 1 bit | 8 bit | 23 bit | 127 | 8,43ˇ10-37< |N| <3,37ˇ1038 |
Dupla pontosságú | 64 bit | kettes alapra normált (1.F) | 1 bit | 11 bit | 52 bit | 1023 | 4,19ˇ10-307< |N| <1,67ˇ10308 |
Kiterjesztett pontosságú | 80 bit | kettes alapra normált (1.F) | 1 bit | 15 bit | 64 bit | 16383 | |
Négyszeres pontosságú | 128 bit | kettes alapra normált (1.F) | 1 bit | 15 bit | 112 bit | 16383 |
Általában a lebegőpontos számokkal kapcsolatban a fentiek tekinthetők a minimumnak egy programozó számára, de mert a jelentősége ennél jóval nagyobb, ássunk egy kicsit mélyebbre a lebegőpontos ábrázolás témakörében, és vizsgáljuk meg gyakorlati oldalról a használatát, hogy hiteles és szemléletes képünk legyen alkalmazásáról és alkalmazásának korlátairól.
Az első vizsgálati irányunk legyen a használhatósági tartományok kérdése, hiszen ez közvetlenül érinti az eredmény pontosságát (lásd ).
Pozitív túlcsordulás akkor következik be, amikor olyan nagy szám ábrázolására kerül sor, ahol az adott véges bit hosszúságú kitevő nem elég nagy.
Hogy megértsük a túlcsordulás lényegét, vegyünk egy elméleti jóval kisebb ábrázolási tartományt, mert a valódi szabványos ábrázolási tartományok talán felfoghatatlanul nagyok első ismerkedésre. Tehát képezzünk egy lebegőpontos számot a következő ábrázolási korlátokkal:
Széles-ség | Mantissza | s | e | m | o | |
Saját tesztelési pontosságú | 8 bit | kettes alapra normált (1.F) | 1 bit | 3 bit | 4 bit | 3 |
A legnagyobb pozitív szám, amit ábrázolni tudunk, az az eset, amikor a következő bitmintát vesszük fel:
Határozzuk meg ezt a számot. Az eltolt karakterisztika értéke éppen 7, ez az eltolás figyelembevételével azt jelenti, hogy a tényleges karakterisztikánk 4. Ha most a négybites mantisszát (a rejtett egyessel) visszaírjuk normál alakba, akkor az a következőképpen néz ki:
1,11112 × 24 = 111112 = 3110
Vagyis ez a lebegőpontos pontosság nem képes olyan számot tárolni, ami nagyobb, mint 31.
Ellenőrizzük le, mi is történik, ha a tesztelési virtuális lebegőpontos programcsomagunk (vagyis most mi magunk) kap egy ennél nagyobb számot tárolás és műveletvégzés számára. Legyen ez a szám csak egy kicsivel nagyobb, mondjuk 32. Első lépésben elvégzi a kettes számrendszerre a konverziót, és azt kapja, hogy a bitminta még normalizálatlanul 001000002. Ha elvégzi a normál alakra történő átalakítást, akkor a 1,000002×25 adódik. Ha most az exponenssel elvégzi az eltolást, akkor a karakterisztikára három biten 3+5=8 → 0112+1012=10002 keletkezik, amiből a legalsó három digitet fogja a tárolási bitminta összeállításához felhasználni (hiszen csak ennyi hely áll rendelkezésére). A mantissza értékét is úgy kell felhasználnia, hogy az értékes jegyek közül az első négy fér el a tárolási helyen, vagyis a rejtett egyes alkalmazásával a lebegőpontosra átalakított szám így néz ki:
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Ha most ezt a számot valamilyen művelet számára elő kell állítani, akkor már nem fog emlékezni a tároláskor elveszített jegyekre, és „gátlástalanul” előállítja (hacsak már a tároláskor nem jelezte a hibát) a következő számot: mivel a karakterisztika 0, levonva belőle az eltolás (3) értékét -3 adódik, a rejtett egyessel a normalizált mantissza pedig
1,0002∙2-3=0,0012=0∙20+0∙2-1+0∙2-2+1∙2-3=1/23=0,12510, ami köszönő
viszonyban sincs az eredeti 3210-el.
A túlcsordulás következménye tehát jegy- és értékvesztés a teszt lebegőpontos formátumunknál! (A valóságban a karakterisztika és a szignifikáns azonos nulla esetét külön kezelik [egzakt nullának tekintik], erről még szólunk később, most csak a szemléltetés érdekében tekintettünk el ettől.)
Egy programozónak mindig tudnia kell, sőt kötelessége pontosan megbecsülni és mérnöki szempontból „túlméretezni” a program algoritmusai által végzett aritmetika műveletek korlátait. Ha pedig megjósolhatatlan egy-egy mérnöki számítás eredménye (ilyen is van), akkor magában az algoritmusban kell gondoskodni arról, hogy az ún. „exception framework” (kivételkezelő keretrendszer, később sokat beszélünk még róla) megdobja az erre az esetre megtervezett figyelmeztetést.
Ez ebben az ábrázolásban azt jelenti, hogy valahányszor egy eredményt átadnánk a lebegőpontos csomagnak, elvégezzük a komparálást (összehasonlítást), amivel megállapítjuk az esetleges túlcsordulást, és ha bekövetkezett, akkor sajnálattal közöljük a UI-on (User interface, felhasználói felület, szintén lesz még szó róla bőven), hogy a számítások túlcsordulás miatt nem folytathatók.
Nézzük az alulcsordulás esetét ugyanerre a lebegőpontos tárolásra. Most már gyorsabban haladva látjuk, hogy az eltolt exponens minimuma nyilván 0, amelyből az eltolás miatt a -3 karakterisztika adódik. Ha tehát a legkisebb ábrázolható számot keressük, akkor az így nézhetne ki:
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Az előbb kiszámoltuk, hogy ez a szám éppen:
1,0002∙2-3=0,0012=0∙20+0∙2-1+0∙2-2+1∙2-3=1/23=0,12510
Mivel még mindig a pozitív tartományon belül vagyunk, úgy néz ki, hogy a 0 és 0,125 között, valamint a 31 fölött lesz egy-egy tartomány, ahova a legnagyobb jóindulat mellett sem fogunk tudni aritmetikai eredményeket tárolni. A korábbiak figyelembevételével tehát a pozitív oldalon a 0,125 és 31 közötti szakaszt használhatjuk majd tárolásra, és csak ebben a tartományban vagyunk képesek eredmények kiszámítására a teszt lebegőpontos csomagunkkal. Természetesen ez a probléma felvetődik a negatív számok esetében is ugyanezekkel a határokkal a negatív tartományon.
Utalva a korábban az ábrázolható számtartomány felső, alsó és belső korlátaira, ne gondoljuk, hogy ebben a tartományban minden valós számot ábrázolni tudunk.
Tekintsük például a 2,210 számot, és próbáljuk meg ábrázolni a teszt lebegőpontos formátumunkban:
2,210=10,00110011001100112=1, 000110011001100112∙21
A karakterisztikánk 1 lesz, ami eltolva 4-et (=1002) ad. Ábrázolni a mantisszában csak a zölddel jelzett biteket tudjuk, a lebegőpontos számunk tehát így néz ki:
0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
Számoljuk most vissza a kapott számot:
1,00012∙21=10,0012=2,12510
Hát ez nem éppen az, amiből eredetileg kiindultunk!
Egy, a valóságból származó példa: szoftver kerekítési hiba volt az oka az ARIANE 5 hordozórakéta az indítás utáni 37 mp-ben történt felrobbanásának (1996. június 4-én), melynek részletes analízise az interneten megtalálható. A lényeg, hogy egy 64 bites szám 16 bites konverziója okozta az irányító rendszer és a másodlagos rendszer leállását és az önmegsemmisítő rendszer beindulását.
Az okozott kárral 7 billió dollár és 10 év fejlesztés ment veszendőbe.
(Ez a pontatlanság az oka annak, hogy a BCD számok jelentősége a mára elhanyagolható hátrányai miatt megnőtt, hiszen a pénzügyi, gazdasági, tudományos számítások induló adatainak már felvétel idején torzulhat a pontossága, ha lebegőpontos formátumot használunk, míg a BCD kódolással jobban tartható a kiinduló pontosság.)
Azt is érezzük, hogy minél inkább növeljük az értékes jegyek (a szignifikáns) számát, annál inkább vagyunk képesek a virtuális „1”-ről eljutni a „2”-re (megközelíteni azt) a valós tartományon, és minél inkább növeljük a karakterisztika bitjeinek számát, annál inkább növelhetjük a túlcsordulás, és annál inkább csökkenthetjük az alulcsordulás határát.
Egy másik könnyen belátható következménye a lebegőpontos ábrázolásnak, hogy a valós számok tartományának nullához közeli tartományában sokkal több racionális szám ábrázolható pontosan, mint amikor a szám abszolút értéke távolabb van a számegyenesen a nullától. Ezt úgy szokták mondani, hogy a nullához közeli tartományban több az egységnyi szakaszra eső reprezentánsok száma, mint távolabb, vagyis minél nagyobb a szám, annál kisebb az elvárható pontosság.
Gyakori probléma továbbá, hogy egy nagyon nagy szám és egy nagyon kicsi szám különbségének (kivonásának) műveletében eltűnik a kis szám hatása (úgy mondjuk, hogy a nagyobb szám elnyeli a sokkal kisebbet).
Amikor a program írása során egy lebegőpontos változót összehasonlítunk egy egésszel, gyakran kapunk megbízhatatlan eredményt, mert a pontos összehasonlítás nem teljesül mondjuk az egész 1 és a lebegőpontos 1.00000000000000000004651 között, ahol a jelölt pontatlanság könnyen előfordulhat.
A lebegőpontos ábrázolás különleges esetei azok, amikor az egyes alkotó bitcsoportok speciális tartalommal és kombinációban jelennek meg (lásd ).
Szám értelmezése | Előjel | Karakterisztika | Mantissza |
Normalizált érték | Signum | Tetszőleges exp | Tetszőleges bitminta |
Denormalizált érték | Signum | 0/Újra van rejtett egyes | Nem zérus bitminta |
Egzakt nulla | Signum | 0 | 0 |
Negatív végtelen | 1 | Csupa egyes 255/2047/16384 | 0 |
Pozitív végtelen | 0 | Csupa egyes 255/2047/16384 | 0 |
Not a Number (NaN) | Mindegy | Csupa egyes 255/2047/16384 | Nem zérus |
A NaN esetet a számításkor fellépő hibák (pl. nullával való osztás, negatív gyökvonás stb.) jelzésére használják, amit a műveletvégző vagy a hívó hibakezelése kezel le. A denormalizált érték akkor használt, ha az adott szám alulcsordult.
[3] IEEE (Institute of Electrical and Electronics Engineers) 754/1985 (ejtsd: áj tripple í ) szabvány