iStalker

float a = 0.12f;
float b = a * 100f;
Console.WriteLine((int) b); // prints 12, ok
Console.WriteLine((a * 100f).GetType().Name); // prints Single
Console.WriteLine((int)(a * 100f)); // prints 11 !!!!!!!!

Strange thing. Why does the output of last line is 11 instead of 12


Re: Visual C# General Strange math

OmegaMan

Float is only accurate to about fifteen decimal places. Change it to decimal which is used in financial calculations. See my post float issues entitled, Double subtraction...extra value unexpected (.000000..76)





Re: Visual C# General Strange math

iStalker

I know it is. But why we get 2 different result here Why 1-st WriteLine prints 12, not 11
The code seems to be the same in both cases




Re: Visual C# General Strange math

Matthew Watson

That's a very good question!

I can't see any reason for the code to give different answers either. It actually looks like a bug!

For example, this code gives two different answers:



static void Main(string[] args)
{
float a = 0.12f;
float b = a * 100f;
Console.WriteLine((int) b);
Console.WriteLine(f1());
}

static int f1()
{
float a = 0.12f;
float b = a * 100f;
return (int)b;
}



There's DEFINITELY something wrong here...

Also: It gives different answers depending on whether it is compiled in release or debug mode!

I'd say that definitely points towards a compiler bug.





Re: Visual C# General Strange math

Matthew Watson

I have an even better way of showing the compiler bug. Try the following code in release mode:



static void Main(string[] args)
{
float a = 0.12f;
float b = a * 100f;
int c = (int)b;

Console.WriteLine(c);

// In RELEASE MODE:
// If the following WriteLine() is commented out,
// the code prints "11".
// If the line is uncommented,
// the code prints "12" and "12".

Console.WriteLine(b);
}







Re: Visual C# General Strange math


Re: Visual C# General Strange math

Matthew Watson

Hmm. If you look at the code that I posted, the difference occurs IN RELEASE MODE when you comment-out a single Console.WriteLine(). You don't need to compare release mode and debug mode code; the bug manifests itself in release mode only.

I'm sure it's a bug!

[edit] I read the additional link you posted - which I think is confirming that it is a bug.

I have posted it as a bug on the MS feedback site.





Re: Visual C# General Strange math

timvw

From what i understand (now) is that it' not really a bug..

Choosing for more predictable behaviour would require a lot of explicit casts to get rid off the 'additional' precision (which is something the developer can still add himself.. eg: Console.WriteLine( (int) (float) ( a * 100f)); )





Re: Visual C# General Strange math

Matthew Watson

Surely you would agree that if the effect of commenting out a Console.WriteLine() is to change the value printed in an earlier Console.WriteLine()... that is a bug How can it be otherwise





Re: Visual C# General Strange math

timvw

The issue with your little sample is that i can't reproduce the problem (commented out last writeline or not, debug or release, i always get 12...





Re: Visual C# General Strange math

Matthew Watson

I forgot to say I was running on Vista.

I just checked - the problem only occurs if you compile the problem on Vista!
I think that confirms that it is a bug.

To the OP: Are you running on Vista too






Re: Visual C# General Strange math

Thomas Danecker

I think it may have to do with the definitions by the CLI specifications (ecma 335):

The two following points reason your problem:

1) Converting to integers truncates towards zero.

2) The internal representation of floating point numbers on the evaluation stack may be of higher resolution.

That means: If you load a floating point number from a store (your local variable), it may be beneath 12 (e.g. 11.99999...999) and is converted to 11 because it is truncated towards zero. If you do not load it from a store (calculate the value and leave it on the evaluation stack) it may have a higher resolution and be above or exactly 12.

