将float转换为int的差异,32位C

ulydmbyx  于 2023-06-21  发布在  其他
关注(0)|答案(4)|浏览(146)

我目前正在使用一个需要运行32位系统的旧代码。在这项工作中,我偶然发现了一个问题(出于学术兴趣),我想了解其原因。
在32位C中,如果对变量或表达式进行强制转换,则从float到int的强制转换行为似乎有所不同。考虑该程序:

#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);
   }
}

直接在32位系统上编译(使用gcc),或者在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更改为shortlong,则也会存在该问题,如果程序编译为64位,则该问题也不会出现。
为了澄清,我在这里试图理解的问题不是关于浮点算术/舍入,而是32位内存处理的差异。
对该问题进行了测试:

  • Linux版本4.15.0-45-通用(buildd@lgw01-amd 64 -031)(gcc版本7.3.0(Ubuntu 7.3.0- 16 ubuntu 3)),使用以下代码编译程序: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

任何提示,以帮助我了解这里发生了什么是赞赏。

o4tp2gmn

o4tp2gmn1#

在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堆栈上(并再次转换为double),以调用__ftol2_sse,这是一个将double转换为int的运行时函数。
对于c2,中间值 * 不 * 转换为float,并立即传递给__ftol2_sse函数。对于这个函数,也可以在Convert double to 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
moiiocjp

moiiocjp2#

在“32位系统”中,差异是由于f1*10.0使用完整的double精度,而f10只有float精度,因为这是它的类型。f1*10.0使用double精度,因为10.0double常量。当f1*10.0被赋值给f10时,值会发生变化,因为它被隐式转换为float,后者的精度较低。
如果使用float常量10.0f,则差异消失。
考虑第一种情况,i为1。然后:

  • f1 = 3+i*0.1中,0.1是一个double常数,所以在double中执行算术,结果是3.10000000000000088817841970012523233890533447265625。然后,为了将其分配给f1,将其转换为float,生成3.099999904632568359375。
  • f10 = f1*10.0;中,10.0是一个double常数,因此在double中再次执行运算,结果为30.99999904632568359375。对于f10的赋值,它被转换为float,结果是31。
  • 稍后,当打印f10f1*10.0时,我们看到上面给出的值,小数点后有9位,f10为“31.000000000”,“30.999999046”。

如果打印f1*10.0f,使用float常量10.0f而不是double常量10.0,结果将是“31.000000000”而不是“30.999999046”。
(The上面使用IEEE-754基本32位和64位二进制浮点运算。)
特别要注意的是:当f1*10.0转换为float以分配给f10时,f1*10.0f10之间的差异出现。虽然C允许实现在计算表达式时使用额外的精度,但它要求实现在赋值和强制转换时放弃此精度。因此,在符合标准的编译器中,对f10的赋值必须使用float精度。这意味着,即使程序是为“64位系统”编译的,差异应该发生。如果不这样做,编译器就不符合C标准。
此外,如果将float更改为double,则不会发生到float的转换,并且不会更改值。在这种情况下,f1*10.0f10之间应该没有差异。
考虑到问题报告的差异在“64位”编译中没有表现出来,而在double中表现出来,是否准确报告了观察结果值得怀疑。为了澄清这一点,应显示确切的代码,并由第三方复制观察结果。

9fkzdhlc

9fkzdhlc3#

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中1的个数。准确的结果是“2.99999997”。当你改变1的数量时,准确的结果仍然是“2.99...997”的形式。当1的数量增加时,9的数量也增加)。
在某个点(也就是一些1),您将到达一个点,将结果存储在浮点数中将结果舍入为“3.0”,而double仍然能够保存“2.999999.....”。然后转换为int将给出不同的结果。
进一步增加1的数量将导致double也将舍入为“3.0”的点,并且转换为int将因此产生相同的结果。

ukqbszuj

ukqbszuj4#

主要原因是下面两行的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,或者把float改成double)
像这样:

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

然后使用gdb调试,断点在0x 0804845 b,运行到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

观察FSTP的执行结果,
此时,fpu上控制寄存器的RC值为Round to nearest
FPU寄存器上的值是30.99999904632568359(80位)。
0x 44(%esp)(variable "f10")上的值为31.0。(四舍五入至最接近值)
然后使用gdb调试,断点在0x 0804848 b,运行到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

此时,fpu上控制寄存器的RC值为Round towards zero
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

相关问题