5

This is a followup to this question: Why does a division result differ based on the cast type?

Quick Summary:

byte b1 = (byte)(64 / 0.8f); // b1 is 79
int b2 = (int)(64 / 0.8f); // b2 is 79
float fl = (64 / 0.8f); // fl is 80

The question is: Why are the results different depending on the cast type? While working out an answer I ran into an issue I wasn't able to explain.

var bytes = BitConverter.GetBytes(64 / 0.8f).Reverse(); // Reverse endianness
var bits = bytes.Select(b => Convert.ToString(b, 2).PadLeft(8, '0'));
Console.WriteLine(string.Join(" ", bits));

This outputs the following:

01000010 10100000 00000000 00000000

Breaking it down in IEEE 754 format:

0 10000101 01000000000000000000000

Sign:

0 => Positive

Exponent:

10000101 => 133 in base 10

Mantissa:

01000000000000000000000 => 0*2^-1 + 1*2^-2 + 0*2^-3 ... = 1/4 = 0.25

Decimal Representation:

(1 + 0.25) * 2^(133 - 127) (Subtract single precision bias)

This results in exactly 80. So why does casting the result make a difference?

Community
  • 1
  • 1
ConditionRacer
  • 4,418
  • 6
  • 45
  • 67

3 Answers3

4

My answer in the other thread is not entirely correct: Actually, when computed at runtime, (byte)(64 / 0.8f) is 80.

When casting a float containing the result of 64 / 0.8f, to byte at runtime, the result actually is 80. However, this is not the case when the cast is done as a part of the assignment:

float f1 = (64 / 0.8f);

byte b1 = (byte) f1;
byte b2 = (byte)(64 / 0.8f);

Console.WriteLine(b1); //80
Console.WriteLine(b2); //79

While b1 contains the expected result, b2 is off. According to the disassembly, b2 is assigned as following:

mov         dword ptr [ebp-48h],4Fh 

Thus, the compiler seems to calculate a different result from the result at runtime. I don't know, however, if this is the expected behavior or not.

EDIT: Maybe it is the effect Pascal Cuoq described: During compile time, the C# compiler uses double to calculate the expression. This results in 79,xxx which is truncated to 79 (as a double contains enough precision to cause an issue, here).
Using float, however, we don't actually run into an issue, as the floating-point "error" happens not within the range of a float.

During runtime, this one also prints 79:

double d1 = (64 / 0.8f);
byte b3 = (byte) d1;
Console.WriteLine(b3); //79

EDIT2: As of request of Pascal Cuoq, I ran the following code:

int sixtyfour = Int32.Parse("64");
byte b4 = (byte)(sixtyfour / 0.8f);
Console.WriteLine(b4); //79

Result is 79. So the above statement that the compiler and the runtime calculate a different result is not true.

EDIT3: When changing the previous code to (credits to Pascal Cuoq, again), the result is 80:

byte b5 = (byte)(float)(sixtyfour / 0.8f);
Console.WriteLine(b5); //80

Note, however, that this is not the case when writing (results in 79):

byte b6 = (byte)(float)(64 / 0.8f);
Console.WriteLine(b6); //79

So here is what seems to be happening: (byte)(64 / 0.8f) is not evaluated as a float, but evaluated as a double (before casting it to byte). This results in a rounding error (which does not occur when the calculation is done using float). An explicit cast to float before casting to double (which is marked as redundant by ReSharper, BTW) "solves" this issue. However, when the calculation is done during compile time (possible when using constants only), the explicit cast to float seems to be ignored / optimized away.

TLDR: Floating point calculations are even more complicated than they initially seem.

Matthias
  • 12,053
  • 4
  • 49
  • 91
  • 1
    I do not know the exact definition used by C#, but in other languages that allow extra precision for intermediate floating-point computations, such as C, any assignment has to round to the precision of the type. If C# works the same, then `float f1 = (64 / 0.8f); byte b1=(byte)f1;` has to assign `f1` with the rounded results of the division, and needs not produce the same result as `byte b2 = (byte)(64 / 0.8f);`. The way to check is to have the 64 be an input of the program, so that neither is optimized. – Pascal Cuoq Sep 06 '14 at 20:11
  • It looks like you a right: I just did that - the result is 79. I am now a bit confused, however, what is actually happening. – Matthias Sep 06 '14 at 20:23
  • Wow, thanks for investigating this. In C99 it would be forbidden to eliminate the explicit cast as an “optimization”, since the cast changes the result (by forcing to convert from the working precision to the precision of the type). – Pascal Cuoq Sep 06 '14 at 20:49
  • "Fun" fact: The MS-CLR-C++ Compiler (VS 2013) produces different results when compiling translated code (I am no C++-programmer, however - I just tried it by changing byte to int): Only "int b4 = (int)(sixtyfour / 0.8f)" results in 79, everything else (including the calculations involving the constants) result in 80. It might be interesting, how the Mono compilers treat this scenario and whether using NGEN-optimizations makes any difference. – Matthias Sep 06 '14 at 21:07
3

The C# language specification allows to compute intermediate floating-point results at a precision greater than that of the type. This is very likely what is happening here.

While 64 / 0.8 computed to higher precision is slightly lower than 80 (because 0.8cannot be represented exactly in binary floating-point), and converts to 79 when truncated to an integer type, if the result of the division is converted to float, it is rounded to 80.0f.

(Conversions from floating-point to floating-point are to the nearest—technically, they are done according to the rounding mode of the FPU, but C# does not allow changing the rounding mode of the FPU from its “to the nearest” default. Conversions from floating-point to integer types truncate.)

Pascal Cuoq
  • 79,187
  • 7
  • 161
  • 281
  • 1
    @winSharp93 Arg! I was just guessing, though. Better remove that part of the answer. – Pascal Cuoq Sep 06 '14 at 19:54
  • It seems you are right, but in a different way (see my edit): As "(byte)(64 / 0.8)" is a constant, the compiler evaluates it during compile time. This optimization, however, is done using double instead of float for calculations. – Matthias Sep 06 '14 at 20:08
0

Even though C# follows Java's (IMHO unfortunate) lead in requiring an explicit cast any time something which is specified as double is stored to a float, code generated by the C# compiler allows the .NET Runtime to perform calculations as double and use those double values in many contexts where the type of an expression should, according to the language rules, be float.

Fortunately, the C# compiler does offer at least one way to ensure that things which are supposed to be rounded to the nearest representable float actually are: cast them explicitly to float.

If you write your expression as (byte)(float)(sixtyFour / 0.8f), that should force the result to get rounded to the nearest representable float value before truncating the fractional part. Although the cast to float may appear redundant (the compile-time type of the expression already is float), the cast will turn "thing which is supposed to be float but is really double" into something which is actually a float.

Historically, some languages would specify that all floating-point operations are performed on type double; float existed not to speed computations, but to reduce storage requirements. There was generally no need to specify constants as type float, since dividing by 0.800000000000000044 (the double value 0.8) was no slower than dividing by 0.800000011920929 (the value 0.8f). C# somewhat annoyingly won't allow float1 = float2 / 0.8; because of "loss of precision" but instead favors the less-precise float1 = float2 / 0.8f; and doesn't even mind the likely-erroneous double1 = float1 / 0.8f;. The fact that operations are performed between float values doesn't mean the result will actually be a float, though--it merely means the compiler will allow it to be silently rounded to a float in some contexts but won't force it in others.

supercat
  • 77,689
  • 9
  • 166
  • 211