gcc '-m32' опция променя закръгляването на плаваща запетая, когато не се изпълнява valgrind

Получавам различни закръгляния с плаваща запетая при различни сценарии за изграждане/изпълнение. Обърнете внимание на 2498 във второто изпълнение по-долу...

   #include <iostream>
   #include <cassert>
   #include <typeinfo>
   using std::cerr;

void domath( int n, double c, double & q1, double & q2 )
   {
   q1=n*c;
   q2=int(n*c);
   }

int main()
   {
   int n=2550;
   double c=0.98, q1, q2;
   domath( n, c, q1, q2 );
   cerr<<"sizeof(int)="<<sizeof(int)<<", sizeof(double)="<<sizeof(double)<<", sizeof(n*c)="<<sizeof(n*c)<<"\n";
   cerr<<"n="<<n<<", int(q1)="<<int(q1)<<", int(q2)="<<int(q2)<<"\n";
   assert( typeid(q1) == typeid(n*c) );
   }

Работи като 64-битов изпълним файл...

$ g++ -m64 -Wall rounding_test.cpp -o rounding_test && ./rounding_test
sizeof(int)=4, sizeof(double)=8, sizeof(n*c)=8
n=2550, int(q1)=2499, int(q2)=2499

Работи като 32-битов изпълним файл...

$ g++ -m32 -Wall rounding_test.cpp -o rounding_test && ./rounding_test
sizeof(int)=4, sizeof(double)=8, sizeof(n*c)=8
n=2550, int(q1)=2499, int(q2)=2498

Работи като 32-битов изпълним файл под valgrind...

$ g++ -m32 -Wall rounding_test.cpp -o rounding_test && valgrind --quiet ./rounding_test
sizeof(int)=4, sizeof(double)=8, sizeof(n*c)=8
n=2550, int(q1)=2499, int(q2)=2499

Защо виждам различни резултати при компилиране с -m32 и защо резултатите отново са различни при стартиране на valgrind?

Моята система е Ubuntu 14.04.1 LTS x86_64, а gcc е версия 4.8.2.


РЕДАКТИРАНЕ:

В отговор на искането за разглобяване преработих малко кода, за да мога да изолирам съответната част. Подходът, възприет между -m64 и -m32, очевидно е много по-различен. Не съм много загрижен защо те дават различен резултат от закръгляването, тъй като мога да поправя това, като приложа функцията round(). Най-интересният въпрос е: защо valgrind променя резултата?

rounding_test:     file format elf64-x86-64 
                                  <
