.NET Math.Log10() behaves differently on different machines

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.

Leave a Comment

tech