.Чистая математика.Log10 () ведет себя по-разному на разных машинах


Я обнаружил, что бегу

Math.Log10(double.Epsilon) 

Вернет о -324 на машину А, но вернет -Infinity на машину Б.

Первоначально они вели себя точно так же, возвращаясь -324.

Обе машины начинали с одной и той же операционной системы (WinXP SP3) и версии .NET (3.5 SP1). Возможно, на машине B были обновления Windows, но в остальном никаких изменений не произошло. Чем можно объяснить разницу в поведении?

Подробнее из обсуждений в комментарии:

  • Машина процессор-это 32-разрядный Intel Core Duo T2500 2 ГГц
  • процессор Machine B представляет собой 32-разрядный процессор Intel P4 2,4 ГГц
  • результаты, полученные из кода, запущенного в большом приложении с использованием нескольких сторонних компонентов. Впрочем, то же самое .версии exe и компонентов работают на обеих машинах.
  • печать Math.Log10(double.Epsilon) в простом консольном приложении на машине B печатает -324, а не -Infinity
  • управляющее слово FPU на обеих машинах всегда 0x9001F (читается с помощью _controlfp()).

UPDATE: последний пункт (управляющее слово FPU) больше не соответствует действительности: использование более новой версии _controlfp () выявило различные управляющие слова, что объясняет несогласованное поведение. (Подробнее см. ответ rsbarro ниже.)

2 31

2 ответа:

Основываясь на комментариях @CodeInChaos и @Alexandre C, я смог собрать вместе некоторый код для воспроизведения проблемы на моем ПК (Win7 x64, .NET 4.0). По-видимому, эта проблема связана с денормальным управлением, которое может быть установлено с помощью _controlfp_s. Значение двойное.Эпсилон одинаков в обоих случаях, но способ его оценки меняется, когда денормальный контроль переключается с сохранения на сброс.

Вот пример кода:

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();
        }
    }
}

Несколько вещей, чтобы отметить. Во-первых, я должен был ... укажите CallingConvention = CallingConvention.Cdecl в объявлении ControlFPS, чтобы избежать получения несбалансированного исключения стека во время отладки. Во-вторых, мне пришлось прибегнуть к небезопасному коду, чтобы получить значение управляющего слова в GetCurrentControlWord(). Если кто-нибудь знает лучший способ написать этот метод, пожалуйста, дайте мне знать.

Вот результат:

_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
Чтобы определить, что происходит с машинами A и B, вы можете взять пример приложения выше и запустить его на каждой машине. Я думаю, ты найдешь это. либо:
  1. машины A и B с самого начала используют разные настройки для _controlfp_s. Пример приложения покажет различные значения управляющих слов в первом блоке выходных данных на машине а, чем на машине Б. После того, как приложение принудительно сохранит Денормальный элемент управления, выходные данные должны совпадать. Если это так, то, возможно, вы можете просто заставить denormal control сэкономить на машине B, Когда ваше приложение запускается.
  2. машина А и машина б используют одни и те же настройки для _controlfp_s, и выходные данные примера приложения точно такие же на обеих машинах. Если это так, то в вашем приложении должен быть какой-то код (возможно DirectX, WPF?) то есть переключение настроек _controlfp_s на машине B, но не на машине A.

Если у вас есть возможность попробовать пример приложения на каждой машине, пожалуйста, обновите комментарии с результатами. Мне интересно посмотреть, что произойдет.

Возможно, что dll была загружена в процесс, который испортил флаги x87 с плавающей запятой. Библиотеки, связанные с DirectX/OpenGL, печально известны этим.

Также могут быть различия в jitted-коде (нет требования, чтобы плавающие точки вели себя определенным образом в .net), но это очень маловероятно, так как вы используете одну и ту же версию .net и OS.

В .net константы запекаются в вызывающем коде, поэтому не должно быть никаких различий между double.Epsilons.