Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unrelated code changes results of calculation

We have some code that is giving unexpected results on some machines. I've narrowed it down to a simple example. In the linqpad snippet below, the methods GetVal and GetVal2 have essentially the same implementation, although the former also includes a check for NaN. However, the results returned by each are different (at least on my machine).

void Main()
{
    var x = Double.MinValue;
    var y = Double.MaxValue;
    var diff = y/10 - x/10;

    Console.WriteLine(GetVal(x,6,diff));
    Console.WriteLine(GetVal2(x,6,diff));
}

public static double GetVal(double start, int numSteps, double step)
{
    var res = start + numSteps * step;
    if (res == Double.NaN)
        throw new InvalidOperationException();
    return res;
}

public static double GetVal2(double start, int numSteps, double step)
{
    return start + numSteps * step;
}

Results

3.59538626972463E+307
Infinity

Why does this happen, and is there a simple way of avoiding it? Something to do with registers?

like image 572
Rob Avatar asked Nov 08 '22 16:11

Rob


1 Answers

You didn't specify the environment and compiling options but I've been able to reproduce your issue in .NET Standard 4.8 in 32-bit Release mode where the output is

3,59538626972463E+307
∞

Note: the radix point and infinity representation depends on locale

In the 32-bit Debug mode the result is like this

3,59538626972463E+307
3,59538626972463E+307

The result is consistently like below in 64-bit mode

∞
∞

Demo:

  • dotnetfiddle.net: Prints out two 3.59538626972463E+307s
  • sharplabs.io: Prints out two 3.59538626972463E+307s in both 32-bit Debug and Release mode, probably because they use a different .NET framework version. If change to "Default" then it prints out two s, and in 64-bit mode it prints out two Infinitys

It's because in 32-bit .NET Standard floating-point operations are done with x87 instructions in the 80-bit extended floating-point format. numSteps * step doesn't fit in a double and results in infinity, but in extended precision it doesn't overflow and the final result fits in a double

But why sometimes it's 3.59538626972463E+307 and sometimes it's infinity? That's because when calculating an expression sometimes the intermediate values must be spilled to memory to free up some registers, which casts the value down to double precision. Therefore the same expression may produce different results. We don't know when the compiler spills to memory so we can't predict the output. Another thing that affects the result is compiler optimization: here GetVal2 is pre-calculated by the compiler which does everything in double precision. You can easily see that the compiler just loads the constant result from memory instead of calling GetVal2 in the disassembly below§

The phenomenon never happens in 64-bit mode in .NET Standard, either Debug or Release because math in 64-bit is always done in SSE registers. It's also not reproducible in .NET Core either because .NET Core also use SSE registers to do floating-point math even in 32-bit. You can easily check that by debugging in VS and view the disassembly. It make sense because SSE is faster and makes more deterministic results, and almost all modern computers in the last 2 decades support SSE

To avoid that non-deterministic feature the easiest way is to avoid 32-bit x86 completely, or move to .NET Core where determinism is more guaranteed. Otherwise you'll have to use some 3rd party libraries. See

  • Deterministic floating point and .NET
  • Coercing floating-point to be deterministic in .NET?
  • Floating point determinism for gamedev in .NET Core
  • Is SSE floating-point arithmetic reproducible?
  • Is floating point arithmetic stable?
  • Is floating-point math consistent in C#? Can it be?
  • How deterministic is floating point inaccuracy?

There are lots of similar issues:

  • C# - Inconsistent math operation result on 32-bit and 64-bit
  • Floating point inconsistency between expression and assigned object
  • CLR JIT optimizations violates causality?
  • Casting a result to float in method returning float changes result
  • Why does Math.Exp give different results between 32-bit and 64-bit, with same input, same hardware
  • Is SSE floating-point arithmetic reproducible?
  • Why does this floating-point calculation give different results on different machines?
  • C# XNA Visual Studio: Difference between "release" and "debug" modes?
  • C# rounding differently depending on platform?

It's the same issue in C and C++ when FLT_EVAL_METHOD > 1.

  • Does any floating point-intensive code produce bit-exact results in any x86-based architecture?
  • Is C floating-point non-deterministic?
  • Difference in floating point arithmetics between x86 and x64
  • different behaviour or sqrt when compiled with 64 or 32 bits
  • std::pow produce different result in 32 bit and 64 bit application
  • acos(double) gives different result on x64 and x32 Visual Studio

§ Here's the full disassembly listing with my comments. Many lines disappeared because they've been optimized out

