Der Grund warum C weg von 0 runden kann und Pascal nicht ist das C zwei Rundungsfunktionen besitzt:
round wozu der POSIX Standard das folgende sagt
These functions shall round their argument to the nearest integer value in floating-point format, rounding halfway cases away from zero, regardless of the current rounding direction.
Sowie
rint
These functions shall return the integral value (represented as a double) nearest x in the direction of the current rounding mode. The current rounding mode is implementation-defined.
If the current rounding mode rounds toward negative infinity, then rint() shall be equivalent to floor. If the current rounding mode rounds toward positive infinity, then rint() shall be equivalent to ceil. If the current rounding mode rounds towards zero, then rint() shall be equivalent to trunc. [MX] [Option Start] If the current rounding mode rounds towards nearest, then rint() differs from round in that halfway cases are rounded to even rather than away from zero. [Option End]
Der Grund dafür ist, das die FPUs verschiedene Rounding Modes implementieren und damit rint, was das Implementierungsspezifische Runden der FPU ausnutzen kann deutlich effizienter ist als round, was zwar "konsistent" ist, dafür emuliert werden muss.
Beispiel:
Code: Alles auswählen
#include <stdio.h>
#include <math.h>
#include <time.h>
int main()
{
int acc=0;
clock_t start = clock();
for(int i=0;i<100000000; ++i) {
acc += (int)round(i+0.5);
}
clock_t end = clock();
printf("round: %d: %f\n", acc, (float)(end-start)/CLOCKS_PER_SEC);
acc=0;
start = clock();
for(int i=0;i<100000000; ++i) {
acc += (int)rint(i+0.5);
}
end = clock();
printf("rint: %d: %f\n", acc, (float)(end-start)/CLOCKS_PER_SEC);
return 0;
}
rint, was die Hardware der FPU voll ausnutzt ist extrem viel effizienter.
Und wie ich immer schreibe wenn dieses Thema aufkommt, Nearest-Even-Rounding (also das was FPC bei Round macht), ist eigentlich in fast allen Situationen die bessere Option, da es im Durchschnitt genauer ist:
Code: Alles auswählen
program Project1;
{$mode objfpc}{$H+}
uses
sysutils, math;
function RoundAway(d: Double): Integer;
begin
if d < 0 then
Result := trunc(d-0.5)
else
Result := trunc(d+0.5);
end;
function RoundError(Iters: Integer): Double;
var
Sum, rnd: Double;
RoundedSum, i: Int64;
begin
Sum := 0;
RoundedSum := 0;
for i:=1 to Iters do
begin
rnd := Random(100)/10;
Sum += rnd;
RoundedSum += Round(rnd);
end;
Result := Sum - RoundedSum;
end;
function RoundAwayError(Iters: Integer): Double;
var
Sum, rnd: Double;
RoundedSum, i: Int64;
begin
Sum := 0;
RoundedSum := 0;
for i:=1 to Iters do
begin
rnd := Random(100)/10;
Sum += rnd;
RoundedSum += RoundAway(rnd);
end;
Result := Sum - RoundedSum;
end;
begin
Randomize;
WriteLn('RoundError(1000): ', RoundError(1000).ToString);
WriteLn('RoundError(10000): ', RoundError(10000).ToString);
WriteLn('RoundError(100000): ', RoundError(100000).ToString);
WriteLn('RoundAwayError(1000): ', RoundAwayError(1000).ToString);
WriteLn('RoundAwayError(10000): ', RoundAwayError(10000).ToString);
WriteLn('RoundAwayError(100000): ', RoundAwayError(100000).ToString);
end.
Ergebnis:
Code: Alles auswählen
RoundError(1000): 11,7000000000062
RoundError(10000): -19,199999999837
RoundError(100000): -8,19999999884749
RoundAwayError(1000): -55,8000000000002
RoundAwayError(10000): -507,600000000035
RoundAwayError(100000): -5056,80000000255
Der Rundungsfehler beim Away-From-0 Rounding (also das "Schulrunden") ist nicht nur deutlich größer, sondern akkumuliert sich über mehrere Berechnungen. Nearest-Even-Rounding der FPU hingegen hat im durchschnitt einen sehr kleinen Fehler der auch egal wie viele Operationen man durchführt nicht zunimmt.
wp_xyz hat geschrieben: ↑Mi 20. Dez 2023, 20:15
Round verwende ich in der Regel nur, wenn ein Float in einen Integer konvertiert werden muss, so wie in der Graphik auf dem Monitor mit ganzzahligen Pixeln. Und da, ganz ehrlich, fällt es in den allermeisten Fällen nicht auf, wenn wegen Bankers-Rounding die Mitte eines Rechtecks um 1 Pixel nach oben rutscht, je nachdem ob die Rechteckhöhe gerade oder ungerade ist.
Da muss man aber auch dazu sagen das das mehr eine Limitation des Canvas in Lazarus ist als alles andere. Moderne Grafikbibliotheken unterstützen Subpixel rendering, womit man Problemlos auch halbe Pixel ansteuern kann ohne das man Hässliche Sprünge oder Knicks hat. Zum Beispiel das HTML5 Canvas das Browser unterstützen:
Code: Alles auswählen
<canvas id="myCanvas" width="200" height="100" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML canvas tag.</canvas>
<script>
var pos = 0;
function line() {
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.clearRect(0,0,c.width,c.height);
ctx.beginPath();
ctx.moveTo(pos,0);
ctx.lineTo(pos,100);
ctx.stroke();
pos+=0.5;
if (pos>c.width) pos=0;
window.setTimeout(line, 1);
}
line();
</script>
Gibt eine Animation mit einer Linie die sich ganz sauber von links nach rechts bewegt in halbpixel Schritten.
Das Problem ist halt eher das wir mit dem LCL Canvas bei der Technologie von Win32 Forms aus den 90ern Hängen geblieben sind.