我目前正在处理需要运行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);
}
}
使用-m32
修饰符直接在32位系统或64位系统上编译(使用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
和/或将int
更改为short
或long
,该问题也不会出现如果程序被编译为64位。
为澄清起见,我要在此处理解的问题不是关于浮点算术/舍入,而是32位内存处理方面的差异。
问题已在以下位置进行了测试:
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
任何能帮助我了解此处发生情况的指示,均应感谢。
答案 0 :(得分:7)
使用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
,结果然后转换回float并存储,然后再次检索并放置在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
答案 1 :(得分:5)
在“ 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。f10
和f1*10.0
时,我们看到上面给出的值,小数点后有9位数字,f10
的值为“ 31.000000000”,“ 30.999999046”。 如果您使用f1*10.0f
常数float
而不是10.0f
常数double
打印10.0
,结果将是“ 31.000000000”,而不是“ 30.999999046”。
(以上使用IEEE-754基本的32位和64位二进制浮点算法。)
尤其要注意:f1*10.0
和f10
之间的差异是在f1*10.0
转换为float
并分配给f10
时出现的。尽管C允许实现在计算表达式时使用额外的精度,但它要求实现在赋值和强制转换中放弃这种精度。因此,在符合标准的编译器中,对f10
的赋值必须使用float
精度。这意味着,即使程序是为“ 64位系统”编译的,也会出现应该的差异。如果不符合,则表明编译器不符合C标准。
此外,如果将float
更改为double
,则不会发生到float
的转换,并且该值也不会更改。在这种情况下,f1*10.0
和f10
之间应该没有区别。
鉴于问题报告的差异没有通过“ 64位”编译显示,而是通过double
出现,因此是否正确报告了观察结果值得怀疑。为了澄清这一点,应显示确切的代码,并由第三方复制观察结果。
答案 2 :(得分:2)
对于如何进行浮点数学运算,C标准并不十分严格。该标准允许实现比涉及的类型更精确地进行计算。
您所遇到的结果很可能来自以下事实:c1
被计算为“浮点对整数”,而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会得到不同的结果。
进一步增加位数将导致双精度也将四舍五入为“ 3.0”,并且转换为int将因此产生相同的结果。
答案 3 :(得分:1)
主要原因是以下两行中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值舍入到最接近的。
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 $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