Разница в приведении типа float к int, 32-битный C - PullRequest
0 голосов
/ 26 февраля 2019

Я сейчас работаю со старым кодом, который должен работать на 32-битной системе.Во время этой работы я наткнулся на проблему, из-за которой (из академического интереса) я хотел бы понять причину.

Кажется, что приведение от float к int в 32-битном C ведет себя иначе, если приведение выполненопо переменной или по выражению.Рассмотрим программу:

#include <stdio.h>
int main() {
   int i,c1,c2;
   float f1,f10;
   for (i=0; i< 21; i++)  {
      f1 = 3+i*0.1;
      f10 = f1*10.0;
      c1 = (int)f10;
      c2 = (int)(f1*10.0);
      printf("%d, %d, %d, %11.9f, %11.9f\n",c1,c2,c1-c2,f10,f1*10.0);
   }
}

Скомпилировано (с использованием gcc) либо непосредственно в 32-битной системе, либо в 64-битной системе с использованием модификатора -m32. Вывод программы:

30, 30, 0, 30.000000000 30.000000000
31, 30, 1, 31.000000000 30.999999046
32, 32, 0, 32.000000000 32.000000477
33, 32, 1, 33.000000000 32.999999523
34, 34, 0, 34.000000000 34.000000954
35, 35, 0, 35.000000000 35.000000000
36, 35, 1, 36.000000000 35.999999046
37, 37, 0, 37.000000000 37.000000477
38, 37, 1, 38.000000000 37.999999523
39, 39, 0, 39.000000000 39.000000954
40, 40, 0, 40.000000000 40.000000000
41, 40, 1, 41.000000000 40.999999046
42, 41, 1, 42.000000000 41.999998093
43, 43, 0, 43.000000000 43.000001907
44, 44, 0, 44.000000000 44.000000954
45, 45, 0, 45.000000000 45.000000000
46, 45, 1, 46.000000000 45.999999046
47, 46, 1, 47.000000000 46.999998093
48, 48, 0, 48.000000000 48.000001907
49, 49, 0, 49.000000000 49.000000954
50, 50, 0, 50.000000000 50.000000000 

Следовательно, очевидно, что существует различие между приведением переменной и выражения.Обратите внимание, что проблема существует также, если float изменяется на double и / или int изменяется на short или long, также проблема не проявляется, если программа скомпилирована как 64-битная.

Чтобы прояснить, проблема, которую я пытаюсь понять, заключается не в арифметике / округлении с плавающей точкой, а скорее в различиях в обработке памяти в 32-разрядной системе.

Проблема была проверенаon:

  • универсальная версия Linux 4.15.0-45 (buildd @ lgw01-amd64-031) (версия gcc 7.3.0 (Ubuntu 7.3.0-16ubuntu3)), программа скомпилированаиспользование: gcc -m32 Cast32int.c

  • версия Linux 2.4.20-8 (bhcompile@porky.devel.redhat.com) (версия gcc 3.2.2 20030222 (Red Hat Linux 3.2).2-5)), программа скомпилирована с использованием: gcc Cast32int.c

Любые указатели, которые помогут мне понять, что здесь происходит, приветствуются.

Ответы [ 4 ]

0 голосов
/ 27 февраля 2019

Основная причина в том, что значения the rounding-control (RC) field of the x87 FPU control register не соответствуют в следующих двух строках.в конечном итоге значения c1 и c2 различаются.

0x08048457 <+58>:    fstps  0x44(%esp)
0x0804848b <+110>:   fistpl 0x3c(%esp)

Добавьте опцию компиляции gcc -mfpmath=387 -mno-sse, ее можно воспроизвести (даже без -m32 или изменить число с плавающей точкой на double)
Как это:

gcc -otest test.c -g -mfpmath=387 -mno-sse -m32

