In this question, the discussion is on possible discrepancies between Debug and Release mode when dealing with floating-point operations. In particular, in this answer we see that a seemingly innocent operation,
Single f1 = 0.00000000002f;
Single f2 = 1 / f1;
Double d = f2;
Console.WriteLine(d);
can give rise to different results in x86 Debug and Release mode (where it prints 49999998976 and 50000000199,7901) respectively. From the point of view of the assembly, this happens because the store-then-load pair
fstp dword ptr [ebp-44h]
fld dword ptr [ebp-44h]
has been optimized away in Release mode, where in Debug mode, taking the value from the 80-bit x87 register, moving it to a dword and back again, is enough to kill off precision.
Now, what I'm wondering is if this is in fact in accordance with specification. The C# specification states that
Floating-point operations may be performed with higher precision than the result type of the operation. For example, some hardware architectures support an "extended" or "long double" floating-point type with greater range and precision than the double type, and implicitly perform all floating-point operations using this higher precision type. Only at excessive cost in performance can such hardware architectures be made to perform floating-point operations with less precision, and rather than require an implementation to forfeit both performance and precision, C# allows a higher precision type to be used for all floating-point operations.
However, as I read it, this applies only to individual operations and since in our case we are very clear about how the result of the division should be stored in a Single
and only then converted to a Double
, given only this piece of specification I would expect the resulting Double
to always be representable as a single-precision floating-point. On the other hand, as also spelled out in the accepted answer to the other question, the runtime specification goes on to state that
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 float32or float64, but its value can be represented internally with additional range and/or precision. The size of the internal floating-point representation is implementation-dependent, can vary, and shall have precision at least as great as that of the variable or expression being represented.
So my question becomes whether I'm reading and understanding this correctly:
Is the above discrepancy between Debug and Release mode due to the fact that since we are dealing with local variables, Single
should in fact always be read as "Single
or more precise"? And that in particular, since if we had used only Double
throughout we would be getting a different result, the compiler is allowed to optimize as if all locals were doubles?
I realize that this can be answered with a simple "yes" or "no", but any further insights would be appreciated as well.
Notably, if we change all of f1
, f2
, and d
to be fields of a class, Debug and Release mode results agree, which would support the above interpretation.