Luku 3

Kokonaislukujen ja liukulukujen esitysmuodot

Käymme tässä osiossa läpi kokonaislukujen neljä esitysmuotoa ja IEEE-standardin mukaisen 32-bittisten liukulukujen esitysmuodon.

Kokonaisluvut

Positiiviset kokonaisluvut ovat helppoja, koska niiden esitysmuoto on useimmiten (mutta ei aina!) niiden binääriarvo. Tiedon pituus pitää kuitenkin niissäkin ottaa aina huomioon, koska yleisesti ottaen vasemmanpuolimmainen bitti pitää varata etumerkille. Täten esimerkiksi 8-bittisen tavun suurin kokonaisluku on yleensä 0111 1111 eli 127 eli 27-1 eli 128-1.

Kokonaislukujen etumerkkiin perustuva esitysmuoto

Meille ihmisille luontevin tapa esittää kokonaisluvut on käyttää etumerkkiä, jolloin vasemmanpuolimmainen (eniten merkitsevä) bitti on positiivisille luvuille 0 ja negatiivisille 1. Esimerkiksi, +57 ja -57 ovat tavuina 0x39 ja 0x95, sekä 32-bittisinä sanoina 0x00000039 ja 0x80000039.

+57 = 0011 1001 = 0x39 (tavuna)   +57 = 0x 00 00 00 39 (32-bittisenä sanana)
-57 = 1011 1001 = 0xB9 (tavuna)   -57 = 0x 80 00 00 39 (32-bittisenä sanana)

Etumerkkibitin käyttö on vähän huono kokonaislukujen aritmetiikan toteutukseen, joten sitä ei useinkaan käytetä sen vuoksi. Esitystavan parhaimpana puolena on sen soveltuvuus ihmisille, jotka ovat tottuneet etumerkkien käyttöön.

Kokonaislukujen yhden komplementin esitysmuoto

Pelkkää etumerkkiä paremmin laitteiston ALU-piireille sopiva esitystapa on yhden komplementti. Siinä positiivisilla luvuilla on edelleen tavallinen binääriesitys, mutta negatiiviset luvut saadaan komplementoimalla positiivisen luvun esitysmuodon kaikki bitit. Esimerkin luvut +57 ja -57 ovat nyt tavuina 0x39 ja 0xC6, sekä sanoina 0x00000039 ja 0xFFFFFFC6.

+57 = 0011 1001 = 0x39 (tavuna)   +57 = 0x 00 00 00 39 (32-bittisenä sanana)
-57 = 1100 0110 = 0xC6 (tavuna)   -57 = 0x FF FF FF C6 (32-bittisenä sanana)

Vasemmanpuolimmainen (eniten merkitsevä) bitti toimii edelleen etumerkkibittinä, mutta negatiivisten lukujen lukuarvo ei ole niin helposti luettavissa. Yhden komplementin esitystavalla on se hyvä ominaisuus, että positiivisia ja negatiivisia lukuja on yhtä monta. Esimerkiksi yhden tavun arvoalue on siis [-127, +127]. Huonona puolena on, että nollalla on kaksi esitystapaa, esimerkiksi tavuina +0 = 0x00 ja -0 = 0xFF. Tästä on haittaa aritmetiikkaoperaatioissa ja vertailuoperaatioissa, kun pitää varautua kahteen nollaan. Nollaan vertailu on harmillisesti ohjelmissa huomattavan yleinen operaatio.

Kokonaislukujen kahden komplementin esitysmuoto

Yleensä kokonaisluvuille käytetään yhden komplementin esitysmuodon sijasta kahden komplementin esitysmuotoa. Positiiviset luvut ovat edelleen tavallisessa binääriesityksessä. Negatiivisen luvun esitysmuoto saadaan nyt vastaavan positiivisen luvun esitysmuodosta komplementoimalla kaikki bitit ja lisäämällä esitysmuotoon 1. Huomaa, että binäärijärjestelmässä yhteenlasku tehdään ihan samalla tavalla kuin 10-järjestelmässäkin. Jos yhteenlaskua tehtäessä tulee muistinumero vasemmanpuolimmaisen bitin kohdalla, niin tässä tapauksessa se unohdetaan eli jätetään pois.

               +57 = 0011 1001 = 0x39
