Lyhyt johdatus äärettömyyteen
eli miksi Inf ja NaN ovat hyödyllisiä tietokonelaskennassa

Tämä juttu pyrkii vastaamaan kysymykseen, miksi liukulukulaskenta tietokoneissa on määritelty niin, että siinä voi esiintyä suureita Inf (ääretön) ja NaN (ei mikään luku, epäluku). Lyhyt vastaus on, että nämä suureet voivat "kuljettaa tietoa" laskennassa tapahtuneista poikkeustilanteista, niin että ohjelmassa myöhemmin voidaan selvittää, mihin kaikkiin ohjelman muuttujiin ne tilanteet ovat voineet vaikuttaa.

Lasse Lehtimäki on esityksessään Java-kielestä kuvannut liukulukulaskennan erikoistilanteita seuraavasti, käyttäen NaNille näppärää suomennosta epäluku:

Java noudattaa liukulukulaskennassa IEEE-standardia 754. Liukulukuaritmetiikka ei esimerkiksi aiheuta mitään poikkeuksia. Ylivuodon merkki on positiivinen tai negatiivinen ääretön; esimerkiksi laskutoimituksen 1.0/0.0 tulos on positiivinen ääretön (+Inf). Jos operaatiolla ei ole mielekästä tulosta, kuten neliöjuuri luvusta -1, tulos on epäluku (NaN, Not a Number). Standardi sisältää sääntöjä myös tällaisia erikoislukuja käsitteleville matemaattisille operaatioille. Esimerkiksi NaN * x on 0, jos x on 0, tai Inf, jos x on Inf. Muuten tulos on NaN. Standardissa 754 on paljon muitakin kiinnostavia sääntöjä, mutta niiden merkitys Java-ohjelmoinnissa on vähäisempi.

Lasse Lehtimäki: Yleiskatsaus Java-kieleen (osa Lappeenrannan TKK:n kurssilla Internet-ohjelmointi esitettyä aineistoa); pari kirjoitusvirhettä korjattu tässä; sisällön kannalta olennainen korjaus on -1 pro 1; jäljempänä kuvattavaa asiavirhettä ei tässä ole korjattu.

Mutta mihin epälukuja ja äärettömyyksiä sitten voisi käyttää? Jos suoraan tulostamme muuttujan arvon, niin tulostusrutiini (esim. print-lause) saattaisi esittää äärettömyyden vaikkapa muodossa Infinity tai Inf, periaatteessa myös vaikka erikoissymbolilla . Tästä nähtäisiin sitten, että jotain erikoista on tapahtunut. Mutta sama voitaisiin havaita - itse asiassa jo silloin, kun erikoistilanne sattuu - siten, että ohjelman suoritus katkeaa virheilmoitukseen. Ja useissa kielissä niin käykin, ellei ohjelmoija erikseen sitä estä eli ohjaa kielen toteutusta (ns. ajonaikaista järjestelmää, joko kääntäjän optioilla tai erityisillä aliohjelmakutsuilla) tämän asemesta jatkamaan ohjelman suoritusta. Ja jos ohjelman suoritus jatkuu, täytyy poikkeustilanteen aiheuttaneen laskutoimituksen kuten nollalla jaon tuottaa jokin tulos jatkossa käytettäväksi.

Miksi sitten haluttaisiin jatkaa ohjelman suoritusta esimerkiksi nollalla jaon jälkeen? Pienissä ohjelmissa, esim. harjoitustlöissä, siihen ei useinkaan ole mitään syytä, vaan virheet pitää "saada kiinni" heti. Mutta isossa ohjelmassa saattaa poikkeustilanne syntyä jossakin kokonaisuuden kannalta melko vähämerkityksisessä yksityiskohdassa. Se saattaa vaikuttaa joihinkin lopputuloksiin, mutta emme ehkä halua pitkään jatkuneen laskennan kokonaan katkeavan. Ja jos ohjelma esimerkiksi ohjaa ydinvoimalan toimintaa tai lentokoneen lentoa, ei oikein ole varaa vain antaa ohjelman tulostaa Floating exception ja päättyä.

