Будет ли диапазон переменных с плавающей запятой влиять на их значения?

Если мы выполним следующий код С# в консольном приложении, мы получим сообщение как The sums are Not equal.

Если мы выполним его после раскола строки System.Console.WriteLine(), мы получим сообщение как The sums are equal.

    static void Main(string[] args)
    {
        float f = Sum(0.1f, 0.2f);
        float g = Sum(0.1f, 0.2f);

        //System.Console.WriteLine("f = " + f + " and g = " + g);

        if (f == g)
        {
            System.Console.WriteLine("The sums are equal");
        }
        else
        {
            System.Console.WriteLine("The sums are Not equal");
        }
    }

    static float Sum(float a, float b)
    {
        System.Console.WriteLine(a + b);
        return a + b;
    }

Какова фактическая причина такого поведения?

Ответ 1

Это не связано с областью. Это комбинация динамики стека и обработки с плавающей запятой. Некоторые знания компиляторов помогут сделать это противоречивое поведение понятным.

Когда комментарий Console.WriteLine прокомментирован, значения f и g находятся в стеке оценки и остаются там до тех пор, пока вы не пройдете тест равенства в методе Main.

Когда Console.WriteLine не комментируется, значения f и g перемещаются из стека оценки в стек вызовов в момент вызова, чтобы быть восстановлены в стек оценки, когда возвращается Console.WriteLine. И ваше сравнение if (f == g) выполняется потом. При таком хранении значений в стек вызовов может возникать некоторое округление, и некоторая информация может быть потеряна.

В сценарии, в котором вы вызываете Console.WriteLine, f и g в тесте сравнения не совпадают. Они были скопированы и восстановлены в формате, который имеет разные правила точности и округления, виртуальной машиной.

В вашем конкретном коде, когда комментарий Console.WriteLine прокомментирован, стек оценки никогда не сохраняется в стеке вызовов и не происходит округления. Поскольку для реализации платформы допускается повышение точности в стеке оценки, это несоответствие может возникнуть.

РЕДАКТИРОВАТЬ Что мы видим в этом случае, допускается спецификацией CLI. В разделе I.12.1.3 он гласит:

Место хранения чисел с плавающей запятой (статика, элементы массива, и поля классов) имеют фиксированный размер. Поддерживаемые размеры хранилища являются float32 и float64. Везде (в стеке оценки, как аргументы, как типы возврата, так и локальные переменные) с плавающей запятой числа представлены с использованием внутреннего типа с плавающей запятой. В каждом такой экземпляр, номинальный тип переменной или выражения либо float32or float64, но его значение может быть представлено внутри с дополнительным диапазоном и/или точностью. Размер внутреннего представление с плавающей запятой зависит от реализации, может варьироваться, и должна иметь точность, по меньшей мере такую ​​же, как у переменной или представленное выражение.

Ключевые слова из этой цитаты "зависят от реализации" и "могут варьироваться". В случае OP мы видим, что его реализация действительно меняется.

Арифметика non-strictfp с плавающей запятой в платформе Java также имеет связанную с этим проблему, для дополнительной проверки информации также мой ответ на Операции с плавающей запятой на JVM дают одинаковые результаты для всех платформы?

Ответ 2

Какова фактическая причина такого поведения?

Я не могу предоставить подробную информацию о том, что происходит в этом конкретном случае, но я понимаю общую проблему и почему использование Console.WriteLine может изменить ситуацию.

Как мы видели в вашем предыдущем сообщении, иногда операции выполняются с использованием типов с плавающей точкой с более высокой точностью, чем та, которая указана в типе переменной. Для локальных переменных это может включать в себя то, как значение хранится в памяти во время выполнения метода.

Я подозреваю, что в вашем случае:

  • используется метод Sum (но см. ниже)
  • сама сумма выполняется с большей точностью, чем ожидаемое 32-битное плавание
  • значение одной из переменных (f say) хранится в высокоточном регистре
    • для этой переменной, "более точный" результат сохраняется непосредственно
  • значение другой переменной (g) хранится в стеке как 32-битное значение
    • для этой переменной "более точный" результат уменьшается до 32 бит.
  • когда сравнение выполняется, переменная в стеке продвигается до значения более высокой точности и сравнивается с другим значением более высокой точности, и разница заключается в том, что одна из них имеет ранее потерянную информацию, а другая -

Когда вы раскомментируете оператор Console.WriteLine, я предполагаю, что (по какой-либо причине) заставляет обе переменные быть сохранены в их "правильной" 32-битной точности, поэтому оба они обрабатываются одинаково.

Эта гипотеза все испорчена тем фактом, что добавление

[MethodImpl(MethodImplOptions.NoInlining)]

... не меняет результат, насколько я могу видеть. Я могу делать что-то еще неправильно по этим строкам, хотя.

Действительно, мы должны посмотреть на код сборки, который выполняется - у меня нет времени сделать это сейчас, к сожалению.

Ответ 3

(Не настоящий ответ, но, надеюсь, какая-то вспомогательная документация)

Конфигурация: Core i7, Windows 8.1, Visual Studio 2013

Платформа x86:

Version      Optimized Code?        Debugger Enabled?          Outcome
4.5.1        Yes                    No                         Not equal
4.5.1        Yes                    Yes                        Equal
4.5.1        No                     No                         Equal
4.5.1        No                     Yes                        Equal
2.0          Yes                    No                         Not Equal
2.0          Yes                    Yes                        Equal
2.0          No                     No                         Equal
2.0          No                     Yes                        Equal

Платформа x64:

Version      Optimized Code?        Debugger Enabled?          Outcome
4.5.1        Yes                    No                         Equal
4.5.1        Yes                    Yes                        Equal
4.5.1        No                     No                         Equal
4.5.1        No                     Yes                        Equal
2.0          Yes                    No                         Equal
2.0          Yes                    Yes                        Equal
2.0          No                     No                         Equal
2.0          No                     Yes                        Equal

Ситуация возникает только с оптимизированным кодом в конфигурациях x86.