Параметр 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. список.

Edit2: В текущей документации 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