namespace FloatDeterminism
{
    class Program
    {
        static void Main(string[] args)
        {
            var x = Double.MinValue;
02C40848 55                   push        ebp
02C40849 8B EC                mov         ebp,esp
02C4084B DD 05 90 08 C4 02    fld         qword ptr [FloatDeterminism.Program.Main(System.String[])+048h (02C40890h)]  # Load -1.7976931348623157e+308
02C40851 83 EC 08             sub         esp,8
02C40854 DD 1C 24             fstp        qword ptr [esp]
02C40857 DD 05 98 08 C4 02    fld         qword ptr [FloatDeterminism.Program.Main(System.String[])+050h (02C40898h)]  # Load 3.5953862697246315e+307
02C4085D 83 EC 08             sub         esp,8
02C40860 DD 1C 24             fstp        qword ptr [esp]
02C40863 B9 06 00 00 00       mov         ecx,6
02C40868 FF 15 60 4D 1C 01    call        dword ptr [Pointer to: FloatDeterminism.Program.GetVal(Double, Int32, Double) (011C4D60h)]  # Call GetVal()
02C4086E 83 EC 08             sub         esp,8
02C40871 DD 1C 24             fstp        qword ptr [esp]
02C40874 E8 7F 10 2F 70       call        System.Console.WriteLine(Double) (72F318F8h)  # Print GetVal()
02C40879 DD 05 A0 08 C4 02    fld         qword ptr [FloatDeterminism.Program.Main(System.String[])+058h (02C408A0h)]  # Load inf
02C4087F 83 EC 08             sub         esp,8
02C40882 DD 1C 24             fstp        qword ptr [esp]
02C40885 E8 6E 10 2F 70       call        System.Console.WriteLine(Double) (72F318F8h)  # Print GetVal2() = inf
02C4088A 5D                   pop         ebp
02C4088B C3                   ret
            Console.WriteLine(GetVal2(x, 6, diff));
02C4088C 00 00                add         byte ptr [eax],al
02C4088E 00 00                add         byte ptr [eax],al
02C40890 FF                   ?? ??????
02C40891 FF                   ?? ??????
02C40892 FF                   ?? ??????
02C40893 FF                   ?? ??????
02C40894 FF                   ?? ??????
02C40895 FF                   ?? ??????
            Console.WriteLine(GetVal2(x, 6, diff));
02C40896 EF                   out         dx,eax
02C40897 FF 99 99 99 99 99    call        fword ptr [ecx-66666667h]
02C4089D 99                   cdq
02C4089E C9                   leave
02C4089F 7F 00                jg          FloatDeterminism.Program.Main(System.String[])+059h (02C408A1h)
02C408A1 00 00                add         byte ptr [eax],al
02C408A3 00 00                add         byte ptr [eax],al
02C408A5 00 F0                add         al,dh
02C408A7 7F 20                jg          FloatDeterminism.Program.GetVal(Double, Int32, Double)+011h (02C408C9h)
02C408A9 13 1C 01             adc         ebx,dword ptr [ecx+eax]
02C408AC 00 00                add         byte ptr [eax],al
02C408AE 00 00                add         byte ptr [eax],al
02C408B0 14 13                adc         al,13h
02C408B2 1C 01                sbb         al,1
02C408B4 58                   pop         eax
02C408B5 4D                   dec         ebp
02C408B6 1C 01                sbb         al,1

        public static double GetVal(double start, int numSteps, double step)
        {
            var res = start + numSteps * step;
02C408B8 56                   push        esi
02C408B9 50                   push        eax
02C408BA 89 0C 24             mov         dword ptr [esp],ecx
02C408BD DB 04 24             fild        dword ptr [esp]
02C408C0 DC 4C 24 0C          fmul        qword ptr [esp+0Ch]
02C408C4 DC 44 24 14          fadd        qword ptr [esp+14h]
02C408C8 DD 05 00 09 C4 02    fld         qword ptr [FloatDeterminism.Program.GetVal(Double, Int32, Double)+048h (02C40900h)]
02C408CE DF F1                fcomip      st,st(1)  # Check for NaN
02C408D0 7A 06                jp          FloatDeterminism.Program.GetVal(Double, Int32, Double)+020h (02C408D8h)
02C408D2 75 04                jne         FloatDeterminism.Program.GetVal(Double, Int32, Double)+020h (02C408D8h)
02C408D4 DD D8                fstp        st(0)
02C408D6 EB 05                jmp         FloatDeterminism.Program.GetVal(Double, Int32, Double)+025h (02C408DDh)
02C408D8 59                   pop         ecx
02C408D9 5E                   pop         esi
02C408DA C2 10 00             ret         10h
02C408DD B9 74 D6 3F 72       mov         ecx,723FD674h
02C408E2 E8 0D 28 57 FE       call        CORINFO_HELP_NEWSFAST (011B30F4h)
02C408E7 8B F0                mov         esi,eax
02C408E9 8B CE                mov         ecx,esi
                throw new InvalidOperationException();
02C408EB FF 15 AC D6 3F 72    call        dword ptr [Pointer to: System.InvalidOperationException..ctor() (723FD6ACh)]
02C408F1 8B CE                mov         ecx,esi
02C408F3 E8 88 2D 9D 71       call        74613680
02C408F8 CC                   int         3
02C408F9 CC                   int         3
like image 86
phuclv Avatar answered Nov 15 '22 13:11

phuclv