Я заметил интересное поведение с плаванием, округляющимся / усечение компилятором 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 удалена или даже уменьшена, все "падает на месте".
Ваш вопрос можно упростить, задав вопрос, почему эти два результата различаются:
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);
В комментарии вы спросили
Отличаются ли эти правила?
Да. Или, скорее, правила допускают другое поведение.
И если да, то должен ли я знать об этом из справочной документации по языку C# или MSDN, или это просто случайное расхождение между компилятором и средой выполнения
Это подразумевается спецификацией. Операции с плавающей точкой имеют определенный минимальный уровень точности, который должен быть соблюден, но компилятору или среде выполнения разрешается использовать большую точность, если он считает нужным. Это может привести к большим, заметным изменениям, когда вы выполняете операции, которые увеличивают небольшие изменения. Округление, например, может превратить чрезвычайно малое изменение в чрезвычайно большое.
Этот факт приводит к довольно часто задаваемым здесь вопросам. Для получения некоторой информации об этой ситуации и других ситуациях, которые могут привести к подобным расхождениям, см. следующее:
Почему это вычисление с плавающей точкой дает разные результаты на разных машинах?
C# XNA Visual Studio: Разница между режимами "release" и "debug"?
Марк прав насчет компилятора.Теперь давайте обманем компилятор:
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
, ожидается результат.