Tarkastellaan jakolaskuja 1000.0/1000.0, 1000.0/0.1, 1000.0/0.01, ..., joissa siis jakaja pienenee kymmenenteen osaan edellisestä kussakin vaiheessa. Tulokset ovat siis 1, 10, 100, jne. Jossakin vaiheessa jakaja tulee niin pieneksi, että se ei enää ole esitettävissä tietokoneessa paremmin kuin pyöreänä nollana. Mikä on silloin jakolaskun tulos? Nollallahan ei voi jakaa. Mutta sekä matematiikassa että tietokonealalla sanotaan usein, että voihan sillä jakaa, mutta tulos on ääretön. Mitä mieltä tässä on?

Käytännössä luvuilla laskeminen tietokoneessa käyttää yleensä jotakin äärellistä lukualuetta. Tämä johtuu sekä siitä, että tällainen on paljon helpompi toteuttaa kuin mielivaltainen lukualue, että siitä, että fysikaalisesti tai muutoin mielekkäät luvut eivät yleensä vaihtele rajattomasti. Yleensä ns. yksinkertaisen tarkkuuden liukulukujen (single-precision floating point numbers) lukualue tietokoneissa on yleensä sellainen, että suurin esitettävissä oleva luku on luokkaa 10^38 ja pienin positiivinen luku luokkaa 10^(-38). Tässä ^ tarkoittaa potenssiin korotusta, ja 10^38 (eli 1038) on siis luku, jossa on ykkösen perässä 38 nollaa, ja 10^(-38) on sen käänteisluku, eli erittäin lähellä nollaa. Lukualue saattaa olla laajempikin, mutta tarkat lukuarvot eivät ole tässä tärkeitä vaan alueen äärellisyys. Jos laskennan tulos on lukualueen ulkopuolella, niin syynä on usein jonkinlainen virhetilanne, mutta on myös mahdollista, että laskennan välitulos ei mahdu alueeseen; harvemmin todellinen fysikaalinen suure on lukualueen ulkopuolella, joskin sekin toki mahdollista.

Tietojenkäsittelyssä äärettömän käsite mahdollistaa ensinnäkin virhetilanteita koskevan tiedon tallentamisen. Aina ei suinkaan ole mielekästä, että nollalla jakaminen aiheuttaa ohjelman suorituksen päättymisen. Jos muuttujalle X sijoitetaan arvoksi lausekkeen A/B arvo ja B:n arvon onkin puhdas nolla, voimme sijoittaa X:n arvoksi äärettömän vain merkiksi siitä, että tapahtui nollalla jako. Ohjelman suorituksen lopussa voidaan ehkä tutkia, minkä muuttujien arvot ovat äärettömiä ja siten havaita, missä kaikkialla meni pieleen.

Ääretön on tässä toistaiseksi vain ikäänkuin symboli, jonka merkitys on 'tulos liian suuri esitettäväksi'. Sellainen tulos voidaan saada myös esimerkiksi silloin, kun kertolaskussa kerrottavat ovat hyvin suuria niin, että niiden tulo ei mahdu lukualueeseen. Toki kyseessä voi olla myös nollalla jako, mutta tavallisesti sovelluksen kannalta kyse on silloinkin siitä, että tulos on pikemminkin liian suuri kuin matemaattisesti määrittelemätön, siis jakaja on liian lähellä nollaa.

Tässä on kyse erilaisesta äärettömyydestä kuin matematiikassa. Matematiikassa äärettömällä voidaan tarkoittaa erilaisia asioita. Esimerkiksi raja-arvojen yhteydessä esiintyvä ilmaisu "kun x menee äärettömyyteen", symbolein merkittynä x→∞, ei tarkoita, että x tulisi äärettömäksi; asiallisempaa olisikin ehkä käyttää ilmaisua "kun x kasvaa rajatta". Ns. aktuaalisen äärettömän käsite ja erilaisilla äärettömyyksillä operointi on sitten eri asia, ja varsin hämmentävä; ks. Osmo Pekosen kirjoitusta Yksi, kaksi, kolme... ääretön!.