komplementoi         1100 0110
lisää 1                     +1
               -57 = 1100 0111 = 0xC7

Vasemmanpuolimmainen bitti toimii edelleenkin etumerkkinä. Negatiivisen luvun suuruus (vastaava positiivinen arvo, itseisarvo) saadaan negatiivisen luvun esitysmuodosta ehkä vähän yllättäen samalla tavalla, komplementoimalla kaikki bitit ja lisäämällä 1.

               -57 = 1100 0111
komplementoi         0011 1000
lisää 1                     +1
suuruus              0011 1001 = 0x39 = +57

Kahden komplementin esitysmuodolla on se hyvä ominaisuus, että nollia on vain yksi (tavuna 0x00).

              +0 = 0000 0000 = 0x00
komplementoi       1111 1111
lisää 1                   +1        (unohda yhteenlaskun viim. muistinumero)
suuruus       -0 = 0000 0000 = 0x00 (+1:llä ja -1:llä on sama esitysmuoto)

Huonona puolena on, että negatiivisia lukuja on nyt yksi enemmän kuin positiivisia lukuja. Tavun arvoalue on [-128,- +127]. Tämä otetaan huomioon aritmetiikkapiireissä automaattisesti.

               -128 = 1000 0000 = 0x80
komplementoi          0111 1111
lisää 1                      +1
suuruus               1000 0000 = 128 (Sama esitysmuoto kuin luvulla -128!!)
Tästä seuraa, että esimerkiksi aritmetiikkaoperaatio negaatio (esim. lauseessa Y = -X;) päättyy virhetilanteeseen, jos operandi on pienin mahdollinen negatiivinen luku (tavuna -27 = -128 ja 32-bittisenä sanana -231 =  -2 147 483 648). Kahden komplementin parhaimpana puolena on, että sille tehdyt aritmetiikkaoperaatiot ovat muita esitystapoja helpompia toteuttaa ALU:n piireillä. Tämän vuoksi se on yleisin käytössä oleva kokonaislukujen esitystapa.

On myös huomionarvoista, että normaali 32-bittisten lukujen lukualue [-2 147 483 648, +2 147 483 647] ei ole ihan valtavan iso. Jos sovelluksessa tarvitaan todella suuria kokonaislukuja, niin täytyy käyttää kaksinkertaisen tarkkuuden 64-bittisiä kokonaislukuja.

Kaikissa kokonaislukujen esitysmuodoissa täytyy ottaa huomioon myös tavujen järjestys. Esimerkiksi, luvut +57 ja -57 ovat 32-bittisessä Big-Endian esitysmuodossa

+57 = 0x 00 00 00 39    (binääriluku 11 1001)
-57 = 0x FF FF FF C7 

ja Little-Endian esitysmuodossa

+57 = 0x 39 00 00 00  
-57 = 0x C7 FF FF FF

Kokonaislukujen vakiolisäykseen perustuva esitysmuoto

Joissakin tapauksissa (esim. seuraavaksi esitettävien liukulukujen yhteydessä) kokonaisluvut esitetään positiivisina binäärilukuina. Tämä tarkoittaa, että kaikki binääriluvut tulkitaan ensin etumerkittöminä kokonaislukuina ja sitten esitysmuodosta vähennetään sovittu vakio sen lukuarvon saamiseksi. Yleensä tuo vakio on n-bittiselle tiedolle 2n-1-1.
Tavussa on 8 bittiä, joten vakio on yleensä 127 (27-1 = 011111112). Tällöin arvoalue [-127,+128] on suunnilleen yhtä suuri positiivisille ja negatiivisille luvuille. Suurin positiivinen luku on 128, koska suurin 8-bittinen etumerkitön kokonaisluku on 0xFF =&nbsp255 ja 255-127=128. Jos vakio olisi vaikkapa 50, niin arvoalue olisi sitten [-50,+205]. Joskus tuostakin voisi olla hyötyä!
Luku            57 = 0011 1001
vakiolisäys   +127 = 0111 1111
esitysmuoto    184 = 1011 1000 = 0xB8

