Wynik zależy od języka

0

Kod jest taki:

float a=12345678.0f;
float b=12345679.0f;
float c=1.01233995f;
float p=(a+b+c)/2;
float result=(p-b)*p;
result=((a+b+c)/2-b)*b;

W trzech językach otrzymałem różne wyniki.
'C++ 0.0 76162.5
Java 0.0 0.0
C# 76152.5 76162.5'
Dlaczego Java i C# dają różne wyniki, chociaż typ float jest w obu 32-bitowy i ma taki sam zakres?

0

To ja jeszcze dorzucę wyniki z Delphi i Free Pascala:
Delphi.png
FreePascal.png
Kod do Delphi:

{$APPTYPE CONSOLE}
Program Test;
Var A, B, C, P, Result: Extended;
Begin
 A      := 12345678;
 B      := 12345679;
 C      := 1.01233995;
 P      := (a+b+c)/2;
 Result := (p-b)*p;
 WriteLn(Result);
 Result := ((a+b+c)/2-b)*b;
 WriteLn(Result);
 ReadLn;
End.

Kod do Free Pascala:

Program Test;
Var A, B, C, P, Result: Extended;
Begin
 A      := 12345678;
 B      := 12345679;
 C      := 1.01233995;
 P      := (a+b+c)/2;
 Result := (p-b)*p;
 WriteLn(Result);
 Result := ((a+b+c)/2-b)*b;
 WriteLn(Result);
 ReadLn;
End.

