Will the scope of floating point variables affect their values?

If we execute the following C# code on a console application, we will get a message as The sums are Not equal.

If we execute it after uncommenting the line System.Console.WriteLine(), we will get a message as The sums are equal.

    static void Main(string[] args)
    {
        float f = Sum(0.1f, 0.2f);
        float g = Sum(0.1f, 0.2f);

        //System.Console.WriteLine("f = " + f + " and g = " + g);

        if (f == g)
        {
            System.Console.WriteLine("The sums are equal");
        }
        else
        {
            System.Console.WriteLine("The sums are Not equal");
        }
    }

    static float Sum(float a, float b)
    {
        System.Console.WriteLine(a + b);
        return a + b;
    }

What is the actual reason for this behavior?


Solution 1:

It's not related to scope. It's the combination of the stack dynamics and floating point handling. Some knowledge of compilers will help make this counterintuitive behavior clear.

When the Console.WriteLine is commented, the values f and g are on the evaluation stack and stay there until after you've passed the equality test in your Main method.

When Console.Writeline is not commented, the values f and g are moved from the evaluation stack to the call stack at the moment of the invocation, to be restored to the evaluation stack when Console.WriteLine returns. And your comparison if (f == g) is done afterwards. Some rounding can occur during this storing of values to the call stack and some information can be lost.

In the scenario where you do invoke Console.WriteLine, the f and the g in the comparison test are not the same values. They've been copied and restored to a format that has different rules on precision and rounding, by the virtual machine.

In your particular code, when the invocation of Console.WriteLine is commented, the evaluation stack is never stored to the call stack and no rounding occurs. Because it is permitted for implementations of the platform to provide improved precision on the evaluation stack, this discrepancy can arise.

EDIT What we're hitting in this case is allowed by the CLI specification. In section I.12.1.3 it reads:

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.

The keywords from this quote are "implementation-dependent" and "can vary". In the OP's case, we see his implementation does indeed vary.

Non-strictfp floating point arithmetic in the Java platform also has a related issue, for more info check also my answer to Will floating point operations on the JVM give the same results on all platforms?

Solution 2:

What is the actual reason for this behaviour?

I can't provide details for exactly what's going on in this specific case, but I understand the general problem, and why using Console.WriteLine can change things.

As we saw in your previous post, sometimes operations are performed on floating point types at a higher precision than the one specified in the variable type. For local variables, that can include how the value is stored in memory during the execution of a method.

I suspect that in your case:

  • the Sum method is being inlined (but see later)
  • the sum itself is being performed with greater precision than the 32-bit float you'd expect
  • the value of one of the variables (f say) is being stored in a high-precision register
    • for this variable, the "more precise" result is being stored directly
  • the value of the other variable (g) is being stored on the stack as a 32-bit value
    • for this variable, the "more precise" result is being reduced to 32 bits
  • when the comparison is performed, the variable on the stack is being promoted to a higher-precision value and compared with the other higher-precision value, and the difference is due to one of them having previously lost information and the other not

When you uncomment the Console.WriteLine statement, I'm guessing that (for whatever reason) forces both variables to be stored in their "proper" 32-bit precision, so they're both being treated the same way.

This hypothesis is all somewhat messed up by the fact that adding

[MethodImpl(MethodImplOptions.NoInlining)]

... does not change the result as far as I can see. I may be doing something else wrong along those lines though.

Really, we should look at the assembly code which is executing - I don't have the time to do that now, unfortunately.

Solution 3:

(Not a real answer but hopefully some supporting documentation)

Configuration: Core i7, Windows 8.1, Visual Studio 2013

Platform x86:

Version      Optimized Code?        Debugger Enabled?          Outcome
4.5.1        Yes                    No                         Not equal
4.5.1        Yes                    Yes                        Equal
4.5.1        No                     No                         Equal
4.5.1        No                     Yes                        Equal
2.0          Yes                    No                         Not Equal
2.0          Yes                    Yes                        Equal
2.0          No                     No                         Equal
2.0          No                     Yes                        Equal

Platform x64:

Version      Optimized Code?        Debugger Enabled?          Outcome
4.5.1        Yes                    No                         Equal
4.5.1        Yes                    Yes                        Equal
4.5.1        No                     No                         Equal
4.5.1        No                     Yes                        Equal
2.0          Yes                    No                         Equal
2.0          Yes                    Yes                        Equal
2.0          No                     No                         Equal
2.0          No                     Yes                        Equal

The situation only seems to occur with optimized code on x86 configurations.