Jorin jutut / ohjelmointi

Tietokone laskee väärin

Tietokone laskee joissakin tilanteissa epätarkasti tai suorastaan virheellisesti. Tämän tekstin tarkoitus on esittää tästä muutama esimerkki. Tarkoitus ei ole kertoa miksi näin tapahtuu, eikä selittää miten tietokoneohjelma pitäisi tehdä laskuvirheiden minimoimiseksi. Jos lukija osaa tämän luettuaan edes epäillä tekemiensä ohjelmien toimivuutta, on tekstin tavoite saavutettu.

Tekstissä on kaksi C++ -kielistä ohjelmaa, mutta toivoakseni kyseisen kielen osaaminen ei ole välttämätöntä esimerkkien ymmärtämiselle. Esimerkkiohjelmat löytyvät myös muilla kielillä tästä. Ohjelmat toimivat tietokoneissa, jotka noudattavat IEEE:n liukulukustandardia, eli mm. tavanomaisissa PC-yhteensopivissa kotikoneissa.

1/2 + 1/3 = 5/6

Ala-asteella opetettujen laskusääntöjen mukaan otsikon laskutoimitus pitää paikkansa. Tietokoneen mielipide saadaan seuraavalla pienellä ohjelmalla.


#include <iostream.h>

int main()
{
    cout << "1/2=" << 1.0/2.0 << endl;
    cout << "1/3=" << 1.0/3.0 << endl;
    cout << "1/2 + 1/3=" << 1.0/2.0+1.0/3.0 << endl;
    cout << "5/6=" << 5.0/6.0 << endl;
    if (1.0/2.0 + 1.0/3.0 == 5.0/6.0)
        cout << "Yhtäsuuret luvut";
    else
        cout << "Erisuuret luvut";
}

Yllättäen kone väittää lukuja erisuuriksi. Kahden desimaaliluvun yhtäsuuruutta ei koskaan kannata verrata, sillä teoriassa yhtäsuuret luvut voivat käytännössä erota toisistaan. Lisäksi nähdään, että lukujen sisäinen esitystarkkuus (jolla vertailut suoritetaan) voi olla suurempi kuin esitystarkkuus tulostettaessa.

Virheiden moninkertaistuminen lukusarjassa.

Määritellään lukusarjan X ensimmäisiksi luvuiksi yksi ja yksi kolmasosa, siis X0=1 ja X1=(1/3). Määritellään sarjan loppu laskukaavalla: Xn+1=(13/3)*Xn-(4/3)*Xn-1. Siis esimerkiksi X2 = (13/3)*X1-(4/3)*X0 = (13/3)*(1/3)-(4/3)*1 = 13/9-4/3 = 1/9. Laskemalla muutama luku käsin selviää, että sarjan jokainen luku on aina kolmasosa edellisestä. Tietokoneohjelmaksi tässä tapauksessa tulee


#include <iostream.h>

int main()
{
    float ekaLuku=1.0;
    float tokaLuku=1.0/3.0;
    for (int i=2; i<=15; i++)
    {
        float tilap=(13.0/3.0)*tokaLuku - (4.0/3.0)*ekaLuku;
        ekaLuku=tokaLuku;
        tokaLuku=tilap;
        cout << i << "\t" << tokaLuku << endl;
    }
}

Ohjelman tulostus oli seuraavanlainen:

20.111111
30.0370372
40.0123464
50.00411815
60.00138344
70.000504043
80.000339597
90.000799529
100.00301183
110.0119852
120.0479202
130.191674
140.766693
153.06677

Oikeat arvot voi laskea helposti vaikka taskulaskimella, kun tietää että X[n] saadaan laskemalla 1/(3^n). Sarjan 6. luvun pitäisi olla n. 0.00046, eli yksikään desimaali ei enää ole oikea. Lukujen tarkkuus pienentyy jatkuvasti, ja pian tulos muuttuu aivan mielettömäksi.

Ohjelman kuluessa virheet kertyvät ja moninkertaistuvat. Jokainen yksittäinen laskutoimitus tekee pienen virheen. Ensimmäisen kierroksen virhe kertautuu (13/4)-kertaiseksi toisessa kierroksessa. Kolmannessa kierroksessa näkyy ensimmäisen laskukierroksen virhe jo (13/4)*(13/4) -kertaisena, eli noin kymmenkertaisena.

Tarinan opetus: joissakin tilanteissa virheiden kertyminen saa aikaan mielettömän lopputuloksen.

(x-1)8 = 0

Potenssilauseke (x-1)8 voidaan kirjoittaa auki 8. asteen polynomiksi
x8 - 8*x7 + 28*x6 - 56*x5 + 70*x4 - 56*x3 + 28*x2 - 8*x + 1

Teoriassa molemmat lausekkeet antavat täsmälleen saman tuloksen. Miten sitten tietokoneella laskettaessa käy pisteen x=1 ympäristössä? Ensin kokeillaan antaa polynomi potenssimuodossa ja piirretään kuvio
kuvaaja (x-1)^8

Sitten kirjoitetaan polynomi auki ja piirretään taas kuvio.
kuvaaja (x-1)^8 auki kirjoitettuna

Pyöristysvirheet aiheuttavat satunnaisen tuntuisia virheitä laskun tulokseen. Tarinan opetus: älä edes yritä liian hyvää tarkkuutta. Esimerkin minimiarvosta voidaan sanoa vain, että se on jossain välillä 0.98 .. 1.02. Samalla kuvioista näkee, että sama laskutoimitus eri tavoilla ilmaistuna tuottaa kuitenkin eri vastaukset laskuvirheistä johtuen.


Lukusarjaesimerkki on kirjasta Numerical Analysis, jonka ovat kirjoittaneet David Kincaid ja Ward Cheney. Polynomiesimerkin Riitta Niemistö esitti numeerisen matematiikan kurssilla. Viimeisen esimerkin kuvat tehtiin Octavella ja niitä muokattiin Gimpillä.