2013-02-06 10 views
5

Mam prosty dla pętli w PerluPerl dla pętli dzieje bzika

for ($i=0; $i <= 360; $i += 0.01) 
{ 
print "$i "; 
} 

Dlaczego jest tak, że gdy ten kod pojawia się następujący wyjście, gdzie tak szybko, jak to robi do 0.81 to nagle zaczyna dodać obciążenie więcej miejsc dziesiętnych? Wiem, że mógłbym po prostu zaokrąglić w górę, aby uniknąć tego problemu, ale zastanawiałem się, dlaczego tak się dzieje. Przyrost 0,01 nie wydaje się szalony.

0.77 
0.78 
0.79 
0.8 
0.81 
0.820000000000001 
0.830000000000001 
0.840000000000001 
0.850000000000001 
0.860000000000001 
0.870000000000001 
+2

http://learn.perl.org/faq/perlfaq4.html#Why-am-I-getting-long-decimals-eg-19.9499999999999-instead-of-the- numbers-I-should-be-getting-eg-19.95- – mob

+0

http://stackoverflow.com/questions/10908825/, http://stackoverflow.com/questions/9790048, http://stackoverflow.com/questions/ 3916314, http://stackoverflow.com/questions/7066636, http://stackoverflow.com/questions/2080629, http://stackoverflow.com/questions/2056681, http://stackoverflow.com/questions/14204125 – mob

+0

8.1 wydaje się być niezwykle trudną do przeliczenia liczbą. Wiele lat temu miałem mini-projekt próbujący znaleźć * jakiś * sposób manipulowania konkretną wartością: "8.10" dokładnie. Zarówno Java, jak i Perl miały problemy z tym numerem. – Axeman

Odpowiedz

15

Komputery używają reprezentacji binarnych. Nie wszystkie dziesiętne liczby zmiennoprzecinkowe mają dokładne odwzorowania w notacji binarnej, więc może wystąpić błąd (w rzeczywistości różnica zaokrągleń). Jest to ta sama przyczyna why you shouldn't use floating point numbers for monetary values:

messed up recepit http://img.thedailywtf.com/images/200902/errord/DSC00669.JPG

(Zdjęcie zrobione z dailywtf)

najbardziej elegancki sposób, aby ominąć ten problem jest za pomocą liczby całkowite dla obliczeń, dzieląc je na właściwej liczby po przecinku umieszcza i używa sprintf, aby ograniczyć liczbę drukowanych miejsc dziesiętnych.Będzie to zarówno upewnić:

  • Zawsze skorygować wynik wydrukowany
  • Błąd zaokrąglenia nie kumuluje

Spróbuj kod:

#!/usr/bin/perl 
for ($i=0; $i <= 360*100; $i += 1) { 
    printf "%.2f \n", $i/100; 
} 
+7

+1 za dokumentację świadczącą o niewłaściwej praktyce programistycznej na wolności! –

7

Zasadniczo, ponieważ liczba dziesiętna 0,01 nie posiada dokładną reprezentację w binarnym zmiennoprzecinkowych, więc w czasie, dodając najlepsze przybliżenie do 0,01 odbiega od odpowiedzi chcesz.

Jest to podstawowa właściwość arytmetyki zmiennoprzecinkowej (binarnej) i nie jest specyficzna dla Perla. What Every Computer Scientist Should Know About Floating-Point Arithmetic to standardowe odniesienie i można je łatwo znaleźć za pomocą wyszukiwarki Google.

Zobacz także: C compiler bug (floating point arithmetic) i bez wątpienia miriady innych pytań.


Kernighan & Plaugera powiedzieć, w ich starym, ale klasycznej książce "The Elements of Programming Style", że:

  • Mądry stary programista powiedział kiedyś „liczb zmiennoprzecinkowych są jak małe stosy piasku; za każdym razem przenieść jeden , tracisz trochę piasku i zyskujesz trochę brudu ".

Mówią także:

  • 10 * 0,1 jest rzadko 1,0

Obie wypowiedzi wskazują, że arytmetyki zmiennoprzecinkowej nie jest precyzyjna.

Należy pamiętać, że niektóre nowoczesne procesory (IBM PowerPC) mają wbudowaną arytmetykę dziesiętnych liczb dziesiętnych zmiennoprzecinkowych IEEE 754: 2008. Jeśli Perl używał poprawnych typów (prawdopodobnie nie), twoje obliczenia byłyby dokładne.

2

1/10 jest okresowa w binarnym, podobnie jak 1/3, jest kropką dziesiętną. W związku z tym nie można jej dokładnie zapisać w liczbie zmiennoprzecinkowej.

>perl -E"say sprintf '%.17f', 0.1" 
0.10000000000000001 

Albo praca z liczb całkowitych

for (0*100..360*100) { 
    my $i = $_/100; 
    print "$i "; 
} 

czy wiele zaokrągleń

for (my $i=0; $i <= 360; $i = sprintf('%.2f', $i + 0.01)) { 
    print "$i "; 
} 
4

Aby wykazać odpowiedź Jonathana, jeśli kodować tę samą strukturę pętli w C, dostaniesz takie same wyniki. Chociaż C i Perl mogą się różnie kompilować i działać na różnych maszynach, podstawowe reguły arytmetyki zmiennoprzecinkowej powinny powodować spójne wyniki. Uwaga: Perl używa zmiennoprzecinkowej precyzji podwójnej dla swojej reprezentacji zmiennoprzecinkowej, podczas gdy w C koder wyraźnie wybiera float lub double.

pętli w C:

#include <stdio.h> 

    int main() { 
     double i; 
     for(i=0;i<=1;i+=.01) { 
      printf("%.15f\n",i); 
     } 
     } 

wyjściowa:

0.790000000000000 
    0.800000000000000 
    0.810000000000000 
    0.820000000000001 
    0.830000000000001 
    0.840000000000001 
    0.850000000000001 

Aby zademonstrować punkt nawet dalej, kod pętli w C, ale teraz używać pojedynczej precyzji zmiennoprzecinkowych arytmetycznych i zobaczyć, że na wyjściu jest mniej precyzyjny i jeszcze bardziej niekonsekwentny.

wyjściowa:

0.000000000000000 
    0.009999999776483 
    0.019999999552965 
    0.029999999329448 
    0.039999999105930 
    0.050000000745058 
    0.060000002384186 
    0.070000000298023 
    0.079999998211861 
    0.089999996125698 
    0.099999994039536