Genauigkeit beim Rechnen

Für Fragen zur Programmiersprache auf welcher Lazarus aufbaut
Benutzeravatar
Niesi
Lazarusforum e. V.
Beiträge: 612
Registriert: So 26. Jun 2016, 19:44
OS, Lazarus, FPC: Linux Mint Cinnamon, Laz 4.1 Fpc 3.2.3 und allerlei mit FpcUpDeLuxe
Kontaktdaten:

Genauigkeit beim Rechnen

Beitrag von Niesi »

.

Moin,

hat jemand eine Ahnung, was das für ein ... sagen wir mal: "Mieses Ding" ist?

Wenn ich einen Wert mit "1.0" im Code habe, dann kommt da was anderes raus als bei "1" (ohne Dezimalpunkt).

Das haut mich gerade um ...
MainForm_001.png
MainForm_001.png (16.43 KiB) 851 mal betrachtet
RadDegRad.7z
(129.94 KiB) 80-mal heruntergeladen

Passiert mit Linux Mint Cinnamon 22.1 und auch in Windows 10

Mal ganz, ganz vorsichtig ausgedrückt: Das geht gar nicht, oder? :mrgreen:


Mit meiner Float2Str() sieht das dann so aus:

Auswahl_002.png
Auswahl_002.png (8.6 KiB) 851 mal betrachtet
Wissen ist das einzige Gut, das sich vermehrt, wenn es geteilt wird ...

Benutzeravatar
m.fuchs
Lazarusforum e. V.
Beiträge: 2825
Registriert: Fr 22. Sep 2006, 19:32
OS, Lazarus, FPC: Winux (Lazarus 2.0.10, FPC 3.2.0)
CPU-Target: x86, x64, arm
Wohnort: Berlin
Kontaktdaten:

Re: Genauigkeit beim Rechnen

Beitrag von m.fuchs »

Schau mal hier:

Code: Alles auswählen

program ExtendedTest;
{$MODE ObjFpc}
{$H+}

uses
  SysUtils;

var
  x: Extended;
  a_rad: Extended;

begin
    a_rad := 1.0 / 180 * pi;
    WriteLn(FloatToStr(a_rad));

    a_rad := 1 / 180 * pi;
    WriteLn(FloatToStr(a_rad));

    WriteLn;

    x := 1.0;
    a_rad := x / 180 * pi;
    WriteLn(FloatToStr(a_rad));

    x := 1;
    a_rad := x / 180 * pi;
    WriteLn(FloatToStr(a_rad));
end.
Ausgabe:

Code: Alles auswählen

0.0174532929425641
0.0174532925199433

0.0174532925199433
0.0174532925199433
Software, Bibliotheken, Vorträge und mehr: https://www.ypa-software.de

Benutzeravatar
Niesi
Lazarusforum e. V.
Beiträge: 612
Registriert: So 26. Jun 2016, 19:44
OS, Lazarus, FPC: Linux Mint Cinnamon, Laz 4.1 Fpc 3.2.3 und allerlei mit FpcUpDeLuxe
Kontaktdaten:

Re: Genauigkeit beim Rechnen

Beitrag von Niesi »

m.fuchs hat geschrieben: Fr 20. Jun 2025, 18:48 Schau mal hier:
Ja, Danke für die Mühe.

ABER: Warum???????
Wissen ist das einzige Gut, das sich vermehrt, wenn es geteilt wird ...

Mathias
Beiträge: 6967
Registriert: Do 2. Jan 2014, 17:21
OS, Lazarus, FPC: Linux (die neusten Trunk)
CPU-Target: 64Bit
Wohnort: Schweiz

Re: Genauigkeit beim Rechnen

Beitrag von Mathias »

So wie es scheint, nimmt der im ersten Beispiel ein Single für 1.0

Folgendes spuckt das gleiche aus:

