Because system.round() uses "bankers rounding", long time ago I created a "normal" rounding function:
function round_normal(x: double): int64;
var k: double;
begin
if x < 0 then k:=-0.5 else k:=0.5;
exit(round(int(x+k)));
end;
This worked all the time with 32-bit. Now with 64-bit I found strange rounding issues for larger values around 4.5E+15:
- only for these larger values and
- only for odd values and
- only with 64-bit
then the result of my function is too big by 1.
And function
math.SimpleRoundTo() - which is based on the same formula - has exactly the same behaviour.
Examples:
- for x=4503,599627,370
495 both my round_normal() and math.SimpleRoundTo() return the same as 'x' (which is correct)
- same for x=4503,599627,370
496 - but for x=4503,599627,370
497 both my round_normal() and math.SimpleRoundTo() return 4503,599627,370
498, which is too big by 1.
I created a demo (attached as compilable project) to test this:
{$mode objfpc} {$H+}
{$OPTIMIZATION OFF} {same results with Optimitation Level 1}
uses math;
function komma6Str(s: string): string;
{inserts Kommas ',' for each 6 digits; Usable for integers, floats, empty strings and negative numbers}
var i,m: integer;
begin
i:=pos('.',s); if i > 0 then dec(i) else i:=length(s);
m:=6; if s[1]='-' then m:=7; {auch ok wenn s=''}
while i > m do begin insert(',', s,i-5); dec(i,6); end;
exit(s);
end;
function i64Str(i: int64): string;
{returns 'i' as a string with Kommas ',' for each 6 digits}
var s: string;
begin
str(i,s);
exit(komma6Str(s)); {inserts Kommas ',' for each 6 digits}
end;
function delTrailingDotZeroes(s: shortstring): shortstring;
{cuts all trailing zeros and decimal dot '.' if last char}
begin
while (length(s) > 0) and (s[length(s)] = '0') do dec(s[0]);
if (length(s) > 0) and (s[length(s)] = '.') then dec(s[0]);
if s='' then s:='0';
exit(s);
end;
function floatStr(x: extended): string;
{returns 'x' as a string with Kommas ',' for each 6 digits}
var s: string;
begin
str(x:0:30, s);
if pos('E',s) > 0 then exit(s); {if exponential format}
s:=delTrailingDotZeroes(s); {cuts trailing zeros and decimal dot}
exit(komma6Str(s)); {inserts Kommas ',' for each 6 digits}
end;
procedure show_double_interna(d: double);
{shows internal infos about double 'd'}
const K = extended(4503599627370496.0);
var R: TDoubleRec;
ps: string;
mu,ef,v,e2,p: extended;
m: qword;
e: integer;
begin
R:=TDoubleRec(d); {access a 'double' as a record}
m:=R.Mantissa(true); {get Mantissa including Hidden Bit}
mu:=m / K; {converted Mantissa value}
e:=R.Exponent; {decimal Exponent [-1022..+1023] related to 'mu'}
ef:=IntPower(2,e); {factor resulting from Exponent}
if R.Sign then v:=-1 else v:=+1; {Sign}
e2:=mu * ef * v; {must be again the same as 'd'}
p:=ef / K; {current possible precision (resolution) for the range of 'd'}
if p < 0.000001 then str(p,ps) {use exponential format}
else ps:=floatStr(p); {use normal format}
write('Mant=$', hexStr(m,14), {Mantissa including Hidden Bit in Hex}
' Exp=2^', e, {decimal Exponent [-1022..+1023]}
' prec=', ps); {current possible precision for the range of 'd'}
// write(' => ', floatStr(e2)); {must be again the same as 'd'}
end;
procedure test_round(i: int64);
{tests rounding issue for value 'i'}
var x,k,h: double;
i1,i2,i3: int64;
begin
x:=i; {make double}
if x < 0 then k:=-0.5 else k:=0.5;
i1:=round(int(x+k)); {rounding method #1}
i2:=round(math.SimpleRoundTo(x,0)); {rounding method #2}
h:=x+k; i3:=round(int(h)); {rounding method #3 with stored interim result}
write('i=', i64Str(i), ' => i1=', i64Str(i1), ' i2=', i64Str(i2), ' i3=', i64Str(i3));
if i1 <> i then write(' i1=BAD');
if i2 <> i then write(' i2=BAD');
if i3 <> i then write(' i3=BAD');
writeln;
write('':3); show_double_interna(x); writeln;
end;
procedure Test_rounding_issue;
{tests rounding issues for a range of values.
'MaxDoublePrec1' is the highest 'double' value up to which you can store
EACH integer value in a 'double' variable; For higher values only each 2nd
integer value can be stored and so on; For that this loop must be stopped
there}
const MaxDoublePrec1 = 9007199254740992; {2^53 = 9E15}
var i,min,max: int64;
begin
writeln('FPC-Version ', {$I %FPCVERSION%}, ', ',
{$IFDEF CPU32} '32-bit' {$ELSE} '64-bit' {$ENDIF} );
i:=round(IntPower(2,52)); {4503,599627,370496}
// i:=round(IntPower(2,53)); {9007,199254,740992}
min:=i-2;
max:=i+4;
i:=min;
repeat test_round(i);
inc(i);
until (i > max) or (i > MaxDoublePrec1);
end; {Test_rounding_issue}
begin {main}
Test_rounding_issue;
end.
The output is:
FPC-Version 3.2.2, 64-bit
i=4503,599627,370494 => i1=4503,599627,370494 i2=4503,599627,370494 i3=4503,599627,370494
Mant=$1FFFFFFFFFFFFC Exp=2^51 prec=0.5
i=4503,599627,370495 => i1=4503,599627,370495 i2=4503,599627,370495 i3=4503,599627,370495
Mant=$1FFFFFFFFFFFFE Exp=2^51 prec=0.5
i=4503,599627,370496 => i1=4503,599627,370496 i2=4503,599627,370496 i3=4503,599627,370496
Mant=$10000000000000 Exp=2^52 prec=1
i=4503,599627,370497 => i1=4503,599627,370498 i2=4503,599627,370498 i3=4503,599627,370498 i1=BAD i2=BAD i3=BAD
Mant=$10000000000001 Exp=2^52 prec=1
i=4503,599627,370498 => i1=4503,599627,370498 i2=4503,599627,370498 i3=4503,599627,370498
Mant=$10000000000002 Exp=2^52 prec=1
i=4503,599627,370499 => i1=4503,599627,370500 i2=4503,599627,370500 i3=4503,599627,370500 i1=BAD i2=BAD i3=BAD
Mant=$10000000000003 Exp=2^52 prec=1
i=4503,599627,370500 => i1=4503,599627,370500 i2=4503,599627,370500 i3=4503,599627,370500
Mant=$10000000000004 Exp=2^52 prec=1 FPC-Version 3.2.2, 32-bit
i=4503,599627,370494 => i1=4503,599627,370494 i2=4503,599627,370494 i3=4503,599627,370494
Mant=$1FFFFFFFFFFFFC Exp=2^51 prec=0.5
i=4503,599627,370495 => i1=4503,599627,370495 i2=4503,599627,370495 i3=4503,599627,370495
Mant=$1FFFFFFFFFFFFE Exp=2^51 prec=0.5
i=4503,599627,370496 => i1=4503,599627,370496 i2=4503,599627,370496 i3=4503,599627,370496
Mant=$10000000000000 Exp=2^52 prec=1
i=4503,599627,370497 => i1=4503,599627,370497 i2=4503,599627,370497 i3=4503,599627,370498 i3=BAD
Mant=$10000000000001 Exp=2^52 prec=1
i=4503,599627,370498 => i1=4503,599627,370498 i2=4503,599627,370498 i3=4503,599627,370498
Mant=$10000000000002 Exp=2^52 prec=1
i=4503,599627,370499 => i1=4503,599627,370499 i2=4503,599627,370499 i3=4503,599627,370500 i3=BAD
Mant=$10000000000003 Exp=2^52 prec=1
i=4503,599627,370500 => i1=4503,599627,370500 i2=4503,599627,370500 i3=4503,599627,370500
Mant=$10000000000004 Exp=2^52 prec=1You see,
- that both i1=my round_normal() and i2=math.SimpleRoundTo() fail only on 64-bit and only for odd numbers
- that 'i3', which stores 'x+k' as an interim result, fails
always (64- and 32-bit).
Same results with Optimitation Level 1 and Optimitation=off.
I assume I have an explanation, what causes this issue, but I have no explanation, why this only occurs with 64-bit and not with 32-bit.
I tested with FPC 3.2.2 on Linux 64-bit, where this issue occurs (same with FPC 3.2.0). I have no Windows 64-bit.
I tested with FPC 3.2.2 on Linux 32-bit and Windows 32-bit, where this issue
not occurs (same with FPC 3.2.0 and 3.0.4).
My explanation, why this issue
should always (
also with 32-bit) occur, is:
- a 'double' variable has a precision (mantissa) of 52 bits
- as long as a value is < 2^52 = 4503,599627,370496 there are enough bits in the mantissa, that a resolution of 0.5 is storable (displayed in above output as 'prec=...')
- but for values >= 4503,599627,370496 you can store only integers, because there are not enough bits to store a resolution less than 1
- because of that, 4503,599627,370497 + k = 4503,599627,370497.5 is
not storable and must be rounded up to 4503,599627,370498 which is too big by 1.
Question1: why does this occur only with 64-bit and not with 32-bit?
Question2: can please someone, who has Windows 64-bit, run the attached project and report the result (including your FPC version please)?
Question3: has somebody an idea to repair this issue (that all values up to const 'MaxDoublePrec1' = 9007,199254,740992 work correctly)?
Thanks in advance