1
votes

I am seeing some strange behaviour when rounding floating point numbers in my Ada code.

I understand that floats are not reliable, and that sometimes they aren't represented exactly.

My question is around why it would matter if you store the value in a variable first.

float'rounding(8.9 / 0.2)

the above comes out as 45, which seems to make sense, 8.9 / 0.2 = 44.5 then round away from zero. (I also tried casting each number to a float to make sure there wasnt some type difference)

but then

a : float : 8.9;
b : float : 0.2;
float'rounding(a / b)

this time it comes out as 44. which means that a / b = 44.4999999 then round down.

Why does saving the values in a variable change the outcome of the calculation when its all the same types?!

Any insight would be amazing!

2
At least in C, 8.9, 0.2, 8.9 / 0.2 are double precision and a / b is single and so slightly different quotients,. Not sure if that applies to Ada. - chux - Reinstate Monica
@chux Yeah I thought it could be something like that. which is why I tried float(8.9) / float(0.2) and still got 44.5 - The Iron Duckling
@TheIronDuckling I don't know ADA but in C++ (and in C with a similar cast) float(8.9)/float(0.2) involves double rounding: 8.9 to double precision then single, so the result might be different from 8.9f/0.2f where the literals are rounded to single precision directly - phuclv

2 Answers

3
votes

Ada real literals are of the type universal_real and may be used in calculations involving an real type, such as float or long_float as long as the literal value is within the range of valid values for the type.

Universal_real is implemented as the "largest" type representable by the computer floating point hardware. This is similar to the C language where real literals are double precision.

See the following example:

with Ada.Text_IO; use Ada.Text_IO;

procedure Main is
   a_val : constant := 8.9;
   b_val : constant := 0.2;
   a     : float := a_val;
   b     : float := b_val;
   a_l   : long_float := a_val;
   b_l   : long_float := b_val;
begin
   Put_Line(Float'Image(Float'Rounding(a_val / b_val)));
   Put_Line(Float'Image(Float'Rounding(a / b)));
   Put_Line(Long_Float'Image(Long_Float'Rounding(a_l / b_l)));
end Main;

The named numbers a_val and b_val are of the type universal_real. The output of the program is:

 4.50000E+01
 4.40000E+01
 4.50000000000000E+01

The division when directly using the name numbers is identical to the division when using long_float, and different than the division using float.

1
votes

I don't know Ada, but most of those numbers can't be represented accurately as (binary) floating point numbers. Jeffrey pointed out that the difference is due to your calculation being performed exactly at compile time, but thought it might be instructive to show why the runtime version gives the "wrong" answer.

Using binary32 representation, i.e. float variables in Ada, we get:

input       nearest representable float
8.9       =  8.8999996185302734375
0.2       =  0.20000000298023223876953125
8.9 / 0.2 = 44.499996185302734375

rounding the result works as expected mathematically, but due to the previous loss of precision you end up with 44.

For comparison, with binary64 floats, i.e. long_float in Ada, it happens to go the other way:

8.9       =  8.9000000000000003552713678800500929355621337890625
0.2       =  0.200000000000000011102230246251565404236316680908203125
8.9 / 0.2 = 44.5

the errors happened to cancel during division hence the exact result of 44.5, the rounding of this goes the expected way.

I tend to use the Python REPL for quickly checking these interactively, the decimal module is especially useful for seeing the expansion of floats. Python only knows about binary64 floats natively, but the NumPy library provides access to other floating-point types.