Плавающая / двойная точность в режимах отладки / выпуска - PullRequest
12 голосов
/ 18 сентября 2008

Отличаются ли операции с плавающей запятой в C # / .NET точностью между режимом отладки и режимом выпуска?

Ответы [ 5 ]

22 голосов
/ 18 сентября 2008

Они действительно могут быть разными. Согласно спецификации 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?).
    }
  }
}

Сообщение возврата домой: никогда не проверяйте плавающие значения на равенство.

11 голосов
/ 18 сентября 2008

Это интересный вопрос, поэтому я немного поэкспериментировал. Я использовал этот код:

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 FP32 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.

2 голосов
/ 18 сентября 2008

На самом деле они могут отличаться, если в режиме отладки используется FPU x87, а в режиме выпуска используется SSE для операций с плавающей запятой.

1 голос
/ 27 марта 2019

Вот простой пример, когда результаты отличаются не только между режимами отладки и выпуска, но и тем, как они это делают, зависит от того, какой платформой используется 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 #, в тот момент, когда появляются числа с плавающей запятой.

1 голос
/ 18 сентября 2008

В ответ на запрос Фрэнка Крюгера выше (в комментариях) о демонстрации разницы:

Скомпилируйте этот код в 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;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...