Luku           -57
vakiolisäys   +127
esitysmuoto     70 = 0100 0110 = 0x46
Kun vakiolisäyksenä käytetään 2n-1-1 (missä n on bittien lukumäärä), niin esitystavalla on kaksikin etua. Ensinnäkin, positiivisilla ja negatiivisilla luvuilla on suunnilleen yhtä iso arvoalue. Toiseksi, vasemmanpuolimmainen bitti toimii myös etumerkkinä. Toisin kuin aikaisemmissa esitysmuodoissa, etumerkkibitin arvo 0 indikoi nyt negatiivista lukua ja arvo 1 positiivista lukua.

Liukuluvut

Kuten jo ensimmäisessä luvussa mainittiin, tietokoneissa ei ole käytettävissä reaalilukuja. Sen sijaan käytämme tietokonearitmetiikkaa varten kehitettyä reaalilukujen approksimaatiota, liukulukuja. Liukuluvuille on tyypillistä vakiomuoto ja etukäteen määritelty lukutarkkuus. Esimerkiksi, reaaliluku Π (pii) esitetään likiarvona 3.1415927 sen yleisimmässä 32-bittisessä esitysmuodossa.

Esitysmuodossa on kolme kenttää: etumerkki, lukuarvo (mantissa) ja suuruusluokka. Desimaaliluvuilla esitysmuoto toimisi seuraavanlaisesti. Mantissa skaalataan sillä tavoin, että kokonaisosassa on vain yksi numero ja desimaaliosaan otetaan vaikkapa 6 numeroa.

+1.23         =    + 1.230000  *  100
+123.0        =    + 1.230000  *  102
-0.00123      =    - 1.230000  *  10-3
-0.000000123  =    - 1.230000  *  10-7
+123.456789   =    + 1.234568  *  102

Tietokoneessa käytämme tietenkin binäärijärjestelmää, jolloin desimaaliosan asemesta binääripisteen jälkeen luvuissa on binääriosa. Nytkin mantissa skaalataan siten, että kokonaisosaan jää vain yksi numero. Normaalissa 32-bitin esitysmuodossa mantissassa on 24 bittiä, mutta tässä esimerkissä mantissassa on vain 9 bittiä yhteensä, joista 8 on binääriosassa. Esimerkin esitystarkkuus on siten 9 bittiä, mikä ei vastaa edes 3 desimaalinumeron tarkkuutta.

+1.510   =   +1.1        =  +  1.10000000    * 20
+2.510   =  +10.1        =  +  1.01000000    * 21
-96.7510 =  -1100000.11  =  -  1.10000011    * 26
+96.87510 = +1100000.111 =  +  1.10000100    * 26 (pyöristetty)

Tietokoneiden alkuaikoina jokaisella valmistajalla oli oma tapansa esittää liukulukuja, mutta tästä aiheutui liikaa harmia. On jo tarpeeksi ärsyttävää laskea koko ajan likiarvoilla, saati sitten niin että sama ohjelma antaa erilaisia (likiarvo) tuloksia eri koneilla. Jo vuodesta 1985 käytössä on ollut IEEE 754 standardi liukulukujen esitysmuodolle ja aritmetiikalle. Standardin uusin päivitys on vuodelta 2008. Nyt lähes kaikki suorittimet noudattavat tuota standardia ja laskevat likiarvonsa samalla tavalla. Esittelemme tässä nyt standardin pääperiaatteet 32-bittisille liukuluvuille.

IEEE:n 32-bittisessä standardissa liukulukujen esitysmuoto on seuraavanlainen. Vasemmanpuolimmainen bitti on etumerkki, ja se on 0 positiivisille luvuille ja 1 negatiivisille luvuille. Seuraavat 8 bittiä ovat eksponentti, ja sen esitysmuoto on vakiolisäys 127. Loput 23 bittiä ovat mantissalle, joka yleisessä tapauksessa esitetään normalisoidussa muodossa. Normalisoinnissa mantissa skaalataan ensin siten, että kokonaisosassa on vain yksi numero. Koska kyseessä on binäärijärjestelmä, tuo numero on aina 1, joten sitä ei tarvitse tallettaa! Sitä kutsutaan piilobitiksi. Normalisoidusta luvusta talletetaan siis vain mantissan binääriosa. Tällä nerokkaalla tempulla saamme 23 bittiin talletettua 24 bittiä informaatiota, eli lukutarkkuus kaksinkertaistuu.

