Я писал поучительный пример для коллеги, чтобы показать ему, почему тестирование плаваний для равенства часто является плохой идеей. Пример, с которым я пошел, добавлял.1 десять раз и выдерживал сравнение с 1,0 (тот, который мне показали в моем вводном числовом классе). Я был удивлен найти, что два результата были равны (код + вывод).
float @float = 0.0f;
for(int @int = 0; @int < 10; @int += 1)
{
@float += 0.1f;
}
Console.WriteLine(@float == 1.0f);
Некоторое расследование показало, что на этот результат нельзя было положиться (во многом как равенство плавающее). Тот, который я нашел самым удивительным, был то, что, добавляя код после того, как другой код мог изменить результат вычисления (код + вывод). Обратите внимание, что этот пример имеет точно тот же код и IL с еще одной строкой добавленного C#.
float @float = 0.0f;
for(int @int = 0; @int < 10; @int += 1)
{
@float += 0.1f;
}
Console.WriteLine(@float == 1.0f);
Console.WriteLine(@float.ToString("G9"));
Я знаю, что я, как предполагается, не использую равенство на плаваниях и таким образом не должен заботиться слишком много об этом, но я нашел, что это было довольно удивительно, как имело обо всех, кому я показал это. Выполнение материала после выполнения вычисления, изменяет значение предыдущего вычисления? Я не думаю, что это - модель людей вычисления, обычно знают.
Я не полностью озадачен, кажется безопасным предположить, что существует некоторая оптимизация, происходящая в "равном" случае, который изменяется, результат вычисления (создающий в режиме отладки предотвращает "равный" случай). По-видимому, от оптимизации отказываются, когда CLR находит, что это должно будет позже упаковать плавание.
Я искал немного, но не мог найти причину этого поведения. Может кто-либо подсказка меня в?
Это побочный эффект работы оптимизатора JIT. Он выполняет больше работы, если нужно сгенерировать меньше кода. Цикл в исходном фрагменте кода компилируется так:
@float += 0.1f;
0000000f fld dword ptr ds:[0025156Ch] ; push(intermediate), st0 = 0.1
00000015 faddp st(1),st ; st0 = st0 + st1
for (int @int = 0; @int < 10; @int += 1) {
00000017 inc eax
00000018 cmp eax,0Ah
0000001b jl 0000000F
Когда вы добавляете дополнительный оператор Console.WriteLine (), он компилирует его так:
@float += 0.1f;
00000011 fld dword ptr ds:[00961594h] ; st0 = 0.1
00000017 fadd dword ptr [ebp-8] ; st0 = st0 + @float
0000001a fstp dword ptr [ebp-8] ; @float = st0
for (int @int = 0; @int < 10; @int += 1) {
0000001d inc eax
0000001e cmp eax,0Ah
00000021 jl 00000011
Обратите внимание на разницу в адресе 15 и адресе 17 + 1a, первом цикле сохраняет промежуточный результат в FPU. Второй цикл сохраняет его обратно в локальную переменную @float. Пока он остается внутри FPU, результат вычисляется с полной точностью. Однако при его сохранении промежуточный результат обрезается до числа с плавающей запятой, теряя много бит точности в процессе.
Хотя это неприятно, я не верю, что это ошибка. Компилятор x64 JIT пока ведет себя иначе. Вы можете изложить свои аргументы на сайте connect.microsoft.com
Вы запускали это на Процессор Intel?
Согласно одной из теорий, JIT позволил @float
полностью накапливаться в регистре с плавающей запятой, что обеспечивало бы полную точность 80 бит. Таким образом, расчет может быть достаточно точным.
Вторая версия кода не помещалась в регистры полностью, поэтому @float
пришлось «пролить» в память, что привело к округлению 80-битного значения до одинарной точности, давая результаты, ожидаемые от арифметики одинарной точности.
Но это всего лишь очень случайное предположение. Нужно было бы проверить фактический машинный код, сгенерированный JIT-компилятором (отладка с открытым представлением дизассемблирования).
Редактировать:
Хм ... Я тестировал ваш код локально (Intel Core 2, Windows 7 x64, 64-битная среда CLR) и всегда получал "ожидаемую" ошибку округления. Как в выпуске, так и в отладочной конфигурации.
Ниже приведена дизассемблированная версия, отображаемая Visual Studio для первого фрагмента кода на моем компьютере:
xorps xmm0,xmm0
movss dword ptr [rsp+20h],xmm0
for (int @int = 0; @int < 10; @int += 1)
mov dword ptr [rsp+24h],0
jmp 0000000000000061
{
@float += 0.1f;
movss xmm0,dword ptr [000000A0h]
addss xmm0,dword ptr [rsp+20h]
movss dword ptr [rsp+20h],xmm0 // <-- @float gets stored in memory
for (int @int = 0; @int < 10; @int += 1)
mov eax,dword ptr [rsp+24h]
add eax,1
mov dword ptr [rsp+24h],eax
cmp dword ptr [rsp+24h],0Ah
jl 0000000000000042
}
Console.WriteLine(@float == 1.0f);
etc.
Есть различий между JIT-компиляторами x64 и x86, но у меня нет доступа к 32-битная машина.
Моя теория, что без строки ToString компилятор может статически оптимизировать функцию до одного значения и что он каким-то образом компенсирует ошибку с плавающей запятой. Но когда добавляется строка ToString, оптимизатор должен обрабатывать float по-другому, потому что это требуется для вызова метода. Это просто предположение.
К вашему сведению, спецификация C # отмечает, что такое поведение является законным и распространенным. См. Эти вопросы для получения дополнительных сведений и аналогичных сценариев: