12

I've noticed something very odd when working with addition of nullable floats. Take the following code:

float? a = 2.1f;
float? b = 3.8f;
float? c = 0.2f;
float? result = 
(a == null ? 0 : a)
+ (b == null ? 0 : b)
+ (c == null ? 0 : c);
float? result2 = 
(a == null ? 0 : a.Value)
+ (b == null ? 0 : b.Value)
+ (c == null ? 0 : c.Value);

result is 6.099999 whereas result2 is 6.1. I'm lucky to have stumbled on this at all because if I change the values for a, b, and c the behavior typically appears correct. This may also happen with other arithmetic operators or other nullable value types, but this is case I've been able to reproduce. What I don't understand is why the implicit cast to float from float? didn't work correctly in the first case. I could perhaps understand if it tried to get an int value given that the other side of the conditional is 0, but that doesn't appear to be what's happening. Given that result only appears incorrect for certain combinations of floating values, I'm assuming this is some kind of rounding problem with multiple conversions (possibly due to boxing/unboxing or something).

Any ideas?

daveaglick
  • 3,600
  • 31
  • 45
  • Just so that you are aware, [floats aren't exact values in computers](http://floating-point-gui.de/) – default Aug 28 '14 at 14:54
  • http://stackoverflow.com/questions/6683059/are-floating-point-numbers-consistent-in-c-can-they-be – Donal Aug 28 '14 at 14:54
  • 1
    I'm guessing the difference is "values in registers" vs "values on the stack"; registers are **much wider** than the same value on the stack (in this case, 80 bits instead of 32). If the compiler can do everything native on the register, it can retain more precision. If it compiles it to use the stack: much less. But: you should always expect rounding with floating points. – Marc Gravell Aug 28 '14 at 14:55
  • To put what Default said into context, 1/10 (and thus 6 1/10) is periodic in binary (like 1/3 is in decimal) and thus that specific number cannot be stored exactly as a floating point number. The problem surely relates to this, though I don't know enough about C# to help you further. – ikegami Aug 28 '14 at 14:55
  • 1
    Interestingly `a + b + c` and `a.Value + b.Value + c.Value` returns same value, so the check against null has something to do with it ? – Habib Aug 28 '14 at 15:06
  • 1
    Yeah, I get the whole "floats aren't exact" thing - what confuses me is why using `.Value` instead of the implicit conversion would change the result. – daveaglick Aug 28 '14 at 15:14
  • 1
    if you do `a + (b + c);` you also get `6.1`. So it looks to do something with in which order they are added to each other. – default Aug 28 '14 at 15:22
  • 8
    @somedave: **ANYTHING** is permitted to change the result -- let me emphasize that again **ANYTHING WHATSOEVER** including phase of the moon is permitted to change whether floats are computed in 32 bit accuracy or higher accuracy. The processor is *always* allowed to *for any reason whatsoever* decide to suddenly start doing floating point arithmetic in 80 bits or 128 bits or whatever it chooses so long as it is more than or equal to 32 bit precision. See http://stackoverflow.com/questions/15117037/1f-2f-3f-1f-2f-equals-3f-why/15117741#15117741 for more details. – Eric Lippert Aug 28 '14 at 15:34
  • 1
    Asking what in particular in this case caused the processor to decide to use higher precision in one case and not in another is a losing game. It could be **anything**. If you require accurate computations **in decimal figures** then use the aptly named `decimal` type. If you require repeatable computations in floats then C# has two mechanisms for forcing the processor back to 32 bits. (1) explicitly cast to `(float)` unnecessarily, or (2) store the result in a float array element or float field of a *reference type*. – Eric Lippert Aug 28 '14 at 15:36
  • @EricLippert - Thanks! That makes sense. In this case I had incorrectly assumed that the Nullable representation has something to do with the inconsistency, but that's obviously a red herring. It's still interesting to me that it does appear to be deterministic depending on which sequence of code I write - but I guess that might only be true for my own system with it's own way of figuring these things out... – daveaglick Aug 28 '14 at 15:43
  • 3
    @somedave: You're welcome. I note that "being deterministic on a particular machine" is a subset of "anything whatsoever", and therefore is permitted. I would not rely upon it; "appearing deterministic for a while and then suddenly changing unpredictably" is also a subset of "anything whatsoever" and hard to distinguish from the former. – Eric Lippert Aug 28 '14 at 15:47
  • @EricLippert From the spec on binary operations on floating point numbers, `the operation is performed using at least float range and precision, and the type of the result is float`. Does that not apply to the intermediate result of `a + b` in `a + b + c`? – Rawling Aug 29 '14 at 07:51
  • @Rawling: The spec also says "Floating-point operations may be performed with higher precision than the result type of the operation" and "C# allows a higher precision type to be used for all floating-point operations". Is any of that unclear? I am not understanding what your question is. – Eric Lippert Aug 29 '14 at 15:24
  • @EricLippert So is `a + b + c` a single floating-point operation? Because otherwise, sure, the spec says you can *perform* the `a + b` operation at a higher precision, but it also says the *result* should be a `float`, not "a `float` unless it is immediately fed into a second operation, in which case it might be a `double`". – Rawling Aug 29 '14 at 16:30
  • 1
    @Rawling: You are making precisely the unwarranted assumption that the spec takes pains to warn against. Your assumption is "if the type of the expression is `float` then the storage location associated with the value of that expression is exactly a 32 bit IEEE float". **That assumption is false.** If the type of an expression is `float` then the storage location associated with the value of that expression is *at least as precise as an IEEE 32 bit float*. It is permitted to be of any size whatsoever so long as that minimum bar is met. – Eric Lippert Aug 29 '14 at 16:35
  • 1
    @Rawling: This is true of **any** expression of type `float` in C#. The guarantee that a static field of type `float`, a field of a reference type of type `float`, or an element of a `float` array are *exactly* 32 bits is a feature of the CLR, not a requirement of the C# language. **A conforming implementation of the C# language would be allowed to silently replace all floats with doubles**. – Eric Lippert Aug 29 '14 at 16:38
  • @Rawling: More generally, stop thinking that a *type* has anything whatsoever to do with the *storage implementation details* of data. A type is a mathematical object that is logically associated with an expression by the compiler for the purpose of *type analysis* to find *type errors*. It is not a constraint on how code may be generated. – Eric Lippert Aug 29 '14 at 16:40
  • @EricLippert (Replied too soon, deleted.) The spec says the float type is "represented using the 32-bit single-precision... IEEE 754 format" - how does that allow a class's `float` field to be stored in some other, wider format? And then I'm further confused, because the spec says "Floating-point operations may be performed with higher precision than the result type of the operation" but gives `x * y / z` as an example - is this not *two* operations? – Rawling Aug 29 '14 at 17:43
  • @Rawling: The bit of the spec that allows a float to be stored in some other format has been quoted many times in this thread. In the example given both the multiplication and the division may be performed in arbitrary precision greater than or equal to 32 bit precision. – Eric Lippert Aug 29 '14 at 17:59
  • @Rawling: To further clarify: It is specifically the case that local variables of float type and the storage for temporary values of float type may be arbitrarily large, and still considered by the compiler to be "of type float". Truncating such expressions back to 32 bits decreases both accuracy and performance, though it does increase the determinism of the result. The Intel chip designers decided that accuracy and performance was more important than determinism; if that bugs you, take it up with Intel. – Eric Lippert Aug 29 '14 at 18:03
  • @EricLippert The only bits of the spec I've seen quoted in here are about operations, not storage. If wider types can also be used for storage of temporary and local variables on the whim of the processor, maybe the spec should say so; I don't think "storage" falls under "operation". You just said a `float` field of a reference type could be stored with more precision, but further up suggested storing the result of an operation in a `float` field of a reference type as a way to force it to only 32 bits - maybe the spec could be clearer on that too. – Rawling Aug 29 '14 at 18:14
  • @rawling the clr promises truncation on storage. That is not required by the specification of the language. Do you actually misunderstand the allowed semantics, or are you just nitpicking the spec wording? If the former, consider posting a new question. If the latter, the Roslyn forum is the appropriate venue. – Eric Lippert Aug 29 '14 at 19:13
  • @EricLippert The C# specification says the float type is "represented using the 32-bit single-precision... IEEE 754 format". If it's stored in a wider format then it's not being represented by that format. – Rawling Aug 29 '14 at 19:16

1 Answers1

4

See comments by @EricLippert.

ANYTHING is permitted to change the result -- let me emphasize that again ANYTHING WHATSOEVER including phase of the moon is permitted to change whether floats are computed in 32 bit accuracy or higher accuracy. The processor is always allowed to for any reason whatsoever decide to suddenly start doing floating point arithmetic in 80 bits or 128 bits or whatever it chooses so long as it is more than or equal to 32 bit precision. See (.1f+.2f==.3f) != (.1f+.2f).Equals(.3f) Why? for more details.

Asking what in particular in this case caused the processor to decide to use higher precision in one case and not in another is a losing game. It could be anything. If you require accurate computations in decimal figures then use the aptly named decimal type. If you require repeatable computations in floats then C# has two mechanisms for forcing the processor back to 32 bits. (1) explicitly cast to (float) unnecessarily, or (2) store the result in a float array element or float field of a reference type.

The behavior here has nothing to do with the Nullable type. It's a matter of floats never being exact and being calculated in different precision on the whims of the processor.

In general, this comes down to the advice that if accuracy is important, your best bet is to use something other than float (or use the techniques described by @EricLippert to force the processor to use 32 bit precision).

The answer from Eric Lippert on linked question is also helpful in understanding what's going on.

Community
  • 1
  • 1
daveaglick
  • 3,600
  • 31
  • 45