Eksponentin esitysmuoto on siis aina positiivinen kokonaisluku. Miksi tämä esitysmuoto, eikä joku noista muista? Perusteluna on, että liukulukuaritmetiikkaa toteutettaessa eksponentteja käsitellään vain niiden esitysmuotoina välittämättä eksponenttien todellisista arvoista. Normalisointi ja muut liukulukuaritmetiikkaan liittyvät operaatiot on helpompi toteuttaa, kun käsiteltävänä on vain positiivisia lukuja eksponettikentissä.

Esimerkiksi, luku -96.75 on binäärinä -1100000.11 ja normalisoidussa muodossa -1.10000011 * 26. Etumerkki ('-') talletetaan bittinä 1, eksponentti 6 muodossa 6+127=133=10000101, ja mantissasta vain sen binääriosa .10000011 23 bitillä 0x41800 (1000 0011 -> 100 0001 1000 0000 0000 0000 = 0x41800).
-96.7510   = -1.1000 0011    * 26
    → 1 100 0 010 1 100 0001 1000 0000 0000 0000 = 0xC2C18000
Toinen esimerkki. Luku +346.875 on binäärinä +101011010.111 ja normalisoituna +1.010 1101 0111 * 28. Etumerkki talletetaan bittinä 0, eksponentti muodossa 8+127=135=10000111, ja mantissa 23-bittisessä muodossa 0x2D7000 (= 010 1101 0111 0000 0000 0000).
+345.87510 = +1.0101 1010 111 * 28
    → 0 100 0 011 1 010 1101 0111 0000 0000 0000 = 0x43AD7000
Vastaavasti, jos muistissa olevan liukuluvun X esitysmuoto on 0x40780000 (= 0 100 0000 0 111 1000 0000 0000 0000), niin mikä on X:n arvo? Etumerkkibitti on 0, joten luku on positiivinen. Eksponentin esitysmuoto on 1000 0000=128, joten eksponentin arvo on 128-127=1. Mantissan binääriosan esitysmuoto on 0x780000=111 1000 0000 0000 0000 0000, joten piilobitin kanssa mantissa on 1.1111. Muuttujan X arvo on nyt siis 1.1111*21 = 11.111 = 3.87510.
0x40780000 = 0 100 0000 0 111 1000 0000 0000 0000
    → +1.1111 * 21 = +11.1112 =  3.87510

Liukulukujen erikoistapaukset

Liukuluvuille on erikseen määritelty muutama esitysmuodon erityistapaus. Ensinnäkin siellä on ±0, joissa kaikki eksponentin ja mantissan bitit ovat nollia. Siellä on myös määritelty ±∞, joissa exponenttikenttä on 0xFF ja mantissakenttä on nolla. Lisäksi on vielä "alustamaton liukuluku" (NaN, Not a Number), jota on vielä kahta muotoa. Niissä molemmissa eksponenttikenttä on 0xFF ja mantissakenttä nollasta poikkeava. Toisessa muodossa alustamattoman luvun käyttö aiheuttaa keskeytyksen ja toisessa ei.

±0 =           0/1 0000 0000 00...0
±∞ =           0/1 1111 1111 00...0
Quiet Nan:     0/1 1111 1111  1?..?1?..?  (nollasta poikkeava mantissakenttä)
Signaling Nan: 0/1 1111 1111  0?..?1?..?  (nollasta poikkeava mantissakenttä)

Normalisointi asettaa omat rajoituksensa sille, kuinka pieniä liukulukuja voi esittää, koska normalisoidun liukuluvun kokonaisosa on aina 1. Hyvin pienille ei-normalisoiduille luvuille on oma esitystapansa, joka on koodattu eksponenttikenttänä 0 ja nollasta poikkeavana mantissakenttänä. Tuollaisilla luvuilla eksponentti on aina -126 ja piilobitin arvo on 0. Nimensä mukaisesti mantissaa ei ole normeerattu tavalliseen tapaan, vaan se on skaalattu eksponenttiin -126 sopivaksi.

