Importance of rounding or bug?

Hi,

I know it is important to round results of calculations with real. I was expecting this specialty with non decimal values like 1/3, etc…

But i was more surprised with values which for me seemed “safe” without rounding. I was wrong (or is it a bug ?)…

ARRAY REAL($tr_real;6)
$tr_real{1}:=8.73
$tr_real{2}:=9.295
$tr_real{3}:=8.435
$tr_real{4}:=8.435
$tr_real{5}:=8.505
$tr_real{6}:=0.15


C_REAL($vr_sum;$vr_expected)
$vr_expected:=43.55  // = 8.73 + 9.295 + 8.435 + 8.435 + 8.505 + 8.505 + 0.15

$vr_sum:=Sum($tr_real)

// to "fix" the problem (for instance) :
//$vr_sum:=Round($vr_sum;13)
// or
//$vr_sum:=Trunc($vr_sum;14)

ASSERT($vr_sum=$vr_expected;"diff is "+String($vr_expected-$vr_sum))
  // v15 $vr_sum#$vr_expected,  but $vr_expected-$vr_sum = -0,000000000000007105427357601
  // v16, v17, v18 $vr_sum=$vr_expected, but $vr_expected-$vr_sum = -0,000000000000007105427357601

$vr_sum:=0
C_LONGINT($i)
For ($i;1;Size of array($tr_real))
$vr_sum:=$vr_sum+$tr_real{$i}
End for 

// to "fix" the problem (for instance) :
//$vr_sum:=Round($vr_sum;13)
// or
//$vr_sum:=Trunc($vr_sum;14)

ASSERT($vr_sum=$vr_expected;"diff is "+String($vr_expected-$vr_sum))

What’s your view on this ? Do you write safe code when dealing with numeric values ?

Perhaps not directly related to your question, but on a related topic:

When we consider rounding in 64-bit 4D, it is important to understand that since v16 (15 R4), the Intel CPU instruction is generated by the compiler, and the behaviour in interpreted mode follows the same rule.

Nothing has changed for the Round function, it continues to perform a tie-break

  • toward positive infinity for a positive number
  • toward negative infinity for a negative number

However, the CPU does a " Round half to even", a tie-breaking rule without positive/negative bias and without bias toward/away from zero, a variant of the round-to-nearest method.

It is also called convergent rounding, statistician’s rounding, Dutch rounding, Gaussian rounding, odd–even rounding, or bankers’ rounding.

I bring this up, because the change happened in v16 and applies to 64-bit only, which matches your description.

Hi Bruno,

Numbers that appear safe when represented in base-10 aren’t safe when represented in base-2. Because the base-2 number can’t be represented accurately, there is a small rounding error which propagates when you add them together. The result is $vr_expected-$vr_sum = -0,000000000000007105427357601

See here for discussion on this topic.

In 4D, by default any difference between two real numbers that is less than 10^-6 is considered to be equal. You can change the comparison level using SET REAL COMPARISON LEVEL. This is why your ASSERT is passing. I’m surprised that it fails for you in v15 though. Perhaps the change noted by Keisuke is the cause there.

If you needed to work with very small magnitudes, or you found that the small errors propagated enough to cause an issue, then you would need to handle them. In our application we don’t specifically do anything except when displaying real values.

Adam

1 Like

Miyako, Adam :slight_smile:

Thanks for your replies

I missed (or forgot) the SET REAL COMPARISON LEVEL command :roll_eyes:

In the v15 database (written by somebody else), I searched and I found this line :roll_eyes:
SET REAL COMPARISON LEVEL(10^-15)
Default in 4D is 10^-6 (just read the documentation).

I did the tests with 4D v15, with precision set at 10^-15 (at startup)
And I did the tests in v16, v17, v18 in different application (not using SET REAL COMPARISON LEVEL).
So that explains the difference… Sorry about that.

@miyako, the problem occurs without rounding.

So I simplified the code :roll_eyes:

C_REAL($vr_sum;$vr_expected)
$vr_expected:=43.55
$vr_sum:=8.73+9.295+8.435+8.435+8.505+0.15

  // the equality is FALSE because precision on "=" is 10^-15, but $vr_expected-$vr_sum = -0,000000000000007105427357601
