32

I found that running

Math.Log10(double.Epsilon) 

will return about -324 on machine A, but will return -Infinity on machine B.

They originally behaved the same way by returning -324.

Both machines started out using the same OS (WinXP SP3) and .NET version (3.5 SP1). There may have been Windows updates on machine B, but otherwise no changes are known to have happened.

What could explain the difference in behavior?

More details from discussions in comments:

  • Machine A CPU is a 32-bit Intel Core Duo T2500 2 GHz
  • Machine B CPU is a 32-bit Intel P4 2.4 GHz
  • Results collected from code running in a large application using several 3rd party components. However, same .exe and component versions are running on both machines.
  • Printing Math.Log10(double.Epsilon) in a simple console application on machine B prints -324, NOT -Infinity
  • The FPU control word on both machines is always 0x9001F (read with _controlfp()).

UPDATE: The last point (FPU control word) is no longer true: Using a newer version of _controlfp() revealed different control words, which explains the inconsistent behavior. (See rsbarro's answer below for details.)

  • Interesting, I'm getting -323.306215343116. – Yuriy Faktorovich Aug 08 '11 at 22:01
  • 1
    Is this with a simple console app which does nothing but print `Math.Log10(double.Epsilon)`? – Jon Skeet Aug 08 '11 at 22:02
  • @Yuriy Faktorovich - so is [ideaone.com](http://ideone.com/e7QJE) – Chris Gregg Aug 08 '11 at 22:06
  • Yuriy: This is what I meant by "about -324". My code had an additional Math.Floor() around the whole thing; the exact number I'm getting (on machine A) is -323.30621534311581. – Johannes Petzold Aug 08 '11 at 22:06
  • Jon: machine B output is from a debug print statement in a large application. The debug statement uses string.Format, and one of its argument is nothing but Math.Log10(double.Epsilon) – Johannes Petzold Aug 08 '11 at 22:09
  • @Johannes: better post a complete sample, including the Floor(). And run that separate project on both machines too. Could be something with an internal state (rounding mode, error) in the FPU. – Henk Holterman Aug 08 '11 at 22:11
  • 1
    @Ben: Or maybe they overwrote the CLR with a JVM... – Henk Holterman Aug 08 '11 at 22:13
  • @Henk: No doubt they're using the weird results from `Math.Log10(double.Epsilon)` to justify exactly that. – Ben Voigt Aug 08 '11 at 22:16
  • 1
    @Ben it's a constant, you can't do that. You'd have to modify the dll or somehow inject different functionality into the CLR. – Yuriy Faktorovich Aug 08 '11 at 22:19
  • 1
    @Ben: .NET constants are constant. They're burned into the code. `Math.Log10(double.Epsilon)` will get compiled as if it was something like `Math.Log10(4.94065645841247E-324)`, so there's no way you can change `double.Epsilon` and affect this code. It's `readonly` members that can be changed like `string.Empty`. – R. Martinho Fernandes Aug 08 '11 at 22:24
  • 3
    One way to find out whether Double.Epsilon is really different is to run the following code: `Console.Write("{0:X16}",BitConverter.DoubleToInt64Bits(Double.Epsilon));` on each machine. More worthwhile would be to check whether each machine exhibits different floating point behavior in other cases, as it may be likely that floating-point results can vary even in the same machine, see http://stackoverflow.com/questions/6683059/are-floating-point-numbers-consistent-in-c-can-they-be . – Peter O. Aug 08 '11 at 22:30
  • Fyi - I also collected the output of double.Epsilon on machine B, and it is the same as machine A (4.94065645841247E-324). – Johannes Petzold Aug 08 '11 at 22:30
  • It shouldn't be different. `double` is required to comply with IEEE754. You can get different results when doing arithmetic, but the constant `double.Epsilon` should be the same. – R. Martinho Fernandes Aug 08 '11 at 22:34
  • even Silverlight returns -324 – Lukasz Madon Aug 08 '11 at 22:35
  • 1
    I think this is a bug and you should figure out what is different about the two machines and report it. – R. Martinho Fernandes Aug 08 '11 at 22:39
  • @Ben: Trying to change `double.Epsilon` through reflection throws `FieldAccessException` even when running under full trust. – R. Martinho Fernandes Aug 08 '11 at 22:50
  • @Ben, Marhino, Could one of you please post the question "Can you change double.Epsilon with reflection" ? – Henk Holterman Aug 08 '11 at 22:55
  • @R.: You're correct, it appears that the runtime protects literals against modification through reflection (unlike `initonly` fields). – Ben Voigt Aug 09 '11 at 01:08
  • Differing float values *can* be a processor issue. Do you have the same processor in both machines? (Note: It shouldn't be but it sometimes is. I am terrible with .Net though so it might be a .Net issue and I just don't know it) – medivh Aug 09 '11 at 08:34
  • Same CLR version on both machines? Not running mono on either? – alun Aug 09 '11 at 08:46
  • @alun: Mono 2.8.1 on windows return -323.306215343116 – sehe Aug 09 '11 at 09:03
  • Are both CPUs x86 only, and what about regional settings on each PC that control the formatting of numbers? – ta.speot.is Aug 09 '11 at 09:32
  • Does this mean that if you write C# code, you have to test it on every possible CPU and every Windows version??? – NoChance Nov 18 '18 at 02:22

2 Answers2

22

Based on the comments by @CodeInChaos and @Alexandre C, I was able to throw together some code to reproduce the issue on my PC (Win7 x64, .NET 4.0). It appears this issue is due to the denormal control that can be set using _controlfp_s. The value of double.Epsilon is the same in both cases, but the way it is evaluated changes when the denormal control is switched from SAVE to FLUSH.

Here is the sample code:

using System;
using System.Runtime.InteropServices;

namespace fpuconsole
{
    class Program
    {
        [DllImport("msvcrt.dll", EntryPoint = "_controlfp_s",
            CallingConvention = CallingConvention.Cdecl)]
        public static extern int ControlFPS(IntPtr currentControl, 
            uint newControl, uint mask);

        public const int MCW_DN= 0x03000000;
        public const int _DN_SAVE = 0x00000000;
        public const int _DN_FLUSH = 0x01000000;

        static void PrintLog10()
        {
            //Display original values
            Console.WriteLine("_controlfp_s Denormal Control untouched");
            Console.WriteLine("\tCurrent _controlfp_s control word: 0x{0:X8}", 
                GetCurrentControlWord());
            Console.WriteLine("\tdouble.Epsilon = {0}", double.Epsilon);
            Console.WriteLine("\tMath.Log10(double.Epsilon) = {0}",
                Math.Log10(double.Epsilon));
            Console.WriteLine("");

            //Set Denormal to Save, calculate Math.Log10(double.Epsilon)
            var controlWord = new UIntPtr();
            var err = ControlFPS(controlWord, _DN_SAVE, MCW_DN);
            if (err != 0)
            {
                Console.WriteLine("Error setting _controlfp_s: {0}", err);
                return;
            }
            Console.WriteLine("_controlfp_s Denormal Control set to SAVE");
            Console.WriteLine("\tCurrent _controlfp_s control word: 0x{0:X8}", 
                GetCurrentControlWord());
            Console.WriteLine("\tdouble.Epsilon = {0}", double.Epsilon);
            Console.WriteLine("\tMath.Log10(double.Epsilon) = {0}", 
                Math.Log10(double.Epsilon));
            Console.WriteLine("");

            //Set Denormal to Flush, calculate Math.Log10(double.Epsilon)
            err = ControlFPS(controlWord, _DN_FLUSH, MCW_DN);
            if (err != 0)
            {
                Console.WriteLine("Error setting _controlfp_s: {0}", err);
                return;
            }
            Console.WriteLine("_controlfp_s Denormal Control set to FLUSH");
            Console.WriteLine("\tCurrent _controlfp_s control word: 0x{0:X8}", 
                GetCurrentControlWord());
            Console.WriteLine("\tdouble.Epsilon = {0}", double.Epsilon);
            Console.WriteLine("\tMath.Log10(double.Epsilon) = {0}", 
                Math.Log10(double.Epsilon));
            Console.WriteLine("");
        }

        static int GetCurrentControlWord()
        {
            unsafe
            {
                var controlWord = 0;
                var controlWordPtr = &controlWord;
                ControlFPS((IntPtr)controlWordPtr, 0, 0);
                return controlWord;
            }
        }

        static void Main(string[] args)
        {
            PrintLog10();
        }
    }
}

A couple things to note. First, I had to specify CallingConvention = CallingConvention.Cdecl on the ControlFPS declaration to avoid getting an unbalanced stack exception while debugging. Second, I had to resort to unsafe code to retrieve the value of the control word in GetCurrentControlWord(). If anyone knows of a better way to write that method, please let me know.

Here is the output:

_controlfp_s Denormal Control untouched
        Current _controlfp_s control word: 0x0009001F
        double.Epsilon = 4.94065645841247E-324
        Math.Log10(double.Epsilon) = -323.306215343116

_controlfp_s Denormal Control set to SAVE
        Current _controlfp_s control word: 0x0009001F
        double.Epsilon = 4.94065645841247E-324
        Math.Log10(double.Epsilon) = -323.306215343116

_controlfp_s Denormal Control set to FLUSH
        Current _controlfp_s control word: 0x0109001F
        double.Epsilon = 4.94065645841247E-324
        Math.Log10(double.Epsilon) = -Infinity

To determine what is going on with machine A and machine B, you could take the sample app above and run it on each machine. I think you're going to find that either:

  1. Machine A and Machine B are using different settings for _controlfp_s right from the start. The sample app will show different control word values in the first block of outputs on Machine A than it does on Machine B. After the app forces the Denormal control to SAVE, then the output should match. If this is the case then maybe you can just force the denormal control to SAVE on Machine B when your application starts up.
  2. Machine A and Machine B are using the same settings for _controlfp_s, and the output of the sample app is exactly the same on both machines. If that is the case, then there must be some code in your application (possibly DirectX, WPF?) that is flipping the _controlfp_s settings on Machine B but not on Machine A.

If you get a chance to try out the sample app on each machine, please update the comments with the results. I'm interested to see what happens.

rsbarro
  • 25,799
  • 7
  • 68
  • 74
  • @Johannes See the update to my answer (especially at the bottom). I don't think that .NET guarantees the FPU control word will be initialized the same way in different environments. You might have to test and modify it accordingly. – rsbarro Aug 10 '11 at 05:39
  • I got the following results from executing your code on several machines: If **compiling for x86** platform target and **running on 64bit** CPU, I get the same results as you. Every other combination (compile with "Any CPU" target and/or run on 32bit CPU) never prints -Infinity. I still haven't executed the code on the original machine B (32bit CPU), will post results here once I got the chance (don't have direct access to that machine, so it may still be a few days). Note - I had to change the code to use _controlfp() instead of _controlfp_s(), the latter is not included in my msvcrt.dll. – Johannes Petzold Aug 10 '11 at 19:26
  • Weird. I get the same results when running on my Win7 x64 machine, regardless of platform. The only difference is that x64 and Any CPU have different initial control words (0x0008001F instead of 0x0009001F), but both print -Infinity when I set the DN control to FLUSH. I have a 32bit Vista machine at home, I'll test on that later tonight and let you know what I find. – rsbarro Aug 10 '11 at 20:47
  • On the 64bit CPU, I get the same initial control words as you. However on the 32bit CPU, the control word does actually not change (it's printed as 9001F three times). Can you test using _controlfp() instead of _controlfp_s()? (As mentioned this is what I had to modify your code to.) – Johannes Petzold Aug 11 '11 at 00:54
  • I checked on my laptop, which has a 64bit AMD processor and is running 32bit Vista, and I got the same results as in the answer for an x86 build. The Any CPU on my Vista laptop started with the 0x0009001F control word, but still produced the same output otherwise. The x64 build obviously won't run on the 32bit OS. This is still using _controlfp_s. I switched the app to use _controlfp, and I still get the same results on both my Win7 machine and Vista machine. – rsbarro Aug 11 '11 at 02:04
  • I ran your code on machine B - unfortunately it looks like the issue is unrelated to the FPU control word :( Results: A stand-alone console application prints 0x9001F three times and never -Infinity, just like any other x86 CPU I tested on. Embedding the code into the larger application which has the original -Infinity problem, it still prints 0x9001F three times, but now with -Infinity every time, unlike any other x86 CPU I tested on. – Johannes Petzold Aug 12 '11 at 17:51
  • 1
    Damn, that sucks. So to sum up here, you're seeing that on 32bit CPUs, Log10 never returns -Infinity when using the test app. Your larger application returns -Infinity in all cases on Machine B. I'll see what I can dig up. Can you tell me if your larger application uses any 3rd party components? – rsbarro Aug 12 '11 at 18:12
  • Yes, several 3rd party components, e.g. for image processing and custom hardware APIs. However the application .exe and all its 3rd party DLLs are deployed by a single installer, and when installing on a different CPU I don't see the -Infinity problem. Btw, I just found that when compiling for .NET4, I get exactly the behavior as you - i.e. no longer need "x86" target to see -Infinity. – Johannes Petzold Aug 12 '11 at 18:21
  • Also, as curious as I am about what is causing this to happen, is there a business reason for trying to do Math.Log10(double.Epsilon)? At this point it might just be easier to come up with a workaround rather than chasing this issue down the rabbit hole. – rsbarro Aug 12 '11 at 18:23
  • Sorry, one more thing, can you update the question with the specs (including processor type) for machine A and machine B? – rsbarro Aug 12 '11 at 18:27
  • As a matter of fact, I already worked around the issue by simply using 1e-100 instead of double.Epsilon, which makes the application work just fine again. So this is now just a matter of interest/trying to avoid similar problems in the future. I'll update the question, thanks for the suggestion. – Johannes Petzold Aug 12 '11 at 18:35
  • 1
    OK, so I looked into this a bit more, and it turns out there are actually two types of floating point operations, x86 and SSE2. It's possible that the flags on Machine B are set for SSE2 and not for x86, but I'm having a hard time trying to reproduce the issue. I think the problem could be introduced however by a call to __control87_2. This function is defined either in msvcr80.dll (or msvcr90.dll or msvcr100.dll). On my Win7 dev machine I have it in msvcr100.dll, but on a test W2K3 server machine I don't see it at all. – rsbarro Aug 16 '11 at 13:58
  • Can you see if those dll's (any msvcr*.dll) are present on both machines? I'm imagining a scenario where one of the third party components calls __control87_2 if it is present but falls back to _control_fp if it is not. That could get you an inconsistency... – rsbarro Aug 16 '11 at 13:59
  • 2
    Your last comments led me to find out that _controlfp() behaves differently, depending on which msvcr*.dll is used. Unlike before, I can now reproduce the issue on any 32-bit CPU simply by using "msvcr100_clr0400.dll" instead of "msvcrt.dll". I currently can't run tests on machine B anymore, but I think it's fair to assume that the FPU control word is the root cause for explaining the inconsistent behavior; that fact was just somewhat covered up by the inconsistent implementation of _controlfp(). Thanks for your help! – Johannes Petzold Aug 16 '11 at 23:25
  • 1
    @rsbarro Thanks for Your post! **Unsafe** code should be possible to be avoided when You add additional declaration `[DllImport("msvcrt.dll", EntryPoint = "_controlfp_s", CallingConvention = CallingConvention.Cdecl)]` `public static extern int ControlFPS(ref int currentControl, int newControl, int mask);` and then call it like this: `ControlFPS(ref controlWord, 0, 0);` It should do same thing as the unsafe code, just behind the scenes. – Roland Pihlakas Jan 05 '14 at 04:17
8

It's possible that a dll was loaded into the process that messed with the x87 floating-point flags. DirectX/OpenGL related libraries are notorious for this.

There could also be differences in the jitted code(There is no requirement for floating points to behave a specific way in .net), but that's very unlikely since you use the same .net and OS version.

In .net constants get baked into the calling code, so there should be no differences between the double.Epsilons.

CodesInChaos
  • 100,017
  • 20
  • 197
  • 251
  • 3
    I'm thinking exactly about denormalization flag. The `Double.Epsilon` (**very poorly** named btw, since epsilon for floating point already has a **very different** traditional meaning) is defined as "smallest positive Double value that is greater than zero". This means it is probably denormalized on your machine, and something set the denormalized flag so that those numbers are truncated to zero as they are fed to `Log10`. Good catch. – Alexandre C. Aug 09 '11 at 09:36
  • Some guys seems to have a similar issue with .NET objects created in delphi: http://wiert.wordpress.com/2009/05/06/delphi-michael-justin-had-strange-floating-point-results-when-his-8087-fpu-control-word-got-hosed/ they have to surround their computation code with 8087 control code too. – Simon Mourier Aug 10 '11 at 06:05
  • Yes, delphi does mess with the floating point flags too. At least delphi applications do, I don't remember if delphi libraries do. – CodesInChaos Aug 10 '11 at 08:16