24

If I run the statement

Math.Exp(113.62826122038274).ToString("R")

on a machine with .net 4.5.1 installed, then I get the answer

2.2290860617259248E+49

However, if I run the same command on a machine with .net framework 4.5.2 installed, then I get the answer

2.2290860617259246E+49

(i.e. the final digit changes)

I realise that this is broadly insignificant in pure numeric terms, but does anyone know of any changes that have been made in .net 4.5.2 that would explain the change?

(I don't prefer one result to the other, I am just interested to understand why it has changed)

If I output

The input in roundtrip format
The input converted to a long via BitConverter.DoubleToInt64Bits
Math.Exp in roundtrip format
Math.Exp converted to a long via BitConverter.DoubleToInt64Bits

then on 4.5.1 I get

113.62826122038274
4637696294982039780
2.2290860617259248E+49
5345351685623826106

and on 4.5.2 I get:

113.62826122038274
4637696294982039780
2.2290860617259246E+49
5345351685623826105

So for the exact same input, I get a different output (as can be seen from the bits so no roundtrip formatting is involved)

More details:

Compiled once using VS2015

Both machines that I am running the binaries on are 64bit

One has .net 4.5.1 installed, the other 4.5.2

Just for clarity: the string conversion is irrelevant... I get the change in results regardless of whether string conversion is involved. I mentioned that purely to demonstrate the change.

Fergus Bown
  • 1,666
  • 9
  • 16
  • is net46 installed as well? – Dbl Oct 15 '15 at 16:11
  • @AndreasMüller - No, just 4.5.2, though the 4.6 manifests the same answer as 4.5.2 - the change appears to be introduced by 4.5.2 not 4.6 – Fergus Bown Oct 15 '15 at 16:13
  • Are you compiling the code on each machine? If so, is one of them using Roslyn and the other using the "old" compiler? That could explain things - but to reduce things, I'd suggest removing the `Math.Exp` call and just printing out `113.62826122038274.ToString("R")`. If my suspicion is right, you'll see different results there too. – Jon Skeet Oct 15 '15 at 16:13
  • @JonSkeet I compiled the code on a machine with 4.6 installed, but targeting .net 4.0 – Fergus Bown Oct 15 '15 at 16:14
  • @JonSkeet 113.62826122038274.ToString("R") returns the same on both machines – Fergus Bown Oct 15 '15 at 16:16
  • Are both systems 32 or 64 bits? Running your code and toggling the target platform appears to reproduce the difference. – petelids Oct 15 '15 at 16:17
  • @petelids both are 64 bit – Fergus Bown Oct 15 '15 at 16:18
  • @FergusBown afaik even just having 4.6 installed can give you bugs(though some major ones have been fixed already), even if your programs are compiled for another target, if the new clr gets a hold of your program. have a look if the same thing is happening when compiling for 32 bit. the new clr can't take over then – Dbl Oct 15 '15 at 16:19
  • @HansPassant This may be a silly question - but why does "R" (Roundtrip format) display more than 15 digits if its just random? And if its random, how come it is completely consistent on machines with 4.5.1 vs those with 4.5.2? – Fergus Bown Oct 15 '15 at 16:24
  • Does the CLR ultimately call the CRT library functions or are they completely independent? – Peter - Reinstate Monica Oct 15 '15 at 16:25
  • If it needs to round-trip then 15 digits isn't enough, that can cause rounding errors. An inevitable side effect of the processor working in base 2 and humans in base 10. Actual number of significant digits is 15.95458977, not a nice round number. The only way to get ahead is to avoid assuming that random decimal digits should not be random. – Hans Passant Oct 15 '15 at 16:34
  • 2.2290860617259248E+49 is `0x1.e81098524c6bap163` and 2.2290860617259246e49 is `0x1.e81098524c6b9p163` in double precision. It's a single digit change – phuclv Oct 15 '15 at 17:02
  • What happens if you examine the **bits** in your double? [`BitConverter.DoubleToInt64Bits`](https://msdn.microsoft.com/en-us/library/system.bitconverter.doubletoint64bits.aspx) will return a `long` with the same binary representation which you can compare more easily. In practice, a `double` may have [extended precision](http://en.wikipedia.org/wiki/Extended_precision) which would mean it's stored internally as 80, not 64 bits... – Wai Ha Lee Oct 15 '15 at 17:36
  • 1
    @WaiHaLee see updated question – Fergus Bown Oct 16 '15 at 08:34
  • 4
    Floating point operations are performed by the CPU, not the framework. Is there a difference in the CPUs? The difference could also be caused by one framework using the FPU and another using SSE. 4.5.2 didn't make such a change but 4.6 did introduce SIMD operations. 4.6 is a binary replacement of 4.5 so *maybe* this is what causes the difference – Panagiotis Kanavos Oct 16 '15 at 08:42
  • @PanagiotisKanavos I have run the binaries on several different machines with 4.5.1 and several different machines with 4.5.2 and consistently get the same results, which would seem to suggest that it is not down to CPU differences. Also ALL of the machines got the same answer consistently before the installation of 4.5.2 (this is essentially a simplified example of code that runs as part of our continuous test suite) – Fergus Bown Oct 16 '15 at 09:31
  • The string to double conversion algorithm was changed in VS 2015 (see http://www.exploringbinary.com/visual-c-plus-plus-strtod-still-broken/ ), though from your description it's not clear that that is the issue. – Rick Regan Oct 16 '15 at 13:43
  • @Rick Regan i don't think that has *anything* to do with this issue – Timothy Groote Nov 06 '15 at 08:39
  • 2
    On my machine with `4.5.2` I get your first result, final digit ending in 8, not the 6, `2.2290860617259248E+49`. – Chris O Nov 07 '15 at 16:03
  • See also https://connect.microsoft.com/VisualStudio/feedback/details/2486915/math-exp-returns-different-results "With .NET 4.6.1 installed and using Math.Exp, I get different results depending on whether KB3098785 has been installed." – Colonel Panic Apr 08 '16 at 12:08

4 Answers4

12

Sigh, the mysteries of floating point math continue to stump programmers forever. It does not have anything to do with the framework version. The relevant setting is Project > Properties > Build tab.

Platform target = x86: 2.2290860617259248E+49
Platform target = AnyCPU or x64: 2.2290860617259246E+49

If you run the program on a 32-bit operating system then you always get the first result. Note that the roundtrip format is overspecified, it contains more digits than a double can store. Which is 15. Count them off, you get 16. This ensures that the binary representation of the double, the 1s and 0s are the same. The difference between the two values is the least significant bit in the mantissa.

The reason that the LSB is not the same is because the x86 jitter is encumbered with generating code for the FPU. Which has the very undesirable property of using more bits of precision than a double can store. 80 bits instead of 64. Theoretically to generate more accurate calculation results. Which it does, but rarely in a reproducible way. Small changes to the code can produce large changes in the calculation result. Just running the code with a debugger attached can change the result since that disables the optimizer.

Intel fixed this mistake with the SSE2 instruction set, completely replacing the floating point math instructions of the FPU. It does not use extra precision, a double always has 64 bits. With the highly desirable property that the calculation result now no longer depends on intermediate storage, it is now much more consistent. But less accurate.

That the x86 jitter uses FPU instructions is a historical accident. Released in 2002, there were not enough processors around that supported SSE2. That accident cannot be fixed anymore since it changes the observable behavior of a program. It was not a problem for the x64 jitter, a 64-bit processor is guaranteed to also support SSE2.

A 32-bit process uses the exp() function that uses FPU code. A 64-bit process uses the exp() function that uses SSE code. The result may be different by one LSB. But still accurate to 15 significant digits, it is 2.229086061725925E+49. All you can ever expect out of math with double.

Community
  • 1
  • 1
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • I'm not convinced this explains "Math.Exp returns different results after installing .NET 4.6.1 and applying KB3098785" https://connect.microsoft.com/VisualStudio/feedback/details/2486915/math-exp-returns-different-results – Colonel Panic Apr 03 '16 at 19:51
  • I told you what to do. Do try it yourself, change the Platform target and observe that x86 generates 0.71612515940795685 and x64 generates 0.71612515940795696. Nothing to do with the framework version, everything to do with FPU vs SSE2. – Hans Passant Apr 03 '16 at 19:59
  • In my case Any CPU gave 2.2290860617259248E+49, because Prefer 32bit was set. But the same with your answer in general. – cassandrad Apr 08 '16 at 15:13
7

.NET uses the math lib functions from the CRT to do these calculations. The CRT used by .NET is often updated with each release, so you can expect the results to change between .NET releases, however, they will always be within the promised +/1ulp.

Holly M
  • 71
  • 3
  • @flq - C Runtime. Its Microsoft's C standard library that ships with their compilers. This SO post has a lot of good information: http://stackoverflow.com/questions/2766233/what-is-the-c-runtime-library – antiduh Nov 13 '15 at 19:14
1

I tripped over the same problem however I only get it after installing .Net 4.6.

The .Net 4.6 installation upgraded c:\windows\system32\msvcr120_clr0400.dll and c:\windows\syswow64\msvc120_clr0400.dll.

Before the install these DLLs had file properties->details: "File version 12.0.51689.34249" and "Product name Microsoft Visual Studio 12 CTP"

After installing .net 4.6 these had "File version 12.0.52512.0" and "Product name Microsoft Visual Studio 2013"

I tweaked my test to include your example and I see the same before/after numbers as you by flipping between versions of this DLL.

(Our test suite didn't show any changed results when running on .net versions 4, 4.5, 4.5.1, or 4.5.2 until these DLLs were updated.)

TomStones
  • 21
  • 2
1

To show how projects targeting different .NET versions affect the double conversion from string I built 4 projects targeting different versions all running on the same dev machine with .NET 4.6.

Here's the code

double foo = Convert.ToDouble("33.94140881672595");

And here's the output

33.941408816725954 (.NET 4)

33.941408816725946 (.NET 4.5)

33.941408816725946 (.NET 4.5.2)

33.941408816725946 (.NET 4.6)

So, there was definitely a change in conversion method after .NET 4