Wyniki dla "Single" zamiast "Extended": Delphi: ![Delphi2.png](//static.4programmers.net/uploads/attachment/16598590454ebe39f2d4f00.png) ![FreePascal2.png](//static.4programmers.net/uploads/attachment/13893578724ebe39f748609.png)
0

uruchomione na ideone.com

Pascal (fpc) (fpc 2.2.0)     0.000000000E+00         7.617253125E+04
Pascal (gpc) (gpc 20070904)  0.000000000000000e+00   7.617253125000000e+04

C (gcc-4.3.4)                0.000000                0.000000
C++ (gcc-4.3.4)              0.000000                0.000000
C++0x (gcc-4.5.1)            0.000000                0.000000
C99 strict (gcc-4.3.4)       0.000000                76172.531250

zmienne typu float volatile

C (gcc-4.3.4)                0.000000                76172.531250
C++ (gcc-4.3.4)              0.000000                76172.531250
C++0x (gcc-4.5.1)            0.000000                76172.531250
C99 strict (gcc-4.3.4)       0.000000                76172.531250 
0

Wyniki wg.Wolfram Alpha:
(p-b)*p=(12345679-12345679)*12345679=0
((a+b+c)/2-b)*b=((12345678+12345679+1.01233995)/2-12345679)*12345679=76172.530788025
Mogłem się gdzieś pomylić, lecz chyba jest dobrze.
Ech, faktycznie był błąd z "p".
Poprawione.

0

Od razu widać, że się pomyliłeś i źle wyliczyłeś p.

0

Przy takich danych i użyciu typu float w tych obliczeniach nie dostaniesz dobrego wyniku. Odejmowanie dwóch bardzo podobnych wielkości znacznie zmniejsza Ci dokładność wyniku. Różnica jest na 10 cyfrze znaczącej, co jest za mało, żeby pomieścić w typie float. Czyli ogólny wynik tego wyrażenia ma zerową dokładność, czyli obydwa wyniki są słuszne, bo i tak nigdzie ich nie możesz użyć (bo obydwa są bezsensowne). Jeżeli wynik znacząco zależy od dokładności obliczeń to znaczy, że robisz coś źle.

0

PARI/GP
<code=PARI/GP>gp > a=12345678.0
12345678.000000000000000000000000000000000000000000
gp > b=12345679.0
12345679.000000000000000000000000000000000000000000
gp > c=1.01233995
1.0123399500000000000000000000000000000000000000000
gp > p=(a+b+c)/2
12345679.006169975000000000000000000000000000000000
gp > (p-b)*p
76172.530826093591500624999999999999999999999999994
gp > ((a+b+c)/2-b)*b
76172.530788024999999999999999999999999999999999994
^
|
+---- dokładność 57 cyfr a już tu różnica


w trakcie obliczeń za naszymi plecami dzieją się dwie rzeczy
 - typy zmiennoprzecinkowe podciągane w górę
 - optymalizator (różnie wyniki uzyska się też przy różnym stopniu optymalizacji, jak pokazałem dodanie volatile też wpływa na wyniki

Za najlepszy wynik dla float czyli single uważam 0.0 i 0.0
0

Wyniki dla Python'a 2.6.6:

a=12345678.0
b=12345679.0
c=1.01233995
p=(a+b+c)/2
print (p-b)*p #wypisze: 76172.5284054
print ((a+b+c)/2-b)*b #wypisze: 76172.5283673
bogdans napisał(a)

Dlaczego Java i C# dają różne wyniki, chociaż typ float jest w obu 32-bitowy i ma taki sam zakres?

próbowałeś zamiast 2 wpisywać 2.0? Niektóre języki są na to wrażliwe, python w obu przypadkach daje te same wyniki. No i w sumie Python nie ma float'ów, wszystkie liczby zmiennoprzecinkowe są typu double.

0

Próbowałem. Kodu z 2.0 ani w Javie, ani w C# nie można skompilować. Kod z 2.0f daje takie same wyniki jak kod z 2

0

Tak dla porównania z resztą w przypadku php wyniki są za to zbliżone to tych z pythona i delphi, pari/gp (na 64bit systemie).

 
//na piechotę
ini_set('precision', 64);
$a=(float)12345678.0;//(float), (double), (real) są aliasami w php
$b=(float)12345679.0;
$c=(float)1.01233995;
$p=($a+$b+$c)/2;
$result=($p-$b)*$p;
var_dump($result);
$result=(($a+$b+$c)/2.0-$b)*$b;
var_dump($result);
//za pomocą bcmath
$a='12345678.0';
$b='12345679.0';
$c='1.01233995';
$precision = 64;
$p=
bcdiv(
  bcadd(
    bcadd($a,$b,$precision)
  ,$c,$precision)
,'2',$precision);
$result=
bcmul(
  bcsub($p,$b,$precision)
,$p,$precision);
var_dump($result);
$result=
bcmul(
  bcsub(
    bcdiv(
      bcadd(
        bcadd($a,$b,$precision)
      ,$c,$precision)
    ,'2',$precision)
  ,$b,$precision)
,$b,$precision);
var_dump($result);

//float(76172.52840540916076861321926116943359375)
//float(76172.528367340564727783203125)
//string(70) "76172.5308260935915006250000000000000000000000000000000000000000000000"
//string(70) "76172.5307880250000000000000000000000000000000000000000000000000000000"

A tak w ogóle do liczb zmiennoprzecinkowych, gdzie potrzeba dużej dokładności lepiej użyć jakiejś biblioteki typu Arbitrary Precision Arithmetic do znalezienia np. na http://en.wikipedia.org/wiki/Bignum#Libraries ;)
I taki tip jeszcze, nigdy nie ufaj wynikom w float co do dokładności i nigdy ich bezpośrednio nie porównuj.
Niektóre wartości w zapisie dziesiętnym po prostu nie mają reprezentacji w zapisie o podst. 2. Przykładowo 0.1, 0.7 zawsze będzie obdarzony błędem np.
wynikiem floor(0.1+0.7)*10 będzie 7 ponieważ zaokrąglać będzie coś w rodzaju 7.9999999999999991118... ;)
(powyższe zaczerpnięte z http://pl2.php.net/manual/en/language.types.float.php , ale jest słuszne i dla innych języków, np. c/c++, python, itd.)

A tak jeszcze wracając bardziej do tematu, nie wiem na ile to jest prawdziwe(może ktoś się zna lepiej na C#), ale wg
http://www.homeandlearn.co.uk/csharp/csharp_s2p6.html
w C# skoro określiłeś wartości jako float to zaokrągli ci na 7 cyfrze liczbę (1.01233995 == 1.012340) ;)

0

Z podanego przykładu jasno wynika, że nie należy używać do obliczeń matematycznych typu float. Nie rozumiem natomiast ludzi, którzy uważają, że podejście C++ jest słuszne. Konsekwencją tego podejścia jest takie coś: piszę wzór na obliczanie pola trójkąta z wzoru Herona:

float pole=sqrt(((a+b+c)/2)*((a+b+c)/2-a)*((a+b+c)/2-b)*((a+b+c)/2-c));

po czym dostrzegam, że w tym wzorze niepotrzebnie liczę cztery razy to samo wyrażenie i zmieniam kod:

float p=(a+b+c)/2;
float pole=sqrt(p*(p-a)*(p-b)*(p-c));

I wynik się zmienia. Dla danych z pierwszego postu z 972730 na 0. I to jest "słuszne"?

0

nie no, z tym "nie należy" to trochę cię poniosło, trzeba tylko wiedzieć co się robi
i ja wolałbym, żeby mniej się działo za moimi plecami.

"Podejście" C++ ma też pewne zalety, kompilator potrafi przechować pewne pośrednie wyniki o podniesionej precyzji
tyle, że trzeba być tego świadomym
może się okazać, że troszkę dalej je zgubi i odczyta ze zmiennej ale to już może być inna liczba

"Matematyka to strasznie ostre narzędzie nie należy go dawać idiotom" S.Banach

0

Ja w C# kompilując dla x86 otrzymałem: 0.0 i 76172,53, zaś dla x64 dwukrotnie było 0,0.

bogdans napisał(a)

Próbowałem. Kodu z 2.0 ani w Javie, ani w C# nie można skompilować. Kod z 2.0f daje takie same wyniki jak kod z 2

Liczba 2.0 jest typu double, zatem pozostałe liczby w działaniu również są konwertowane do tego typu, więc i wynik jest double, a takiego nie da się wstawić do zmiennej typu float.
Zaś samo 2 (całkowite) jest niejawnie konwertowane do float (zgodnie z: http://msdn.microsoft.com/en-us/library/y5b434w4.aspx) czyli w tym działaniu 2 == 2.0f.

Implementacja float w C# (czyli System.Single w .NET) jest zgodna z IEE 754. W Javie jest jakaś inna?

MSDN napisał(a)

The Single value type represents a single-precision 32-bit number with values ranging from negative 3.402823e38 to positive 3.402823e38, as well as positive or negative zero, PositiveInfinity, NegativeInfinity, and not a number (NaN).

Single complies with the IEC 60559:1989 (IEEE 754) standard for binary floating-point arithmetic.
(...)
Floating-Point Values and Loss of Precision

Remember that a floating-point number can only approximate a decimal number, and that the precision of a floating-point number determines how accurately that number approximates a decimal number. By default, a Single value contains only 7 decimal digits of precision, although a maximum of 9 digits is maintained internally. The precision of a floating-point number has several consequences:

Za: http://msdn.microsoft.com/en-us/library/system.single.aspx

0

No toż przecie wiadomo, że przy floatach (a + b) + c != a + (b + c). Zależnie od optymalizacji kompilatora wynik będzie różny, jeżeli kompilator/ maszyna wirtualna/ whatever nie bierze tego zjawiska pod uwagę. Zrobiłem bardziej rozwlekły przykład pod Javę:

public class Main {

    public static void main(String[] args) {
        new Main().run();
    }

    void run() {
        System.out.println("loose");
        loose();
        System.out.println("strict");
        strict();
    }

    void loose() {
        float a = 12345678.0f;
        System.out.println(a);
        float b = 12345679.0f;
        System.out.println(b);
        float c = 1.01233995f;
        System.out.println(c);
        float p = (a + b + c) / 2;
        System.out.println((p - b) * p);
        System.out.println(((a + b + c) / 2 - b) * b);
        float r1 = (a + b);
        System.out.println(r1);
        float r2 = (r1 + c);
        System.out.println(r2);
        float r3 = r2 / 2;
        System.out.println(r3);
        float r4 = r3 - b;
        System.out.println(r4);
        float r5 = r4 * r3;
        System.out.println(r5);
    }

    strictfp void strict() {
        float a = 12345678.0f;
        System.out.println(a);
        float b = 12345679.0f;
        System.out.println(b);
        float c = 1.01233995f;
        System.out.println(c);
        float p = (a + b + c) / 2;
        System.out.println((p - b) * p);
        System.out.println(((a + b + c) / 2 - b) * b);
        float r1 = (a + b);
        System.out.println(r1);
        float r2 = (r1 + c);
        System.out.println(r2);
        float r3 = r2 / 2;
        System.out.println(r3);
        float r4 = r3 - b;
        System.out.println(r4);
        float r5 = r4 * r3;
        System.out.println(r5);
    }
}

Wyniki:

run:
loose
1.2345678E7
1.2345679E7
1.01234
0.0
0.0
2.4691356E7
2.4691358E7
1.2345679E7
0.0
0.0
strict
1.2345678E7
1.2345679E7
1.01234
0.0
0.0
2.4691356E7
2.4691358E7
1.2345679E7
0.0
0.0
BUILD SUCCESSFUL (total time: 1 second)

Wynika z tego, że kod w Javie jest najbardziej przewidywalny. C++ i C# pewnie liczą sobie wynik na etapie kompilacji w rozszerzonej precyzji, stąd inny wynik.

0

Przeprowadziłem małe śledztwo.

Specyfikacja standardu ECMA-335 (w której istnienie wątpią niektórzy hejterzy MS, bo w końcu niemożliwe, żeby CLI miało otwartą i powszechnie dostępną specyfikację) stwierdza w części I, w rozdziale 12.3.1, że:

Storage locations for floating-point numbers (statics, array elements, and fields of classes) are of fixed size. The supported storage sizes are float32 and float64. Everywhere else (on the evaluation stack, as arguments, as return types, and as local variables) floating-point numbers are represented using an internal floating-point
type. In each such instance, the nominal type of the variable or expression is either float32or float64, but its value can be represented internally with additional range and/or precision. The size of the internal floatingpoint representation is implementation-dependent, can vary, and shall have precision at least as great as that of the variable or expression being represented. An implicit widening conversion to the internal representation from float32 or float64 is performed when those types are loaded from storage. The internal representation is typically the native size for the hardware, or as required for efficient implementation of an operation. The internal representation shall have the following characteristics:
• The internal representation shall have precision and range greater than or equal to the nominal
type.
• Conversions to and from the internal representation shall preserve value.

A co z tego wynika:

This design allows the CLI to choose a platform-specific high-performance representation for floating-point numbers until they are placed in storage locations. For example, it might be able to leave floating-point variables in hardware registers that provide more precision than a user has requested. At the same time, CIL generators can force operations to respect language-specific rules for representations through the use of conversion instructions.

Co jest potwierdzone w specyfikacji C#:

Floating-point operations may be performed with higher precision than the result type of the operation. For example, some hardware architectures support an "extended" or "long double" floating-point type with greater range and precision than the double type, and implicitly perform all floating-point operations using this higher precision type.

Kończąc wyjaśnienia - obliczenia na floatach w x86 korzystają z 80 bitowego FPU, zaś te same obliczenia na x64 korzystają z 128 bitowego SSE.
Przynajmniej tak to rozumiem: http://www.manicai.net/comp/debugging/fpudiff/

Kompilacja w trybie x86 zapewne wymusza użycie obliczeń na FPU zawsze i wszędzie, więc daje wyniki liczone z mniejszą precyzją.

Nie znam Javy (nie mam IQ programisty Delphi), ale na tyle ile zrozumiałem z Wiki i Osiłka, to ten sam efekt (mniejszej precyzji obliczeń) daje użycie strictfp w Javie. Niemniej jednak moi szpiedzy z Gdańska donoszą, że Java dla kodu z pierwszego posta zwraca identyczne wyniki na x86 i x64 bez względu na użycie tego słowa kluczowego, więc nie mam pojęcia jak jest naprawdę i po co ono właściwie jest, skoro nie zmienia wyniku.

Reasumując, specyfikacja IEEE 754 jest do d**y, bo nie precyzuje jak mają być przeprowadzane obliczenia i stąd te różnice. ;)

1 użytkowników online, w tym zalogowanych: 0, gości: 1