Затем используйте gdb для отладки, точку останова на 0x0804845b и выполните для i = 1

    0x08048457 <+58>:    fstps  0x44(%esp)
    0x0804845b <+62>:    flds   0x44(%esp)

    (gdb) info float
    =>R7: Valid   0x4003f7ffff8000000000 +30.99999904632568359      
      R6: Empty   0x4002a000000000000000
      R5: Empty   0x00000000000000000000
      R4: Empty   0x00000000000000000000
      R3: Empty   0x00000000000000000000
      R2: Empty   0x00000000000000000000
      R1: Empty   0x00000000000000000000
      R0: Empty   0x00000000000000000000

    Status Word:         0x3820                  PE                        
                           TOP: 7
    Control Word:        0x037f   IM DM ZM OM UM PM
                           PC: Extended Precision (64-bits)
                           RC: Round to nearest
    Tag Word:            0x3fff
    Instruction Pointer: 0x00:0x08048455
    Operand Pointer:     0x00:0x00000000
    Opcode:              0x0000

    (gdb) x /xw 0x44+$esp
    0xffffb594:     0x41f80000 ==> 31.0, s=0, M=1.1111 E=4

наблюдение за результатами выполнения fstps,
в это время,Значение RC в регистре управления на fpu равно Округление до ближайшего .
значение в регистре fpu равно 30.99999904632568359 (80 бит).
значение 0x44 (% esp) (variable "f10") составляет 31,0.(округление до ближайшего)

Затем используйте gdb для отладки, точку останова на 0x0804848b и установите в это время i = 1

    0x0804848b <+110>:   fistpl 0x3c(%esp)

    (gdb) info float
    =>R7: Valid   0x4003f7ffff8000000000 +30.99999904632568359      
      R6: Empty   0x4002a000000000000000
      R5: Empty   0x00000000000000000000
      R4: Empty   0x00000000000000000000
      R3: Empty   0x00000000000000000000
      R2: Empty   0x00000000000000000000
      R1: Empty   0x00000000000000000000
      R0: Empty   0x00000000000000000000

    Status Word:         0x3820                  PE                        
                           TOP: 7
    Control Word:        0x0c7f   IM DM ZM OM UM PM
                           PC: Single Precision (24-bits)
                           RC: Round toward zero
    Tag Word:            0x3fff
    Instruction Pointer: 0x00:0x08048485
    Operand Pointer:     0x00:0x00000000
    Opcode:              0x0000

, значение RC в регистре управленияна fpu - округление до нуля .
значение в регистре fpu - 30.99999904632568359 (80 бит).значение то же, что и выше
, очевидно, когда целое число преобразуется, десятичная точка усекается, а значение равно 30.

Ниже приведен main декомпилированный код

    (gdb) disas main
    Dump of assembler code for function main:
       0x0804841d <+0>:     push   %ebp
       0x0804841e <+1>:     mov    %esp,%ebp
       0x08048420 <+3>:     and    $0xfffffff0,%esp
       0x08048423 <+6>:     sub    $0x50,%esp
       0x08048426 <+9>:     movl   $0x0,0x4c(%esp)
       0x0804842e <+17>:    jmp    0x80484de <main+193>
       0x08048433 <+22>:    fildl  0x4c(%esp)
       0x08048437 <+26>:    fldl   0x80485a8
       0x0804843d <+32>:    fmulp  %st,%st(1)
       0x0804843f <+34>:    fldl   0x80485b0
       0x08048445 <+40>:    faddp  %st,%st(1)
       0x08048447 <+42>:    fstps  0x48(%esp)
       0x0804844b <+46>:    flds   0x48(%esp)
       0x0804844f <+50>:    flds   0x80485b8
       0x08048455 <+56>:    fmulp  %st,%st(1)
       0x08048457 <+58>:    fstps  0x44(%esp)        // store to f10
       0x0804845b <+62>:    flds   0x44(%esp)
       0x0804845f <+66>:    fnstcw 0x2a(%esp)
       0x08048463 <+70>:    movzwl 0x2a(%esp),%eax
       0x08048468 <+75>:    mov    $0xc,%ah
       0x0804846a <+77>:    mov    %ax,0x28(%esp)
       0x0804846f <+82>:    fldcw  0x28(%esp)
       0x08048473 <+86>:    fistpl 0x40(%esp)
       0x08048477 <+90>:    fldcw  0x2a(%esp)
       0x0804847b <+94>:    flds   0x48(%esp)
       0x0804847f <+98>:    fldl   0x80485c0
       0x08048485 <+104>:   fmulp  %st,%st(1)
       0x08048487 <+106>:   fldcw  0x28(%esp)
       0x0804848b <+110>:   fistpl 0x3c(%esp)       // f1 * 10 convert int
       0x0804848f <+114>:   fldcw  0x2a(%esp)
       0x08048493 <+118>:   flds   0x48(%esp)
       0x08048497 <+122>:   fldl   0x80485c0
       0x0804849d <+128>:   fmulp  %st,%st(1)
       0x0804849f <+130>:   flds   0x44(%esp)
       0x080484a3 <+134>:   fxch   %st(1)
       0x080484a5 <+136>:   mov    0x3c(%esp),%eax
       0x080484a9 <+140>:   mov    0x40(%esp),%edx
       0x080484ad <+144>:   sub    %eax,%edx
       0x080484af <+146>:   mov    %edx,%eax
       0x080484b1 <+148>:   fstpl  0x18(%esp)
       0x080484b5 <+152>:   fstpl  0x10(%esp)
       0x080484b9 <+156>:   mov    %eax,0xc(%esp)
       0x080484bd <+160>:   mov    0x3c(%esp),%eax
       0x080484c1 <+164>:   mov    %eax,0x8(%esp)
       0x080484c5 <+168>:   mov    0x40(%esp),%eax
       0x080484c9 <+172>:   mov    %eax,0x4(%esp)
       0x080484cd <+176>:   movl   $0x8048588,(%esp)
       0x080484d4 <+183>:   call   0x80482f0 <printf@plt>
       0x080484d9 <+188>:   addl   $0x1,0x4c(%esp)
       0x080484de <+193>:   cmpl   $0x14,0x4c(%esp)
       0x080484e3 <+198>:   jle    0x8048433 <main+22>
       0x080484e9 <+204>:   leave  
       0x080484ea <+205>:   ret
0 голосов
/ 26 февраля 2019

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

Результат в вашем случае может быть обусловлен тем фактом, что c1 вычисляется как "float-to-int", в то время как c2 вычисляется как "double-to-int" (или даже более высокая точность).

Вот еще один пример, демонстрирующий то же поведение.

#define DD 0.11111111

int main()
{
  int i = 27;

  int c1,c2,c3;
  float f1;
  double d1;
  printf("%.60f\n", DD);

  f1 = i * DD;
  d1 = i * DD;
  c1 = (int)f1;
  c2 = (int)(i * DD);
  c3 = (int)d1;

  printf("----------------------\n");
  printf("f1: %.60f\n", f1);
  printf("d1: %.60f\n", d1);
  printf("m : %.60f\n", i * DD);
  printf("%d, %d, %d\n",c1,c2,c3);
}

Мой вывод:

0.111111109999999999042863407794357044622302055358886718750000
----------------------
f1: 3.000000000000000000000000000000000000000000000000000000000000
d1: 2.999999970000000182324129127664491534233093261718750000000000
m : 2.999999970000000182324129127664491534233093261718750000000000
3, 2, 2

Хитрость здесь заключается в количестве единиц в 0.11111111.Точный результат - «2.99999997».Когда вы меняете количество единиц, точный результат остается в форме «2,99 ... 997» (т.е. число 9 увеличивается, когда увеличивается число 1).

В какой-то момент (или некоторое количество единиц) вы достигнете точки, в которой при сохранении результата с плавающей запятой округляется результат до «3,0», в то время как двойник все еще может удерживать «2,9999999 .....».Тогда преобразование в int даст другие результаты.

Дальнейшее увеличение числа единиц приведет к точке, в которой двойное число также округлится до «3,0», и преобразование в int приведет к тому же результату.