+1.14794 \* 10-40 = 0.0000 0010 1 \* 2-126
    → 0 000 0000 0 000 0001 0100 0000 0000 0000 = 0x00014000

Huonona puolena tällaisissa (itseisarvoltaan) hyvin pienissä luvuissa on esitystarkkuuden heikentyminen. Edellisessä esimerkissä merkitseviä bittejä on vain 17, kun normaalisti liukulukujen tarkkuus on 24 bittiä. Jokainen nollabitti ei-normalisoidun mantissan alussa puolittaa lukutarkkuuden.

Liukulukulaskenta

Liukulukulaskenta on hieman erilaista kuin mitä koulussa on opittu reaalilukulaskennasta. Esimerkkinä tarkastellaan tilannetta, jossa muuttujan X arvo on 1.0 ja muuttujan Y arvo on 0.00000001. Jos laskemme nämä luvut yhteen (Z=X+Y), niin reaaliluvuilla laskettaessa summan pitäisi olla 1.00000001. Liukuluvuilla (IEEE:n 32-bittinen standardi) laskettaessa tulos on kuitenkin 1.0, koska Y:n bitit jäävät pois normeeratussa 24 bitin esitysmuodossa.

1.00000010 + 0.0000000110 = 1.0000000110
1.000 0000 0000 0000 0000 0000 0001 01012
    → 1.000 0000 0000 0000 0000 00002 (24 bittiä)

Toinen ongelma liukulukulaskennassa on lukujen vertailu. Reaaliluvuilla on ihan normaalia verrata kahta lukua toisiinsa, mutta liukuluvuilla suora vertailu ei useinkaan toimi lähes samanarvoisten lukujen kanssa, koska liukulukujen esitystarkkuus tulee ottaa huomioon. Täten esimerkiksi lause "if (X+Y == 3.0) then ..." ei useinkaan toimi oikein. Liukulukujen vertailussa yhtäsuuruuteen täytyy riittää, että ne ovat "riittävän lähellä" toisiaan. Sama epätarkkuus pitää ottaa huomioon vertailtaessa, onko jokin liukuluku suurempi tai pienempi kuin toinen.

Esimerkki: Liukulukulaskennan epätarkkuus

X = 20.3;  -- tallentuu lukuna 20.299999
Y = 1.33;  -- tallentuu lukuna  1.33000004
Z = X+Y;   -- tulos on luku 21.629999
if (Z == 21.63)     -- koodi toimii väärin, tarkoitus oli haarautua
then ....

Z = X+Y-21.63;
if ( |Z| < 0.00001)   -- koodi toimii oikein
then ....             -- epsilon 0.00001 pitää valita viisaasti!

--

if (Y < Z) -- koodi ei välttämättä toimi oikein
           -- esim. Y = 3.00000011 ja Z = 3.00000008
then  ....

if ( Y < 0.99999 * Z)  -- koodi toimii varmemmin oikein
then ....

Pitkäkestoisessa (tunteja, päiviä, viikkoja?) liukulukulaskennassa ongelmana voi olla, että käytössä olevien liukulukujen esitystarkkuus pikkuhiljaa heikkenee. Tämä pätee erityisesti vähennyslaskuun, jos molemmat operandit ovat suunnilleen samankokoisia. Lukujen vasemmanpuoleiset merkitsevät bitit kumoavat toisensa ja tuloksen normalisoinnin yhteydessä oikealta täytetään nolla-biteillä ilman mitään parempaa tietoa. Kerran menetettyä todellista esitystarkkuutta ei koskaan voi saada takaisin. Joissakin järjestelmissä tällaista tiedon rappeutumista vastaan taistellaan alustamalla (boottamalla) järjestelmä aika ajoin, jolloin liikkeelle lähdetään taas "puhtaalta pöydältä" ja mahdollisimman tarkan datan pohjalta.

Pääsit aliluvun loppuun! Jatka tästä seuraavaan osaan:

Muistathan tarkistaa pistetilanteesi materiaalin oikeassa alareunassa olevasta pallosta!