C++
C#

Проблеми тачности записа реалних бројева

Већина реалних бројева има бесконачно много децимала (познати пример је број \pi). Такве бројеве не можемо да запишемо потпуно тачно ни на папиру ни у рачунару (нити било где друго) јер би за то било потребно бесконачно много простора и времена. Због тога, када рачунамо са реалним бројевима, ми у пракси скоро увек рачунамо са приближним вредностима мање или веће тачности.

Као што знамо, савремени рачунари су у стању да веома брзо изводе рачунске операције са реалним бројевима (оним који се могу записати у рачунару). Да би те операције биле тако брзе, реални бројеви се у рачунару памте на одређени стандардан начин који је условљен архитектуром рачунара и не зависи од програмског језика који користимо. Тај стандардан начин памћења реалних бројева подразумева да се користи простор одређене величине и у њему се памти неки коначан број значајних цифара броја. Значајне цифре броја су све његове цифре почевши од прве цифре која није нула. На пример, за број \(0,0003400\) записан у декадном систему, значајне цифре су \(3400\) (у овом примеру чињеница да се прва значана цифра појављује на четвртој децимали била би засебно памћена). Нуле на крају броја су у овом случају значајне јер показују тачност приближне вредности коју користимо.

Приликом рачунања са приближним вредностима реалних бројева може доћи до губитка тачности. На пример, ако рачунамо користећи 3 значајне цифре, дељењем \(1\) са \(3\) добијамо резулатат \(0,333\) као приближну вредност једне трећине. Множењем овог резултата са \(3\) добијамо \(0,999\) уместо \(1\). Видимо да се појавила грешка величине \(0,001\). Овај проблем се не може отклонити тако што рачунамо са већим бројем значајних цифара. Ако бисмо на пример рачунали са 100 значајних цифара, при истим операцијама бисмо добили грешку на стотој децимали. Према томе, грешке се појављују самим тим што нисмо у стању да операције са реалним бројевима изводимо потпуно тачно (било да користимо рачунар или не).

За памћење реалних бројева у рачунару се користи бинарни систем. Због тога чак и неки бројеви који у декадном систему имају врло кратак тачан запис, у рачунару не могу да буду тачно записани (јер тачан запис таквог броја у бинарном систему може да има бесконачно много цифара). На пример, иако бисмо очекивали да следећа наредба испише da, она ће исписати ne (проверите).

cout << (0.1 + 0.2 == 0.3 ? "da" : "ne");

Исписивањем вредности \(0.1 + 0.2 - 0.3\) са 30 децимала добијамо
\(0.000000000000000055511151231258\). Овакав резултат се добија зато што се реални бројеви какве смо до сада користили у програмима (типа double), у рачунарима памте са 52 значајне цифре у бинарном систему, што је приближно 16 значајних цифара у декадном систему. При датом једноставном рачунању дошло је до грешке на позицији последње значајне цифре и тако добијамо ненулте цифре после 16 децимала једнаких нули.

Овим објашњењем смо само назначили проблем са којим се суочавамо и овде се нећемо бавити његовим детаљнијим проучавањем. Ипак, већ из наведеног се могу извући неки савети за практичан рад.

Претпоставимо да су подаци са којима рачунамо по вредости између 0 и 100 и да су дати са 5 децимала. То значи да имамо до 7 значајних цифара у тим подацима (2 значајне цифре пре децималне тачке и 5 после ње). Уколико после неког рачунања добијемо изразе a и b, који се разликују на шестој значајној цифри, сматраћемо да је та разлика стварна и да дати изрази и треба да буду различити. Међутим, ако је разлика тек на четрнаестој значајној цифри, треба закључити да су изрази a и b у ствари једнаки, а да је до разлике дошло услед ограничене тачности записа реалних бројева у рачунару. Одавде следи да изразе a и b не треба поредити простим испитивањем услова a == b, него испитивањем да ли се вредности a и b разликују довољно мало (занемарљиво мало), дакле испитивањем услова облика

abs(a - b) < eps

где је eps (скраћено од грчког слова \(\varepsilon\) тј. епсилон) мала позитивна константа, коју треба одредити на основу очекиваних величина и тачности података. Напоменимо да се вредност 0.1 може писати и као 1е-1, вредност 0.01 као 1е-2, вредност 0.001 као 1е-3 итд. У поменутом примеру за eps можемо да изаберемо на пример 0.000000001 или краће 1е-9 (уместо 9 би могао да стоји и неки други број већи од 7 а мањи од 16).

Слично овоме, ако реалан број x (уз исте претпоставке о величини и тачности података и вредности eps) заокружујемо на мањи или једнак цео број, уместо floor(x) боље је писати floor(x + eps).

Исто важи и за заокруживање броја x на већи или једнак цео број, где би уместо x требало писати x - eps.