Tietokonelaskennassa tulos on "ääretön" silloinkin, kun se vain ei mahdu lukualueeseen, vaikka on matemaattisesti äärellinen. Äärettömyyksillä voidaan jossain määrin operoida, "laskea", kuten luvuilla, mutta kaikki tutut laskulait eivät suinkaan päde. Jos merkitsemme laskennallista ääretöntä symbolilla Inf (sanasta , 'äärettömyys'), niin voimme esimerkiksi sanoa, että Inf + Inf = Inf. Jos kahden laskutoimituksen tulokset (jotka olisivat matemaattisesti äärellisiä lukuja) eivät mahdu lukualueeseen, niin ei niiden summakaan mahtuisi. Vastaavasti tietenkin 42 + Inf = Inf. (Tämä voi tuntua aika erikoiselta. Tavallisestihan pätee, että yhtälöstä, jonka molemmin puolin esiintyy sama termi, se voidaan pyyhkiä pois, mutta jos tätä sovellettaisiin edellä olevaan, saataisiin 42 = 0. Tuttuja laskulakeja ei siis todellakaan pidä soveltaa näihin!) Voimme myös sanoa, että 1/0 = Inf ja 1/Inf = 0, kun yhtäsuuruusmerkin tulkitaan tarkoittavan 'antaa tietokonelaskennassa tuloksen ...'.

Jos esimerkiksi lasketaan jonkin sarjan summaa, esimerkiksi 1 + 1/4 + 1/9 + 1/16 + ... + 1/(n²) + ..., niin voi hyvin käydä niin, että jakaja "menee äärettömäksi" eli lukualueen ulkopuolelle. Esimerkkitapauksessa termi 1/(n²) on matemaattisesti niin pieni, että sillä ei käytännössä olisi juurikaan vaikutusta lopputulokseen. Tässä mielessä 1/Inf = 0 on mielekäs sääntö. (Käytännössä tällaisessa tapauksessa laskennalla on yleensä jokin lopetusehto, joka lopettaa termien laskemisen, kun ne menevät tarpeeksi pieniksi. Mutta on mahdollista, että summassa, johon otetaan ennalta annettu määrä termejä, jotkin termit "menevät nolliksi" tässä mielessä.)

Entä Inf - Inf? Kun muistetaan, että Inf edustaa matemaattisesti jotakin sellaista tulosta, joka ei mahtunut lukualueeseen, emme voi sanoa, mitä Inf - Inf on. Vain sattumalta tulos olisi matemaattisesti nolla. Ei ole mitään takeita siitä, että tulos edes olisi lukualueessa, ja jos on, emme tiedä, mikä se olisi. Muita vastaavia tilanteita ovat 0 × Inf ja 0/0 ja matemaattisen funktion laskeminen silloin, kun argumentti on määrittelyalueen ulkopuolella. (Voitaisiin tietysti mielivaltaisesti määritellä, että 0/0 on 1, ja joissakin yhteyksissä niin tehdäänkin. Mutta matemaattisesti ja laskennallisesti tämä ei yleensä ole järkevää.) Tätä ilmaistaan tietokonelaskennassa symbolilla NaN, "not a number", 'ei mikään luku' (ainakaan mikään nimenomainen luku).

Miksi 1/0 on Inf mutta 0/0 on NaN? Jossakin mielessä tämä on määritelmäkysymys: normaalissa aritmetiikassa kumpikaan ei ole määritelty. Voitaisiin sanoa, että eihän 1/0:kaan ole oikeasti luku. Mutta sen määritteleminen Inf:ksi eikä NaNiksi on hyödyllistä, koska näin voidaan erottaa toisistaan kaksi olennaisesti erilaista tilannetta. Matemaattinen perustelu on, että lausekkeen 1/x arvo kasvaa rajatta, kun x lähenee rajatta nollaa (matemaattisin merkinnöin: limx→0 = ∞), kun taas lausekkeella x/x ei ole mitään raja-arvoa x:n lähestyessä nollaa. Vähän toisenlaisen kannan määritelmän järkevyydelle esittää Anton Dilin artikkeli Division by Zero - Undefined, or Infinity?