000000000040090d <_Z6domathidRdS_>:               <
  40090d:   55                      push   %rbp       <
  40090e:   48 89 e5                mov    %rsp,%rbp      <
  400911:   89 7d fc                mov    %edi,-0x4(%rbp <
  400914:   f2 0f 11 45 f0          movsd  %xmm0,-0x10(%r <
  400919:   48 89 75 e8             mov    %rsi,-0x18(%rb <
  40091d:   48 89 55 e0             mov    %rdx,-0x20(%rb <
  400921:   f2 0f 2a 45 fc          cvtsi2sdl -0x4(%rbp), <
  400926:   f2 0f 59 45 f0          mulsd  -0x10(%rbp),%x <
  40092b:   48 8b 45 e8             mov    -0x18(%rbp),%r <
  40092f:   f2 0f 11 00             movsd  %xmm0,(%rax)   <
  400933:   f2 0f 2a 45 fc          cvtsi2sdl -0x4(%rbp), <
  400938:   f2 0f 59 45 f0          mulsd  -0x10(%rbp),%x <
  40093d:   f2 0f 2c c0             cvttsd2si %xmm0,%eax  <
  400941:   f2 0f 2a c0             cvtsi2sd %eax,%xmm0   <
  400945:   48 8b 45 e0             mov    -0x20(%rbp),%r <
  400949:   f2 0f 11 00             movsd  %xmm0,(%rax)   <
  40094d:   5d                      pop    %rbp       <
  40094e:   c3                      retq              <

      | rounding_test:     file format elf32-i386

                                  > 0804871d <_Z6domathidRdS_>:
                                  >  804871d:   55                      push   %ebp
                                  >  804871e:   89 e5                   mov    %esp,%ebp
                                  >  8048720:   83 ec 10                sub    $0x10,%esp
                                  >  8048723:   8b 45 0c                mov    0xc(%ebp),%eax
                                  >  8048726:   89 45 f8                mov    %eax,-0x8(%ebp
                                  >  8048729:   8b 45 10                mov    0x10(%ebp),%ea
                                  >  804872c:   89 45 fc                mov    %eax,-0x4(%ebp
                                  >  804872f:   db 45 08                fildl  0x8(%ebp)
                                  >  8048732:   dc 4d f8                fmull  -0x8(%ebp)
                                  >  8048735:   8b 45 14                mov    0x14(%ebp),%ea
                                  >  8048738:   dd 18                   fstpl  (%eax)
                                  >  804873a:   db 45 08                fildl  0x8(%ebp)
                                  >  804873d:   dc 4d f8                fmull  -0x8(%ebp)
                                  >  8048740:   d9 7d f6                fnstcw -0xa(%ebp)
                                  >  8048743:   0f b7 45 f6             movzwl -0xa(%ebp),%ea
                                  >  8048747:   b4 0c                   mov    $0xc,%ah
                                  >  8048749:   66 89 45 f4             mov    %ax,-0xc(%ebp)
                                  >  804874d:   d9 6d f4                fldcw  -0xc(%ebp)
                                  >  8048750:   db 5d f0                fistpl -0x10(%ebp)
                                  >  8048753:   d9 6d f6                fldcw  -0xa(%ebp)
                                  >  8048756:   8b 45 f0                mov    -0x10(%ebp),%e
                                  >  8048759:   89 45 f0                mov    %eax,-0x10(%eb
                                  >  804875c:   db 45 f0                fildl  -0x10(%ebp)
                                  >  804875f:   8b 45 18                mov    0x18(%ebp),%ea
                                  >  8048762:   dd 18                   fstpl  (%eax)
                                  >  8048764:   c9                      leave  
                                  >  8048765:   c3                      ret    

person Brent Bradburn    schedule 12.08.2015    source източник
comment
Предположение: може да използва SSE/SSE2 за скаларна плаваща запетая в 64-битов режим (тъй като поддръжката на x86_64 предполага поддръжка до SSE2) и да използва обикновена x87 с плаваща запетая в 32-битов режим.   -  person Jason R    schedule 12.08.2015
comment
Моля, качете разглобяването на функциите ... и опитайте да компилирате с -O0, предполагам, че това е проблем с оптимизацията, а по-скоро неща, свързани с целевата архитектура.   -  person 0x90    schedule 12.08.2015
comment
С -m32 опитайте: -msse2 -mfpmath=sse   -  person Brett Hale    schedule 12.08.2015
comment
@BrettHale: Добро предложение. Правенето на това води до съвпадение на генерирания сбор (и резултатите) с този на -m64.   -  person Brent Bradburn    schedule 13.08.2015


Отговори (2)


Редактиране: Изглежда, че поне преди много време изчисленията на valgrind с плаваща запетая не са толкова точни, колкото „реалните“ изчисления. С други думи, това МОЖЕ да обясни защо получавате различни резултати. Вижте този въпрос и отговор в пощата на valgrind списък.

Редактиране2: И текущата документация на valgrind.org го има в раздела „ограничения на ядрото“ тук - така че бих очаквал, че наистина е "все още валиден". С други думи, документацията за valgrind казва да се очакват разлики между valgrind и x87 FPU изчисленията. "Предупреден си!" (И както можем да видим, използването на sse инструкции за извършване на същата математика дава същия резултат като valgrind, потвърждавайки, че това е разлика от „закръгляване от 80 бита до 64 бита“)

Изчисленията с плаваща запетая ЩЕ се различават леко в зависимост от това как точно се извършва изчислението. Не съм сигурен точно на какво искате да получите отговор, така че ето един дълъг разхвърлян „някакъв отговор“.

Valgrind наистина променя точното поведение на вашата програма по различни начини (емулира определени инструкции, вместо действително да изпълнява истинските инструкции - което може да включва запазване на междинните резултати от изчисленията). Освен това е добре известно, че изчисленията с плаваща запетая "не са точни" - въпрос на късмет/лош късмет е дали изчислението се окаже точно или не. 0,98 е едно от многото, много числа, които не могат да бъдат описани точно във формат с плаваща запетая [поне не в обикновените IEEE формати].

Добавяйки:

cerr<<"c="<<std::setprecision(30)<<c <<"\n";

виждаме, че изходът е c=0.979999999999999982236431605997 (да, действителната стойност е 0,979999...99982 или нещо такова, оставащите цифри са само остатъчната стойност, тъй като това не е „четно“ двоично число, винаги ще има нещо останало.

Това е частта n = 2550;, c = 0.98 и q = n * c от кода, генериран от gcc:

movl    $2550, -28(%ebp)       ; n
fldl    .LC0
fstpl   -40(%ebp)              ; c
fildl   -28(%ebp)
fmull   -40(%ebp)
fstpl   -48(%ebp)              ; q - note that this is stored as a rouned 64-bit value.

Това е частта int(q) и int(n*c) от кода:

fildl   -28(%ebp)             ; n
fmull   -40(%ebp)             ; c 
fnstcw  -58(%ebp)             ; Save control word
movzwl  -58(%ebp), %eax
movb    $12, %ah
movw    %ax, -60(%ebp)        ; Save float control word.
fldcw   -60(%ebp)
fistpl  -64(%ebp)             ; Store as integer (directly from 80-bit result)
fldcw   -58(%ebp)             ; restore float control word.
movl    -64(%ebp), %ebx       ; result of int(n * c)


fldl    -48(%ebp)             ; q
fldcw   -60(%ebp)             ; Load float control word as saved above.
fistpl  -64(%ebp)             ; Store as integer.
fldcw   -58(%ebp)             ; Restore control word.
movl    -64(%ebp), %esi       ; result of int(q)

Сега, ако междинният резултат е съхранен (и по този начин закръглен) от вътрешната 80-битова точност в средата на едно от тези изчисления, резултатът може да е леко различен от резултата, ако изчислението се случи без запазване на междинни стойности.

Получавам идентични резултати както от g++ 4.9.2, така и от clang++ -mno-sse - но ако активирам sse в случая clang, той дава същия резултат като 64-битовата компилация. Използването на gcc -msse2 -m32 дава отговора 2499 навсякъде. Това показва, че грешката е причинена от „съхраняване на междинни резултати“ по един или друг начин.

По същия начин, оптимизирането в gcc до -O1 ще даде 2499 на всички места - но това е съвпадение, а не резултат от някакво "умно мислене". Ако искате правилно закръглени целочислени стойности на изчисленията си, много по-добре е да се закръглите сами, защото рано или късно int(someDoubleValue) ще излезе "едно малко".

Edit3: И накрая, използването на g++ -mno-sse -m64 също ще доведе до същия отговор 2498, където използването на valgrind на същия двоичен файл произвежда отговора 2499.

person Mats Petersson    schedule 12.08.2015

32-битовата версия използва инструкции X87 с плаваща запетая. X87 вътрешно използва 80-битови числа с плаваща запетая, което ще създаде проблеми, когато числата се преобразуват към и от други точности. Във вашия случай приближението на 64-битовата точност за 0,98 е малко по-малко от истинската стойност. Когато процесорът го преобразува в 80-битова стойност, вие получавате абсолютно същата числена стойност, което е също толкова лошо приближение - наличието на повече битове не ви дава по-добро приближение. След това FPU умножава това число по 2550 и получава цифра, която е малко по-малка от 2499. Ако процесорът използва 64-битови числа докрай, той трябва да изчисли точно 2499, както прави в 64-битовата версия.

person Joni    schedule 12.08.2015