Kokonaislukujen ja liukulukujen esitysmuodot
Kokonaisluvut
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!!)
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
Luku 57 = 0011 1001
vakiolisäys +127 = 0111 1111
esitysmuoto 184 = 1011 1000 = 0xB8
Luku -57
vakiolisäys +127
esitysmuoto 70 = 0100 0110 = 0x46
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ä.
-96.7510 = -1.1000 0011 * 26 → 1 100 0 010 1 100 0001 1000 0000 0000 0000 = 0xC2C18000
+345.87510 = +1.0101 1010 111 * 28 → 0 100 0 011 1 010 1101 0111 0000 0000 0000 = 0x43AD7000
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.
Muistathan tarkistaa pistetilanteesi materiaalin oikeassa alareunassa olevasta pallosta!