SET REAL COMPARISON LEVEL(10^-15)
ASSERT($vr_sum=$vr_expected;"diff is "+String($vr_expected-$vr_sum))

  // the equality is TRUE because precision on "=" is 10^-14, but $vr_expected-$vr_sum = -0,000000000000007105427357601
SET REAL COMPARISON LEVEL(10^-14)
ASSERT($vr_sum=$vr_expected;"diff is "+String($vr_expected-$vr_sum))

I know I can change SET REAL COMPARISON LEVEL in the application level to get the comparison (=) to behave like “almost equal”. And this works in code, not in search (from what the documentation says).

Documentation (v18 says)

  • Real: A number to ±1.7e±308 (13 significant digits)

But what surprised me is that adding 6 numbers with about 3 decimals, with a result of 4 signifiant digits (les than 13 described in the documentation), I get a result which is not exactly what I expected. And the debugger displays a rounded figure, because the result of the sum was not exactly 43.55 (and it is apparent when we do a substraction).

I did not expect that, in this case, that I needed an explicit call to round was needed to get the number I was expecting.

@Adam, thanks for the link.

And a simpler example :

C_REAL($vr_sum;$vr_sumRounded;$vr_expected;$vr_diff;$vr_diffRounded)
$vr_sum:=0.1+0.2  // displays 0.3 in the debugger but it is not really 0.3 but something like 0.300000000000000005551115123126 
$vr_sumRounded:=Round($vr_sum;12)
$vr_expected:=0.3
$vr_diff:=$vr_expected-$vr_sum  // -5,551115123126e-17 not 0
$vr_diffRounded:=$vr_expected-$vr_sumRounded  // 0
ASSERT($vr_sum=$vr_expected;"diff is "+String($vr_expected-$vr_sum))

I guess we have to live with it and use Round generously after any calculation on real and before storing results…

Spend a minute looking in the outside world, out of 4D…

Use Chrome and open Chrome debugger.

Enter:
(0.1+0.2)+0.3 == 0.1+(0.2+0.3)

surprised?

So test both:
0.1+(0.2+0.3)
and
(0.1+0.2)+0.3

oops?

You might also want to check:
0.2 + 0.4

Easy numbers, isn’t it?

Try the same with 4D.

Mpfh, how to try? How to know what is really in the real and what is displayed? You could store the real in a blob and check bits, but’s that’s no fun, so most convert to string or display in debugger.
To display, the number is converted to a string and the conversion to string - yes, that changes the number…

Here insight from our development department:
So here’s how 4D stringifies a number:

  • if the number is lower than 1 and greater than 10^-16, the number is multiplied by 10^(13+abs(log10(number))) and converted to a 64bit signed integer. Then the integer is converted to a string and the decimal separator inserted at the proper place. This way we avoid results such as “0.0999999999999999” instead of “0.1”.

  • in other cases, 4D is calling the printf api from the C standard library, using the “%.g” format and a precision of 14 for negative values and 13 for positive values.
    https://docs.microsoft.com/fr-fr/cpp/c-runtime-library/format-specification-syntax-printf-and-wprintf-functions

Too difficult?
But still remind: this is only to display, it does not change the stored number…

ok, easy answer: do a round whenever you do a calculation with reals representing a currency or similar value. Any calculation. Don’t do it for science values, where you need “full” precision. Ok, “full” is now another discussion

2 Likes

Thanks for your reply Thomas.

I realize it is a general computer problem (that we developer have to deal with).

In my case, I deal with weight in Kg, so having more that 3 digits of precisions does not make sense. I will make sure Round is called after calculations. :grinning: I tend to do it but this is “inherited” code…

When the number of decimals must be under “control”, I often use trigger to round (low level…)

BTW i’d like an option in 4D real type field properties to do that for me, something like “keep N decimals max” would be perfect. In other databases they have “money”, “currency”, etc. type fields. Such a property would be a simple way to mimic these containers in 4D and avoid “bad” values.

1 Like