Вот простой пример, когда результаты отличаются не только между режимами отладки и выпуска, но и тем, как они это делают, зависит от того, какой платформой используется 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
Беглый взгляд на разборку (Debug -> Windows -> Disassembly в 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 -> Registers , затем щелкните правой кнопкой мыши и выберите «Плавающая точка»).
История для 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, установленным ранее.
Оба несоответствия укусили меня на практике. Помимо иллюстрации того, что никогда не следует сравнивать равенство чисел с плавающей запятой, пример также показывает, что все еще есть место для отладки сборки на языке высокого уровня, таком как C #, в тот момент, когда появляются числа с плавающей запятой.