Проблеми тачности записа реалних бројева

Већина реалних бројева има бесконачно много децимала (познати пример је број \pi). Такве бројеве не можемо да запишемо потпуно тачно ни на папиру ни у рачунару (нити било где друго) јер би за то било потребно бесконачно много простора и времена. Због тога, када рачунамо са реалним бројевима, ми у пракси скоро увек рачунамо са приближним вредностима мање или веће тачности.

Као што знамо, савремени рачунари су у стању да веома брзо изводе рачунске операције са реалним бројевима (оним који се могу записати у рачунару). Да би те операције биле тако брзе, реални бројеви се у рачунару памте на одређени стандардан начин који је условљен архитектуром рачунара и не зависи од програмског језика који користимо. Тај стандардан начин памћења реалних бројева подразумева да се користи простор одређене величине и у њему се памти неки коначан број значајних цифара броја. Значајне цифре броја су све његове цифре почевши од прве цифре која није нула. На пример, за број \(0,0003400\) записан у декадном систему, значајне цифре су \(3400\) (у овом примеру чињеница да се прва значана цифра појављује на четвртој децимали била би засебно памћена). Нуле на крају броја су у овом случају значајне јер показују тачност приближне вредности коју користимо.

Приликом рачунања са приближним вредностима реалних бројева може доћи до губитка тачности. На пример, ако рачунамо користећи 3 значајне цифре, дељењем \(1\) са \(3\) добијамо резулатат \(0,333\) као приближну вредност једне трећине. Множењем овог резултата са \(3\) добијамо \(0,999\) уместо \(1\). Видимо да се појавила грешка величине \(0,001\). Овај проблем се не може отклонити тако што рачунамо са већим бројем значајних цифара. Ако бисмо на пример рачунали са 100 значајних цифара, при истим операцијама бисмо добили грешку на стотој децимали. Према томе, грешке се појављују самим тим што нисмо у стању да операције са реалним бројевима изводимо потпуно тачно (било да користимо рачунар или не).

За памћење реалних бројева у рачунару се користи бинарни систем. Због тога чак и неки бројеви који у декадном систему имају врло кратак тачан запис, у рачунару не могу да буду тачно записани (јер тачан запис таквог броја у бинарном систему може да има бесконачно много цифара). На пример, иако бисмо очекивали да следећа наредба испише da, она ће исписати ne (проверите).

Console.WriteLine(0.1 + 0.2 == 0.3 ? "da" : "ne");

Исписивањем вредности \(0.1 + 0.2 - 0.3\) са 30 децимала добијамо
\(0.000000000000000055511151231258\). Овакав резултат се добија зато што се реални бројеви какве смо до сада користили у програмима (типа double), у рачунарима памте са 52 значајне цифре у бинарном систему, што је приближно 16 значајних цифара у декадном систему. При датом једноставном рачунању дошло је до грешке на позицији последње значајне цифре и тако добијамо ненулте цифре после 16 децимала једнаких нули.

Овим објашњењем смо само назначили проблем са којим се суочавамо и овде се нећемо бавити његовим детаљнијим проучавањем. Ипак, већ из наведеног се могу извући неки савети за практичан рад.

Претпоставимо да су подаци са којима рачунамо по вредости између 0 и 100 и да су дати са 5 децимала. То значи да имамо до 7 значајних цифара у тим подацима (2 значајне цифре пре децималне тачке и 5 после ње). Уколико после неког рачунања добијемо изразе a и b, који се разликују на шестој значајној цифри, сматраћемо да је та разлика стварна и да дати изрази и треба да буду различити. Међутим, ако је разлика тек на четрнаестој значајној цифри, треба закључити да су изрази a и b у ствари једнаки, а да је до разлике дошло услед ограничене тачности записа реалних бројева у рачунару. Одавде следи да изразе a и b не треба поредити простим испитивањем услова a == b, него испитивањем да ли се вредности a и b разликују довољно мало (занемарљиво мало), дакле испитивањем услова облика

Math.Abs(a - b) < eps

где је eps (скраћено од грчког слова \(\varepsilon\) тј. епсилон) мала позитивна константа, коју треба одредити на основу очекиваних величина и тачности података. Напоменимо да се вредност 0.1 може писати и као 1е-1, вредност 0.01 као 1е-2, вредност 0.001 као 1е-3 итд. У поменутом примеру за eps можемо да изаберемо на пример 0.000000001 или краће 1е-9 (уместо 9 би могао да стоји и неки други број већи од 7 а мањи од 16).

Слично овоме, ако реалан број x (уз исте претпоставке о величини и тачности података и вредности eps) заокружујемо на мањи или једнак цео број, уместо Math.Floor(x) боље је писати Math.Floor(x + eps).

Исто важи и за заокруживање броја x на већи или једнак цео број, где би уместо x требало писати x - eps.