Странное поведение компилятора с литералами плавающими по сравнению с переменными плавающими

Я заметил интересное поведение с плаванием, округляющимся / усечение компилятором C#. А именно, когда литерал плавающий вне гарантируемого представимого диапазона (7 десятичных цифр), затем a) явно бросая результат плавающий для плавания (семантически ненужная операция) и b) хранение промежуточных результатов вычисления в локальной переменной оба изменяет вывод. Пример:

using System;

class Program
{
    static void Main()
    {
        float f = 2.0499999f;
        var a = f * 100f;
        var b = (int) (f * 100f);
        var c = (int) (float) (f * 100f);
        var d = (int) a;
        var e = (int) (float) a;
        Console.WriteLine(a);
        Console.WriteLine(b);
        Console.WriteLine(c);
        Console.WriteLine(d);
        Console.WriteLine(e);
    }
}

Вывод:

205
204
205
205
205

В отладочной сборке JITted на моем компьютере b вычисляется следующим образом:

          var b = (int) (f * 100f);
0000005a  fld         dword ptr [ebp-3Ch] 
0000005d  fmul        dword ptr ds:[035E1648h] 
00000063  fstp        qword ptr [ebp-5Ch] 
00000066  movsd       xmm0,mmword ptr [ebp-5Ch] 
0000006b  cvttsd2si   eax,xmm0 
0000006f  mov         dword ptr [ebp-44h],eax 

тогда как d вычисляется как

          var d = (int) a;
00000096  fld         dword ptr [ebp-40h] 
00000099  fstp        qword ptr [ebp-5Ch] 
0000009c  movsd       xmm0,mmword ptr [ebp-5Ch] 
000000a1  cvttsd2si   eax,xmm0 
000000a5  mov         dword ptr [ebp-4Ch],eax 

Наконец, мой вопрос: почему отличается вторая строка вывода от четвертого? Тот дополнительный fmul имеет такое значение? Также обратите внимание, что, если последнее (уже непредставимый) цифра от плавания f удалена или даже уменьшена, все "падает на месте".

7
задан Alan 21 June 2010 в 20:42
поделиться

3 ответа

Ваш вопрос можно упростить, задав вопрос, почему эти два результата различаются:

float f = 2.0499999f;
var a = f * 100f;
var b = (int)(f * 100f);
var d = (int)a;
Console.WriteLine(b);
Console.WriteLine(d);

Если вы посмотрите на код в .NET Reflector, вы увидите, что приведенный выше код на самом деле компилируется, как если бы это был следующий код:

float f = 2.05f;
float a = f * 100f;
int b = (int) (f * 100f);
int d = (int) a;
Console.WriteLine(b);
Console.WriteLine(d);

Вычисления с плавающей запятой не всегда могут быть выполнены точно. Результат 2.05 * 100f не совсем равен 205, а лишь немного меньше из-за ошибок округления. Когда этот промежуточный результат преобразуется в целое число, он усекается. При хранении в виде числа с плавающей запятой он округляется до ближайшей представимой формы. Эти два метода округления дают разные результаты.


Что касается вашего комментария к моему ответу, когда вы пишете это:

Console.WriteLine((int) (2.0499999f * 100f));
Console.WriteLine((int)(float)(2.0499999f * 100f));

Вычисления полностью выполняются в компиляторе. Приведенный выше код эквивалентен следующему:

Console.WriteLine(204);
Console.WriteLine(205);
5
ответ дан 6 December 2019 в 23:00
поделиться

В комментарии вы спросили

Отличаются ли эти правила?

Да. Или, скорее, правила допускают другое поведение.

И если да, то должен ли я знать об этом из справочной документации по языку C# или MSDN, или это просто случайное расхождение между компилятором и средой выполнения

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

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

Почему это вычисление с плавающей точкой дает разные результаты на разных машинах?

C# XNA Visual Studio: Разница между режимами "release" и "debug"?

CLR JIT оптимизация нарушает причинно-следственные связи?

https://stackoverflow.com/questions/2494724

4
ответ дан 6 December 2019 в 23:00
поделиться

Марк прав насчет компилятора.Теперь давайте обманем компилятор:

    float f = (Math.Sin(0.5) < 5) ? 2.0499999f : -1;
    var a = f * 100f;
    var b = (int) (f * 100f);
    var c = (int) (float) (f * 100f);
    var d = (int) a;
    var e = (int) (float) a;
    Console.WriteLine(a);
    Console.WriteLine(b);
    Console.WriteLine(c);
    Console.WriteLine(d);
    Console.WriteLine(e);

первое выражение бессмысленно, но не позволяет компилятору оптимизировать. Результат:

205
204
205
204
205

Хорошо, я нашел объяснение.

2.0499999f не может быть сохранен как число с плавающей запятой, потому что он может содержать только 7 цифр с отсчетом от 10. и этот литерал состоит из 8 цифр, поэтому компилятор округлил его, потому что не смог сохранить. (должен выдать предупреждение IMO)

, если вы измените на 2.049999f , ожидается результат.

2
ответ дан 6 December 2019 в 23:00
поделиться
Другие вопросы по тегам:

Похожие вопросы: