Странное поведение компилятора с использованием литералов с плавающей точкой и переменных с плавающей точкой - PullRequest
7 голосов
/ 22 июня 2010

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

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 

Наконец, мой вопрос: почему вторая строка вывода отличается от четвертой?Имеет ли это дополнительный смысл такое различие?Также обратите внимание, что если последняя (уже непредставленная) цифра из числа с плавающей точкой f будет удалена или даже уменьшена, все «встанет на свои места».

Ответы [ 3 ]

5 голосов
/ 22 июня 2010

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

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);
4 голосов
/ 22 июня 2010

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

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

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

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

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

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

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

C # XNA Visual Studio: разница между режимами «релиз» и «отладка»?

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

https://stackoverflow.com/questions/2494724

2 голосов
/ 22 июня 2010

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

    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 цифр, поэтому компилятор округлил его, потому что не смог сохранить. (должен дать предупреждение ИМО)

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

...