Code: Alles auswählen

  a_rad := single(1.0) / 180 * pi;
  WriteLn(FloatToStr(a_rad));
  a_rad := 1.0 / 180 * pi;
  WriteLn(FloatToStr(a_rad)); 
Mit Lazarus sehe ich grün
Mit Java und C/C++ sehe ich rot

Benutzeravatar
m.fuchs
Lazarusforum e. V.
Beiträge: 2825
Registriert: Fr 22. Sep 2006, 19:32
OS, Lazarus, FPC: Winux (Lazarus 2.0.10, FPC 3.2.0)
CPU-Target: x86, x64, arm
Wohnort: Berlin
Kontaktdaten:

Re: Genauigkeit beim Rechnen

Beitrag von m.fuchs »

Niesi hat geschrieben: Fr 20. Jun 2025, 18:52 ABER: Warum???????
Wenn du beim Einsatz von Fließkommazahlen eine bestimmte Genauigkeit benötigst, musst du dafür Sorgen dass alle Variablen die in die Berechnung einfließen auch in dieser Genauigkeit vorliegen.
Software, Bibliotheken, Vorträge und mehr: https://www.ypa-software.de

Benutzeravatar
Niesi
Lazarusforum e. V.
Beiträge: 612
Registriert: So 26. Jun 2016, 19:44
OS, Lazarus, FPC: Linux Mint Cinnamon, Laz 4.1 Fpc 3.2.3 und allerlei mit FpcUpDeLuxe
Kontaktdaten:

Re: Genauigkeit beim Rechnen

Beitrag von Niesi »

Mathias hat geschrieben: Fr 20. Jun 2025, 19:13 So wie es scheint, nimmt der im ersten Beispiel ein Single für 1.0

Folgendes spuckt das gleiche aus:

Code: Alles auswählen

  a_rad := single(1.0) / 180 * pi;
  WriteLn(FloatToStr(a_rad));
  a_rad := 1.0 / 180 * pi;
  WriteLn(FloatToStr(a_rad)); 
Aha.

Ich verstehe ...

Ist trotzdem schlimm.

Im Turbo Pascal gab es mal das Problem, dass Kommawerte im Quelltext immer mit dem Dezimalpunkt geschrieben werden mussten - sonst war es daneben. Daher mache ich das immer noch so, dass ich Kommazahlen mit Punkt :mrgreen: in die Source schreibe ...

Jedenfalls: Danke.
Zuletzt geändert von Niesi am Fr 20. Jun 2025, 19:31, insgesamt 1-mal geändert.
Wissen ist das einzige Gut, das sich vermehrt, wenn es geteilt wird ...

Benutzeravatar
Niesi
Lazarusforum e. V.
Beiträge: 612
Registriert: So 26. Jun 2016, 19:44
OS, Lazarus, FPC: Linux Mint Cinnamon, Laz 4.1 Fpc 3.2.3 und allerlei mit FpcUpDeLuxe
Kontaktdaten:

Re: Genauigkeit beim Rechnen

Beitrag von Niesi »

m.fuchs hat geschrieben: Fr 20. Jun 2025, 19:20
Niesi hat geschrieben: Fr 20. Jun 2025, 18:52 ABER: Warum???????
Wenn du beim Einsatz von Fließkommazahlen eine bestimmte Genauigkeit benötigst, musst du dafür Sorgen dass alle Variablen die in die Berechnung einfließen auch in dieser Genauigkeit vorliegen.
Ja, ok.

Warum wird ein "1.0" aus dem Quelltext anders interpretiert als eine "1"?

Ist irgendwie nicht so schlau, aber ähnliche Probleme gab es früher auch schon.

Ok, Danke, das hilft mir weiter ...
Wissen ist das einzige Gut, das sich vermehrt, wenn es geteilt wird ...

Benutzeravatar
m.fuchs
Lazarusforum e. V.
Beiträge: 2825
Registriert: Fr 22. Sep 2006, 19:32
OS, Lazarus, FPC: Winux (Lazarus 2.0.10, FPC 3.2.0)
CPU-Target: x86, x64, arm
Wohnort: Berlin
Kontaktdaten:

Re: Genauigkeit beim Rechnen

Beitrag von m.fuchs »

Niesi hat geschrieben: Fr 20. Jun 2025, 19:31 Warum wird ein "1.0" aus dem Quelltext anders interpretiert als eine "1"?
1.0 ist ein Single, 1 ist ein Integer. Das dürfte dann die Erklärung sein. Genaueres weiß sicherlich jemand wie PascalDragon.
Software, Bibliotheken, Vorträge und mehr: https://www.ypa-software.de

Warf
Beiträge: 2148
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Genauigkeit beim Rechnen

Beitrag von Warf »

Ganz einfach, Floats sind binäre Fließkommazahlen nach IEEE 754. Das heißt jede Zahl wird ausgedrückt als 1.m * 2^e wobei m die mantisse ist, was ein bitwort ist und e der exponent.

Im Dezimalsystem ist z.B. die zahl 1/2 gleich 0.5 oder 5*10^-1. Im binärsystem ists 0.1, oder 1*2^-1. Jetzt lässt sich aber nicht jede Zahl in jeder Representation endlich darstellen. 1/3 z.B. ist 0.33333... im Dezimalsystem, im tenärsystem allerdings 0.1 oder 1*3^-1. Generell kann man sagen in einem Zahlensystem N lassen sich nur Brüche endlich Darstellen die aus den Primfaktoren von N zusammen gebaut werden. Im Dezimalsystem sind das also 2 und 5, daher lässt sich 1/2 oder 1/5 darstellen, aber nicht 1/3. Genau das gleiche gilt im Binärsystem, da lassen sich nur Brüche deren Teiler ein Vielfaches oder eine Summe von Vielvachen von 2 sind darstellen.

D.h. Das die Endlichen Zahlen im Binärsystem sind eine strikte Teilmenge der endlichen Zahlen im Dezimalsystem und nicht alles was im Dezimalsystem Endlich darstellbar ist ist so auch im Binärsystem. 1/5 ist zwar 0.2 im Dezimalsystem und damit endlich darstellbar, im Binärsystem allerdings 0.001111100100110011... und weil den PC nicht endlos viel Speicher hat muss an einer stelle gerundet werden. Bei Single ist die Mantisse 24 bit lang, bei Double 53 und bei 80 bit Extended 64 bit. Alles was über die Mantissenlänge hinausgeht wird gerundet.
Ganzzahlen lassen sich immer korrekt darstellen solang diese kleiner als die Mantissenlänge sind, als Double enthält alle integers von 0..2^53-1. Extended enthält alle 64 bit integers (sogar unsigned). Alles was größer ist hat rundungsfehler.

Mathias
Beiträge: 6967
Registriert: Do 2. Jan 2014, 17:21
OS, Lazarus, FPC: Linux (die neusten Trunk)
CPU-Target: 64Bit
Wohnort: Schweiz

Re: Genauigkeit beim Rechnen

Beitrag von Mathias »

Im Turbo Pascal gab es mal das Problem, dass Kommawerte im Quelltext immer mit dem Dezimalpunkt geschrieben werden mussten - sonst war es daneben. Daher mache ich das immer noch so, dass ich Kommazahlen mit Punkt :mrgreen: in die Source schreibe ...
Für mich war das ganz normal, bis ich vor ein paar Jahren erfahren habe, das in den EU-Ländern ein Komma und kein Punkt verwendet wird.
Alle Pascal Funktionen, wie Str, Val, Write(ln), arbeiten nur mit dem Punkt. erst das neumodische Zeugs, was mit Delphi eingeführt wurde, kann auch mit dem Komma umgehen, Wobei wen man tief in FloatToStr reinschaut, wird da mit viel Overhead aus dem Komma ein Punkt gemacht, und dann folgt das altbekannt Str.
Mit Lazarus sehe ich grün
Mit Java und C/C++ sehe ich rot

Mathias
Beiträge: 6967
Registriert: Do 2. Jan 2014, 17:21
OS, Lazarus, FPC: Linux (die neusten Trunk)
CPU-Target: 64Bit
Wohnort: Schweiz

Re: Genauigkeit beim Rechnen

Beitrag von Mathias »

Warum wird ein "1.0" aus dem Quelltext anders interpretiert als eine "1"?

Ist irgendwie nicht so schlau, aber ähnliche Probleme gab es früher auch schon.
Ich vermute das hängt mit einem ähnlichem Problem zusammen, über welches ich kürzlich gestolpert bin.
Da hatte er Mühe mit dem automatischen Chat. Es kommt ein Fehler beim kompilieren.

Code: Alles auswählen

  procedure Test(s: single);
  begin
    WriteLn('Single: ', s);
  end;

  procedure Test(i: PtrUInt);
  begin
    WriteLn('PtrUInt: ', i);
  end;

begin
  Test(1.0);
  Test(PtrUInt(1));
  Test(Integer(1)); // geht nicht
  Test(1);          // geht nicht
end.                  
Mit Lazarus sehe ich grün
Mit Java und C/C++ sehe ich rot

Benutzeravatar
Jorg3000
Lazarusforum e. V.
Beiträge: 390
Registriert: So 10. Okt 2021, 10:24
OS, Lazarus, FPC: Win64
Wohnort: NRW

Re: Genauigkeit beim Rechnen

Beitrag von Jorg3000 »

Guten Morgen!
Dass eine untypisierte Gleitkomma-Konstante als Single verwendet wird, war mir bisher auch nicht bewusst.
Folgendes Web-Fundstück erklärt das Verhalten: "Normally, the compiler will set the precision of a floating point constant to the minimally required precision to represent it exactly".
Wenn das stimmt, unternimmt der Compiler also sogar extra eine Prüfung, ob er bei einem Ausdruck wie 1.0 Speicher sparen kann, weil für 1.0 keine hohe Präzision benötigt wird. Das Verhalten ist aber (erst dann) nachteilig, sobald damit gerechnet wird.

SizeOf(pi) => 8
SizeOf(1.0) => 4
SizeOf(1.0/180) => 4
SizeOf(1/180) => 8

Wie man am untersten Beispiel 1/180 sieht, wählt der Compiler für eine Berechnung durchaus vorzugsweise die Double-Precision, außer wenn durch eine Konstante, für die vermeintlich die Single-Präzision ausreicht, der Compiler dann auch die Berechnung in einfacher Präzision ansetzt.
Das ist gut zu wissen, denn somit sollte man Konstanten manuell typisieren: Double(1.0) ... wenn sie für Berechnungen genutzt werden sollen.

Zur Ehrenrettung von FreePascal muss man sagen, dass dieses Verhalten in Delphi auch so war (zumindest vor Version 2009).
Schon in früheren Delphi-Versionen gab es den Compiler-Schalter {$MINFPCONSTPREC 64} womit die Precision für untypisierte Gleitkomma-Konstanten auf 64 Bit (Double) hochgesetzt werden kann.
Dies wird auch im FreePascal-Wiki beschrieben, in der untersten Zeile: https://wiki.freepascal.org/IEEE_754_formats

{$MINFPCONSTPREC 64}
SizeOf(1.0) => 8
SizeOf(1.0/180) => 8

Für Units mit Berechnungen könnte man sich angewöhnen, immer den Compiler-Schalter zu setzen, denn ich glaube heutzutage machen untypisierte Konstanten als Single keinen Sinn mehr.
Grüße, Jörg

Mathias
Beiträge: 6967
Registriert: Do 2. Jan 2014, 17:21
OS, Lazarus, FPC: Linux (die neusten Trunk)
CPU-Target: 64Bit
Wohnort: Schweiz