The exact behaviour is dependent on the system (e.g. 32bit/64bit) and on the optimization the compiles (c# compiler/jit compiler) exactly do on the code.

The following excerpt is from ecma 335, partition I, 11.1.3:

The rounding mode defined in IEC 60559:1989 shall be set by the CLI to ˇ°round to the nearest number,ˇ± and neither the CIL nor the class library provide a mechanism for modifying this setting. Conforming implementations of the CLI need not be resilient to external interference with this setting. That is, they need not restore the mode prior to performing floating-point operations, but rather may rely on it having been set as part of their initialization.

For conversion to integers, the default operation supplied by the CIL is ˇ°truncate towards zeroˇ±. There are class libraries supplied to allow floating-point numbers to be converted to integers using any of the other three traditional operations (round to nearest integer, floor (truncate towards ¨Cinfinity), ceiling (truncate towards +infinity)).

Storage locations for floating point numbers (statics, array elements, and fields of classes) are of fixed size. The supported storage sizes are float32 and float64. Everywhere else (on the evaluation stack, as arguments, as return types, and as local variables) floating point numbers are represented using an internal floating-point type. In each such instance, the nominal type of the variable or expression is either R4 or R8, but its value may be represented internally with additional range and/or precision. The size of the internal floating-point representation is implementation-dependent, may vary, and shall have precision at least as great as that of the variable or expression being represented. An implicit widening conversion to the internal representation from float32 or float64 is performed when those types are loaded from storage. The internal representation is typically the native size for the hardware, or as required for efficient implementation of an operation. The internal representation shall have the following characteristics:

ˇ¤ The internal representation shall have precision and range greater than or equal to the nominal type.

ˇ¤ Conversions to and from the internal representation shall preserve value.

Note: This implies that an implicit widening conversion from float32 (or float64) to the internal representation, followed by an explicit conversion from the internal representation to float32 (or float64), will result in a value that is identical to the original float32 (or float64) value.

Rationale: This design allows the CLI to choose a platform-specific high-performance representation for floating point numbers until they are placed in storage locations. For example, it may be able to leave floating point variables in hardware registers that provide more precision than a user has requested. At the same time, CIL generators can force operations to respect language-specific rules for representations through the use of conversion instructions.

When a floating-point value whose internal representation has greater range and/or precision than its nominal type is put in a storage location it is automatically coerced to the type of the storage location. This may involve a loss of precision or the creation of an out-of-range value (NaN, +infinity, or infinity). However, the value may be retained in the internal representation for future use, if it is reloaded from the storage location without having been modified. It is the responsibility of the compiler to ensure that the retained value is still valid at the time of a subsequent load, taking into account the effects of aliasing and other execution threads (see memory model section). This freedom to carry extra precision is not permitted, however, following the execution of an explicit conversion (conv.r4 or conv.r8), at which time the internal representation must be exactly representable in the associated type.

Note: To detect values that cannot be converted to a particular storage type, a conversion instruction (conv.r4, or conv.r8) may be used, followed by a check for a non-finite value using ckfinite. To detect underflow when converting to a particular storage type, a comparison to zero is required before and after the conversion.

Note: The use of an internal representation that is wider than float32 or float64 may cause differences in computational results when a developer makes seemingly unrelated modifications to their code, the result of which may be that a value is spilled from the internal representation (e.g. in a register) to a location on the stack.






Re: Visual C# General Strange math

Thomas Danecker

I did some test on it:

float a = 0.12f;

float b = a * 100f;

double c = a * 100f;

double d = a * 100.0;

int e = (int)(a * 100f);

a * 100f -> 12

b -> 12

(double)b -> 12

(int)b -> 12

c -> 11,9999997317791

(float)c -> 12

(int)c -> 11

d -> 11,9999997317791

(float)d -> 12

(int)d -> 11

e -> 11

(int)(a*100f) -> 11

(int)(a*100.0) -> 11

As you can see: casting a high-res float (float64/double) to int results in 11 because it's representation is 11,9999997317791.

A low-res float (float32) results in 12 because it's representation is also 12.

It's funny that a high-res floating point number is farther away from 12 than a low-res floating point number.






Re: Visual C# General Strange math

Matthew Watson

That's interesting Thomas, and it seems like it must be related.

However, it doesn't explain why the behaviour is different when you compile on Visual Studio with the Windows Vista patch (running on Vista)...




Re: Visual C# General Strange math

Thomas Danecker

I could reproduce the same behaviour in XP SP2. I used this code:

Code Snippet

float a = 0.12f;

float b = a * 100f;

int c = (int)b;

Console.WriteLine(c);

If optimization is enabled it outputs 11, otherwise it outputs 12.

I analysed the assembler code and can tell you where the difference is:

If optimization is disabled, the value is calculated and stored in b (dword). Than it's loaded again (as dword) and stored at the stack as a qword from where it's processed further.

If optimization is enabled, the stackposition of b is reused (I think as parameter or so) and the calculated value is stored there as a qword instead of a dword.

That's the assembler code for the non-optimized version (only the changes which are relevant:

Code Snippet

00000013 fmul dword ptr ds:[00C500F8h]

00000019 fstp dword ptr [esp]

int c = (int)b;

0000001c fld dword ptr [esp]

0000001f fstp qword ptr [esp+10h]

00000023 movsd xmm0,mmword ptr [esp+10h]

00000029 cvttsd2si esi,xmm0

And that's the code for the optimized version:

Code Snippet

00000012 fmul dword ptr ds:[00C500D0h]

00000018 fstp qword ptr [esp+0Ch]

int c = (int)b;

0000001c fld qword ptr [esp+0Ch]

00000020 fstp qword ptr [esp+0Ch]

00000024 movsd xmm0,mmword ptr [esp+0Ch]

0000002a cvttsd2si esi,xmm0