Операции с плавающей запятой С#/.NET отличаются точностью между режимами отладки и режима выпуска?
Float/double precision в режимах отладки/выпуска
Ответ 1
Они действительно могут быть разными. В соответствии со спецификацией CLR ECMA:
Место хранения для плавающих точек числа (статика, элементы массива и поля классов) имеют фиксированный размер. Поддерживаемые размеры хранилища float32 и float64. Где-либо еще (в стеке оценки, как аргументы, как типы возврата, так и как локальные переменные) с плавающей запятой числа представлены с использованием внутренний тип с плавающей запятой. В каждом такой пример, номинальный тип переменная или выражение являются либо R4, либо R8, но его значение может быть представлено внутренне с дополнительным диапазоном и/или точность. Размер внутреннее представление с плавающей запятой зависит от реализации, может варьироваться, и должны иметь точность, по крайней мере, как велика, как переменная или представленное выражение. неявное расширение преобразования в внутреннее представление от float32 или float64 выполняется, когда эти типы загружаются из хранилища. внутреннее представление обычно собственный размер для оборудования или как требуется для эффективного выполнение операции.
В основном это означает, что следующее сравнение может быть или не быть равным:
class Foo
{
double _v = ...;
void Bar()
{
double v = _v;
if( v == _v )
{
// Code may or may not execute here.
// _v is 64-bit.
// v could be either 64-bit (debug) or 80-bit (release) or something else (future?).
}
}
}
Сообщение о возврате: никогда не проверяйте плавающие значения для равенства.
Ответ 2
Это интересный вопрос, поэтому я немного экспериментировал. Я использовал этот код:
static void Main (string [] args)
{
float
a = float.MaxValue / 3.0f,
b = a * a;
if (a * a < b)
{
Console.WriteLine ("Less");
}
else
{
Console.WriteLine ("GreaterEqual");
}
}
с использованием DevStudio 2005 и .Net 2. Я скомпилировал как отладку, так и выпуск и рассмотрел вывод компилятора:
Release Debug
static void Main (string [] args) static void Main (string [] args)
{ {
00000000 push ebp
00000001 mov ebp,esp
00000003 push edi
00000004 push esi
00000005 push ebx
00000006 sub esp,3Ch
00000009 xor eax,eax
0000000b mov dword ptr [ebp-10h],eax
0000000e xor eax,eax
00000010 mov dword ptr [ebp-1Ch],eax
00000013 mov dword ptr [ebp-3Ch],ecx
00000016 cmp dword ptr ds:[00A2853Ch],0
0000001d je 00000024
0000001f call 793B716F
00000024 fldz
00000026 fstp dword ptr [ebp-40h]
00000029 fldz
0000002b fstp dword ptr [ebp-44h]
0000002e xor esi,esi
00000030 nop
float float
a = float.MaxValue / 3.0f, a = float.MaxValue / 3.0f,
00000000 sub esp,0Ch 00000031 mov dword ptr [ebp-40h],7EAAAAAAh
00000003 mov dword ptr [esp],ecx
00000006 cmp dword ptr ds:[00A2853Ch],0
0000000d je 00000014
0000000f call 793B716F
00000014 fldz
00000016 fstp dword ptr [esp+4]
0000001a fldz
0000001c fstp dword ptr [esp+8]
00000020 mov dword ptr [esp+4],7EAAAAAAh
b = a * a; b = a * a;
00000028 fld dword ptr [esp+4] 00000038 fld dword ptr [ebp-40h]
0000002c fmul st,st(0) 0000003b fmul st,st(0)
0000002e fstp dword ptr [esp+8] 0000003d fstp dword ptr [ebp-44h]
if (a * a < b) if (a * a < b)
00000032 fld dword ptr [esp+4] 00000040 fld dword ptr [ebp-40h]
00000036 fmul st,st(0) 00000043 fmul st,st(0)
00000038 fld dword ptr [esp+8] 00000045 fld dword ptr [ebp-44h]
0000003c fcomip st,st(1) 00000048 fcomip st,st(1)
0000003e fstp st(0) 0000004a fstp st(0)
00000040 jp 00000054 0000004c jp 00000052
00000042 jbe 00000054 0000004e ja 00000056
00000050 jmp 00000052
00000052 xor eax,eax
00000054 jmp 0000005B
00000056 mov eax,1
0000005b test eax,eax
0000005d sete al
00000060 movzx eax,al
00000063 mov esi,eax
00000065 test esi,esi
00000067 jne 0000007A
{ {
Console.WriteLine ("Less"); 00000069 nop
00000044 mov ecx,dword ptr ds:[0239307Ch] Console.WriteLine ("Less");
0000004a call 78678B7C 0000006a mov ecx,dword ptr ds:[0239307Ch]
0000004f nop 00000070 call 78678B7C
00000050 add esp,0Ch 00000075 nop
00000053 ret }
} 00000076 nop
else 00000077 nop
{ 00000078 jmp 00000088
Console.WriteLine ("GreaterEqual"); else
00000054 mov ecx,dword ptr ds:[02393080h] {
0000005a call 78678B7C 0000007a nop
} Console.WriteLine ("GreaterEqual");
} 0000007b mov ecx,dword ptr ds:[02393080h]
00000081 call 78678B7C
00000086 nop
}
В приведенном выше примере показано, что код с плавающей точкой является одинаковым как для отладки, так и для выпуска, компилятор выбирает согласованность над оптимизацией. Хотя программа производит неверный результат (a * a не менее b), это то же самое независимо от режима отладки/выпуска.
Теперь, Intel IA32 FPU имеет восемь регистров с плавающей запятой, вы можете подумать, что компилятор будет использовать регистры для хранения значений при оптимизации, а не записи в память, что улучшит производительность, что-то вроде:
fld dword ptr [a] ; precomputed value stored in ram == float.MaxValue / 3.0f
fmul st,st(0) ; b = a * a
; no store to ram, keep b in FPU
fld dword ptr [a]
fmul st,st(0)
fcomi st,st(0) ; a*a compared to b
но это будет отличаться от версии отладки (в этом случае отобразить правильный результат). Однако изменение поведения программы в зависимости от параметров сборки очень плохо.
Код FPU - это одна из областей, в которой обработка кода вручную может значительно ускорить выполнение компилятора, но вам нужно завести голову вокруг того, как работает FPU.
Ответ 3
Фактически, они могут отличаться, если режим отладки использует FPU x87, а режим деблокирования использует SSE для float-ops.
Ответ 4
Спасибо, ребята, я нашел пару статей, в которых говорится, что в поведении поплавка будет отличаться в режиме выпуска
http://blogs.msdn.com/davidnotario/archive/2005/08/08/449092.aspx
Ответ 5
Они должны быть одинаковыми. Числа с плавающей запятой основаны на стандарте IEEE_754.
Ответ 6
В ответ на запрос Фрэнка Крюгера выше (в комментариях) для демонстрации разницы:
Скомпилируйте этот код в gcc без оптимизаций и -mfpmath = 387 (у меня нет причин думать, что он не будет работать на других компиляторах, но я его не пробовал). Затем скомпилируйте его без оптимизаций и -msse -mfpmath = sse.
Выход будет отличаться.
#include <stdio.h>
int main()
{
float e = 0.000000001;
float f[3] = {33810340466158.90625,276553805316035.1875,10413022032824338432.0};
f[0] = pow(f[0],2-e); f[1] = pow(f[1],2+e); f[2] = pow(f[2],-2-e);
printf("%s\n",f);
return 0;
}
Ответ 7
Вот простой пример, где результаты отличаются не только между режимами отладки и выпуска, но и тем, как они это делают, зависит от того, использует ли x86 или x84 в качестве платформы:
Single f1 = 0.00000000002f;
Single f2 = 1 / f1;
Double d = f2;
Console.WriteLine(d);
Это пишет следующие результаты:
Debug Release
x86 49999998976 50000000199,7901
x64 49999998976 49999998976
Беглый взгляд на разборку (Отладка → Windows → Разборка в Visual Studio) дает некоторые подсказки о том, что здесь происходит. Для случая x86:
Debug Release
mov dword ptr [ebp-40h],2DAFEBFFh | mov dword ptr [ebp-4],2DAFEBFFh
fld dword ptr [ebp-40h] | fld dword ptr [ebp-4]
fld1 | fld1
fdivrp st(1),st | fdivrp st(1),st
fstp dword ptr [ebp-44h] |
fld dword ptr [ebp-44h] |
fstp qword ptr [ebp-4Ch] |
fld qword ptr [ebp-4Ch] |
sub esp,8 | sub esp,8
fstp qword ptr [esp] | fstp qword ptr [esp]
call 6B9783BC | call 6B9783BC
В частности, мы видим, что группа, казалось бы, избыточных "сохранить значение из регистра с плавающей запятой в памяти, а затем немедленно загрузить его обратно из памяти в регистр с плавающей запятой" была оптимизирована в режиме освобождения. Тем не менее, две инструкции
fstp dword ptr [ebp-44h]
fld dword ptr [ebp-44h]
достаточно изменить значение в регистре x87 с +5.0000000199790138e + 0010 на +4.9999998976000000e + 0010, поскольку это можно проверить, выполнив разборку и изучив значения соответствующих регистров (Debug → Windows → Регистрируется, затем правой кнопкой мыши и проверяется "Плавающая точка").
История для x64 совершенно другая. Мы все еще видим ту же оптимизацию, удаляющую несколько инструкций, но на этот раз все зависит от SSE с его 128-битными регистрами и выделенным набором команд:
Debug Release
vmovss xmm0,dword ptr [7FF7D0E104F8h] | vmovss xmm0,dword ptr [7FF7D0E304C8h]
vmovss dword ptr [rbp+34h],xmm0 | vmovss dword ptr [rbp-4],xmm0
vmovss xmm0,dword ptr [7FF7D0E104FCh] | vmovss xmm0,dword ptr [7FF7D0E304CCh]
vdivss xmm0,xmm0,dword ptr [rbp+34h] | vdivss xmm0,xmm0,dword ptr [rbp-4]
vmovss dword ptr [rbp+30h],xmm0 |
vcvtss2sd xmm0,xmm0,dword ptr [rbp+30h] | vcvtss2sd xmm0,xmm0,xmm0
vmovsd qword ptr [rbp+28h],xmm0 |
vmovsd xmm0,qword ptr [rbp+28h] |
call 00007FF81C9343F0 | call 00007FF81C9343F0
Здесь, поскольку модуль SSE избегает использования более высокой точности, чем внутренняя точность одинарной точности (в то время как модуль x87 делает это), мы получаем результат "одинарной точности с точностью до точки" для случая x86 независимо от оптимизаций. Действительно, можно обнаружить (после включения регистров SSE в обзоре регистров Visual Studio), что после vdivss
XMM0 содержит 0000000000000000-00000000513A43B7, что в точности соответствует 49999998976 от предыдущего.
Оба несоответствия укусили меня на практике. Помимо иллюстрации того, что никогда не следует сравнивать равенство чисел с плавающей запятой, пример также показывает, что все еще есть место для отладки сборки на языке высокого уровня, таком как С#, в тот момент, когда появляются числа с плавающей запятой.