0 голосов
/ 26 февраля 2019

В «32-битной системе» разница вызвана тем, что f1*10.0 использует полную точность double, в то время как f10 имеет точность только float, поскольку это ее тип.f1*10.0 использует double точность, потому что 10.0 является константой double.Когда f1*10.0 присваивается f10, значение изменяется, поскольку оно неявно преобразуется в float, что имеет меньшую точность.

Если вместо этого используется float константа 10.0f, различияvanish.

Рассмотрим первый случай, когда i равен 1. Тогда:

  • В f1 = 3+i*0.1, 0.1 является константой double, поэтому арифметикавыполняется в double, и результат составляет 3,100000000000000088817841970012523233890533447265625.Затем, чтобы присвоить это значение f1, оно преобразуется в float, что дает 3,099999904632568359375.
  • In f10 = f1*10.0;, 10.0 является константой double, поэтому арифметика снова выполняется вdouble, и результат равен 30,999999904632568359375.Для присвоения f10 это преобразуется в float, а результат равен 31.
  • Позже, когда печатаются f10 и f1*10.0, мы видим значения, приведенные выше, с девятью цифрамипосле десятичной точки: «31.000000000» для f10 и «30.999999046».

Если вы печатаете f1*10.0f, с константой float 10.0f вместо константы double10.0, результат будет «31.000000000», а не «30.999999046».

(Выше используется базовая 32-битная и 64-битная двоичная арифметика IEEE-754.)

В частности, обратите внимание: разница между f1*10.0 и f10 возникает, когда f1*10.0 преобразуется в float для присвоения f10.В то время как C позволяет реализациям использовать дополнительную точность при оценке выражений, он требует, чтобы реализации отбрасывали эту точность в присваиваниях и приведениях.Поэтому в компиляторе, соответствующем стандарту, присвоение f10 должно использовать float точность.Это означает, что даже когда программа скомпилирована для «64-битной системы», различия должны иметь место.В противном случае компилятор не соответствует стандарту C.

Более того, если float изменяется на double, преобразование в float не происходит, и значение не будетизменилось.В этом случае не должно проявляться никаких различий между f1*10.0 и f10.

Учитывая, что вопрос сообщает о том, что различия не проявляются с «64-битной» компиляцией и проявляются с double, онсомнительно, были ли наблюдения сообщены точно.Чтобы прояснить это, должен быть показан точный код, а наблюдения должны быть воспроизведены третьей стороной.

0 голосов
/ 26 февраля 2019

С помощью MS Visual C 2008 я смог воспроизвести это.

При проверке ассемблера, разница между ними заключается в промежуточном хранении и получении результата с промежуточными преобразованиями:

  f10 = f1*10.0;          // double result f10 converted to float and stored
  c1 = (int)f10;          // float result f10 fetched and converted to double
  c2 = (int)(f1*10.0);    // no store/fetch/convert

Генерируемые ассемблером помещают значения в стек FPU, которые преобразуются в 64 бита, а затем умножаются.Для c1 результат затем преобразуется обратно в число с плавающей запятой и сохраняется, а затем снова извлекается и помещается в стек FPU (и снова преобразуется в удвоение) для вызова __ftol2_sse, функции времени выполнения для преобразования double вint.

Для c2 промежуточным значением является , а не , преобразованное в и из числа с плавающей запятой и незамедлительно переданное в функцию __ftol2_sse.Для этой функции см. Также ответ в Преобразовать double в int? .

Ассемблер:

      f10 = f1*10;
fld         dword ptr [f1] 
fmul        qword ptr [__real@4024000000000000 (496190h)] 
fstp        dword ptr [f10] 

      c2 = (int)(f1*10);
fld         dword ptr [f1] 
fmul        qword ptr [__real@4024000000000000 (496190h)] 
call        __ftol2_sse
mov         dword ptr [c2],eax 

      c1 = (int)f10;
fld         dword ptr [f10] 
call        __ftol2_sse
mov         dword ptr [c1],eax 
...