Vaikka NaN tavallaan merkitsee, että jotain on mennyt pieleen, se on varsin hyödyllinen. Tietokonelaskennassa voidaan sanoa, että NaN yleensä "saastuttaa" kaiken, mihin se koskee. Jos a:n arvo on NaN ja b:n arvo on mitä tahansa, niin a + b:n arvo on NaN. Tämä on täysin mielekästä, kun muistamme, mistä NaN syntyy. Voidaan sanoa, että NaN leviää eli propagoituu laskennassa. Tämän ansiosta voimme tietää laskennan päättyessä, minkä muuttujien arvoihin laskennan virhetilanteet ovat vaikuttaneet.

Olisi luonnollista ajatella, että joissakin tapauksissa NaNin propagoituminen katkeaa, erityisesti että 0 kertaa NaN olisi  0. Näinhän oli tehty alussa siteeratussa Java-kielen liukulukulaskennan esittelyssä. Tuntuisi kätevältä ajatella, että NaN tavallaan edustaa tuntematonta lukua. Koska nollalla kerrottaessa on samantekevää, mikä toinen tekijä on, niin eikö se tavallaan pyyhi pois sitä epävarmuutta, jota NaN ilmaisee? Standardi IEEE 754 (ks. esim. Jamil Khatibin Introduction to Floating point calculations and IEEE 754 standard ja PSC:n The IEEE standard for floating point arithmetic) kuitenkin määrittelee toisin: NaN × 0 on NaN. Taustalla on se, että vaikka NaN ehkä useimmiten juuri heijastelee edellä kuvattua epävarmuutta, se voi heijastaa myös sitä, että jokin arvo ei lainkaan ole luku. Ohjelmointikielen toteutus voisi olla esimerkiksi sellainen, että alustamattomien (initialisoimattomien) muuttujien alkuarvo on NaN. Tämän ansiosta saataisiin usein kiinni loogisia virheitä ohjelmasta. Mutta jos NaN × 0 olisi 0, voisi moni virhe jäädä havaitsematta. (Liukulukulaskennan erikoistilanteista Javan kannalta kertoo muuten Thomas Wangin Java floating-point number intricacies.)

Jos muuttujan arvo on NaN, niin arvo on riippunut jostakin laskutoimituksesta, joka ei ole ollut matemaattisesti mielekäs tai jonka tulos on ollut lukualueen ulkopuolella, jolloin sitä ei ole voinut jatkossa käyttää. Tämän ansiosta voimme virheiden satuttua päätellä, mitkä muuttujat voivat sisältää mielekästä tietoa ja mitkä eivät.

Useissa tietokoneissa liukulukulaskennan poikkeustilanteet saattavat asettaa ns. lippuja (flags) eli prosessorin sisäisiä tiloja, jotka osoittavat, onko jokin poikkeustilanne sattunut vai ei. Niitä tutkimalla ei kuitenkaan voida saada selville, missä tilanteessa poikkeustilanne on syntynyt, saati sitä, mihin kaikkeen se on voinut vaikuttaa.

Entä miten äärettömyys ja NaN voidaan esittää tietokoneissa? Käytännössä esitysmuoto perustuu siihen, että liukulukujen esitysmuoto on sellainen, että kaikki mahdolliset bittikombinaatiot eivät ole käytössä. Tämän ansiosta on voitu varata jotkin bittiyhdistelmät erikoistarkoituksiin eli esittämään näitä "erikoisarvoja".

Laajahko esitys liukulukulaskennasta tietokoneissa on David Goldbergin What Every Computer Scientist Should Know About Floating-Point Arithmetic.


Tämä juttu sai alkunsa siitä, että Jori Mäntysalo kysyi ryhmässä sfnet.atk.ohjelmointi.alkeet (viestissä otsikolla Liukuluvut ja 1/0.0 == Inf), "osaako joku selittää yksinkertaisesti miksi liukuluvuilla laskettaessa 1.0/0.0 on Inf ja miksi on kätevää pystyä tarvittaessa laskemaan äärettömyyksillä? Vastaukseni siihen sisälsi jonkinlaisen yritelmän, ja kun se tuntui kehityskelpoiselta, laajensin sen täksi dokumentiksi.

Viimeisimmän päivityksen ajankohta: 2004-10-03.

Jukka Korpela