将 float 转换为 int 的区别,32 位 C

Difference in casting float to int, 32-bit C

我目前正在使用需要 运行 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 位系统或 64 位系统上使用 -m32 修饰符编译(使用 gcc)程序的输出是:

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 and/or int 更改为 shortlong,该问题也存在如果程序编译为 64 位,则不会显示。

澄清一下,我在这里试图了解的问题不是关于浮点 arithmetic/rounding,而是 32 位内存处理的差异。

该问题已在以下平台上进行测试:

感谢任何帮助我理解这里发生的事情的指示。

使用 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

汇编器生成的值将被转换为 64 位的 FPU 堆栈压入,然后相乘。对于 c1,结果随后被转换回浮点数并存储,然后再次检索并放置在 FPU 堆栈中(并再次转换为双精度数)以调用 __ftol2_sse、运行 -time 函数将 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 

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”形式(即,当 1 的数量增加时,9 的数量增加)。

在某个点(也就是一些点)你会到达一个点,将结果存储在一个浮点数中,将结果四舍五入为“3.0”,而双精度仍然能够保持“2.999999 .....” .然后转换为 int 将给出不同的结果。

进一步增加 1 的数量将导致 double 也将四舍五入为“3.0”,因此转换为 int 将产生相同的结果。

在“32位系统”中,差异是由于f1*10.0使用全double精度,而f10只有float精度因为那是它的类型。 f1*10.0 使用 double 精度,因为 10.0 是一个 double 常量。当 f1*10.0 赋值给 f10 时,值发生变化,因为它被隐式转换为精度较低的 float

如果您改用 float 常量 10.0f,差异就会消失。

考虑第一种情况,当i为1时,则:

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

如果您打印 f1*10.0f,使用 float 常量 10.0f 而不是 double 常量 10.0,结果将是“31.000000000”而不是比“30.999999046”。

(以上使用IEEE-754基本32位和64位二进制浮点运算。)

特别注意:f1*10.0f10 之间的区别出现在 f1*10.0 转换为 float 以分配给 f10 时。虽然 C 允许实现在评估表达式时使用额外的精度,但它要求实现在赋值和强制转换中放弃这种精度。因此,在符合标准的编译器中,对 f10 的赋值必须 使用 float 精度。这意味着,即使程序是为“64 位系统”编译的,差异 也应该 出现。否则,编译器不符合 C 标准。

此外,如果将float更改为double,则不会发生向float的转换,并且不会更改值。在这种情况下,f1*10.0f10 之间应该没有区别。

鉴于问题报告的差异在“64 位”编译中没有体现出来,而在 double 中体现出来,是否准确报告观察结果值得怀疑。为澄清这一点,应显示确切的代码,并应由第三方复制观察结果。

主要原因是后面两行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调试,断点在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的执行结果,
此时fpu上的控制寄存器上的RC值是Round to nearest.
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

此时fpu上控制寄存器的RC值向零舍入.
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    [=14=]xfffffff0,%esp
       0x08048423 <+6>:     sub    [=14=]x50,%esp
       0x08048426 <+9>:     movl   [=14=]x0,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    [=14=]xc,%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   [=14=]x8048588,(%esp)
       0x080484d4 <+183>:   call   0x80482f0 <printf@plt>
       0x080484d9 <+188>:   addl   [=14=]x1,0x4c(%esp)
       0x080484de <+193>:   cmpl   [=14=]x14,0x4c(%esp)
       0x080484e3 <+198>:   jle    0x8048433 <main+22>
       0x080484e9 <+204>:   leave  
       0x080484ea <+205>:   ret