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.
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.
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:
2 | 0.111111 |
3 | 0.0370372 |
4 | 0.0123464 |
5 | 0.00411815 |
6 | 0.00138344 |
7 | 0.000504043 |
8 | 0.000339597 |
9 | 0.000799529 |
10 | 0.00301183 |
11 | 0.0119852 |
12 | 0.0479202 |
13 | 0.191674 |
14 | 0.766693 |
15 | 3.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.
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
Sitten kirjoitetaan polynomi auki ja piirretään taas kuvio.
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ä.