Re: Genauigkeit beim Rechnen

Beitrag von Mathias »

Für Units mit Berechnungen könnte man sich angewöhnen, immer den Compiler-Schalter zu setzen, denn ich glaube heutzutage machen untypisierte Konstanten als Single keinen Sinn mehr.
Grüße, Jörg
Für Vektorgrafik macht Single durchaus noch Sinn. OpenGL, Vulkan arbeiten immer noch mit Float.
Ich habe mal kurz einen Test gemacht, Single ist doch ein wenig schneller als Double. Getestet mit 64bit Linux.
Bei einer 32bit CPU sieht dies sicher ganz anders aus.
Aber im Alltag wird dies kaum relevant sein.

Code: Alles auswählen

var
  t: TDateTime;
  f: single = 0;
  d:Double=0;
  i: int64;
begin
  t := now;
  for i := 0 to 10000000000 do begin
    f := f * i / (f + single(1) + f);
  end;
  WriteLn(f:10:5);
  WriteLn('Single time: ', FormatDateTime('hh:nn:ss.zzz', Now - t));
  t := now;

  for i := 0 to 10000000000 do begin
    d := d * i / (d + double(1) + d);
  end;
  WriteLn(d:10:5);
  WriteLn('Double time: ', FormatDateTime('hh:nn:ss.zzz', Now - t));
end;   
Ausgabe:

Code: Alles auswählen

   0.00000
Single time: 00:00:44.324
   0.00000
Double time: 00:00:49.158
Mit Lazarus sehe ich grün
Mit Java und C/C++ sehe ich rot

Mathias
Beiträge: 6967
Registriert: Do 2. Jan 2014, 17:21
OS, Lazarus, FPC: Linux (die neusten Trunk)
CPU-Target: 64Bit
Wohnort: Schweiz

Re: Genauigkeit beim Rechnen

Beitrag von Mathias »

Bei einer 32bit CPU sieht dies sicher ganz anders aus.
Da habe ich mich wohl getäuscht.

Code: Alles auswählen

   0.00000
Single time: 00:00:11.950
   0.00000
Double time: 00:00:11.908
Die Schleife habe ich noch ein wenig geändert, da es kein int64 gibt.

Code: Alles auswählen

for i := 0 to MaxInt do begin
Mit Lazarus sehe ich grün
Mit Java und C/C++ sehe ich rot

Benutzeravatar
Niesi
Lazarusforum e. V.
Beiträge: 612
Registriert: So 26. Jun 2016, 19:44
OS, Lazarus, FPC: Linux Mint Cinnamon, Laz 4.1 Fpc 3.2.3 und allerlei mit FpcUpDeLuxe
Kontaktdaten:

Re: Genauigkeit beim Rechnen

Beitrag von Niesi »

Habe mich noch mal damit beschäftigt.

Und bin - na ja - enttäuscht. Ich hatte gehofft, diesmal säße das Problem nicht wieder vor meiner Tastatur. Aber so ist das halt ... :mrgreen:

Wie Jörg habe ich im web den Hinweis gefunden, dass der Compiler immer auf die Typen geht, die weniger Speicher benötigen. Danke, Jörg, bis zum Pascal-Wiki war ich noch nicht vorgedrungen. Da steht das, und so ist es auch.

Das MÜSSEN Programmierende wissen, denke ich. Denn die Unterschiede können entscheidend sein.

Hier noch mal eine kleine Übersicht, wie unterschiedlich die Genauigkeit sein kann. So, wie Michael es gezeigt hat, wird es am besten. Denke ich im Moment ...

ExtendedTest_001.png
ExtendedTest_001.png (68.98 KiB) 638 mal betrachtet
ExtendedTest.7z
(152.72 KiB) 206-mal heruntergeladen
Wissen ist das einzige Gut, das sich vermehrt, wenn es geteilt wird ...

Antworten