Win32 GCC4.6.1 C语言 下对浮点数强制类型转换竟出意外!
在做Win32平台下的文本文件转二进制文件的CLI小程序时发现一个意外。代码片段:
float f;
short s;
while(fscanf(in_file, "%f", &f) != EOF)
{
if(fabs(f) < ((1 << 15) / 10))
{
f *= 10;
s = (short)f;
}
else
s = 0x7fff;
}
将其改为如下后跟原来的s值有个别不一样:
float f;
short s;
while(fscanf(in_file, "%f", &f) != EOF)
{
if(fabs(f) < ((1 << 15) / 10))
{
s = (short)(f * 10);
}
else
s = 0x7fff;
}
然后单独写个测试程序测试下,发现其实会不一样:
#include <stdio.h>
int main(int argc, char **argv)
{
float f;
short s1, s2;
printf("输入浮点数:\n");
scanf("%f", &f);
s1 = (short)(f * 10);
f *= 10;
s2 = (short)f;
printf("s1 %s s2\n", (s1 == s2)? "==" : "!=");
printf("s1 = %d, s2 = %d\n", s1, s2);
return 0;
}
输出:
输入浮点数:
1.9
s1 != s2
s1 = 18, s2 = 19
当输入值的小数位为.95以下时不一样,即两者的舍入误差不一样。
下面是其对应的汇编代码:
main:
0040138c: push %ebp
0040138d: mov %esp,%ebp
0040138f: and $0xfffffff0,%esp
00401392: sub $0x30,%esp
159 {
00401395: call 0x40196c <__main>
163 printf("输入浮点数:\n");
0040139a: movl $0x403064,(%esp)
004013a1: call 0x401ba4 <puts>
164 scanf("%f", &f);
004013a6: lea 0x28(%esp),%eax
004013aa: mov %eax,0x4(%esp)
004013ae: movl $0x403070,(%esp)
004013b5: call 0x401bac <scanf>
166 s1 = (short)(f * 10);
004013ba: flds 0x28(%esp)
004013be: flds 0x403098
004013c4: fmulp %st,%st(1)
004013c6: fnstcw 0x1e(%esp)
004013ca: mov 0x1e(%esp),%ax
004013cf: mov $0xc,%ah
004013d1: mov %ax,0x1c(%esp)
004013d6: fldcw 0x1c(%esp)
004013da: fistp 0x2e(%esp)
004013de: fldcw 0x1e(%esp)
167 f *= 10;
004013e2: flds 0x28(%esp)
004013e6: flds 0x403098
004013ec: fmulp %st,%st(1)
004013ee: fstps 0x28(%esp)
168 s2 = (short)f;
004013f2: flds 0x28(%esp)
004013f6: fldcw 0x1c(%esp)
004013fa: fistp 0x2c(%esp)
004013fe: fldcw 0x1e(%esp)
169 printf("s1 %s s2\n", (s1 == s2)? "==" : "!=");
00401402: mov 0x2e(%esp),%ax
00401407: cmp 0x2c(%esp),%ax
0040140c: jne 0x401415 <main+137>
0040140e: mov $0x403073,%eax
00401413: jmp 0x40141a <main+142>
00401415: mov $0x403076,%eax
0040141a: mov %eax,0x4(%esp)
0040141e: movl $0x403079,(%esp)
00401425: call 0x401bb4 <printf>
170 printf("s1 = %d, s2 = %d\n", s1, s2);
0040142a: movswl 0x2c(%esp),%edx
0040142f: movswl 0x2e(%esp),%eax
00401434: mov %edx,0x8(%esp)
00401438: mov %eax,0x4(%esp)
0040143c: movl $0x403083,(%esp)
00401443: call 0x401bb4 <printf>
172 return 0;
00401448: mov $0x0,%eax
173 } s1 = (short)(f * 10);
f *= 10;
s2 = (short)f;
printf("s1 %s s2\n", (s1 == s2)? "==" : "!=");
诸位有什么感想 浮点舍入误差吧。
默认用FPU,那个东西中间结果是80Bit的浮点类型。写回内存需要转换成32/64Bit。
所以FPU直接转换和写回内存后读出来再转,对于小数来说可能是完全不同的两个数,足以导致误差的发生。
如果优化开的高,比如O2,可能就又不出现了,因为O0是一定要写回的,别的就有优化。
GCC可以开mfpmath=sse这样的参数,强制用SSE,不用FPU算浮点。但要CPU支持。 回复【2楼】dr2001
-----------------------------------------------------------------------
即便是32位的浮点数的有效位数也有24位,相比80位的浮点数在数值如此小的情况下不可能
因为精度不同而产生如此大的误差!
我感觉那两都不同是因为两者的舍入方式不一样,一个是截断的舍入、一个是四舍五入的舍入。 回复【3楼】ufbycd
-----------------------------------------------------------------------
基于你的回答,强烈建议重新学习浮点数在表示小数时候的表示方法。
时刻谨记对任意小数,IEEE 754表示下,绝大部分是近似值,仅有少数才是精确值。所以,80位到64/32的舍入误差存在且不一定可以忽略。
小数的位权是1/(2^n),不是整数。
对比你的程序用:gcc a.c -mfpmath=387 -march=core2 和 gcc a.c -mfpmath=sse -march=core2 编译、运行的结果。
要求CPU支持SSE2,这里选的arch是core2. 回复【4楼】dr2001
基于你的回答,强烈建议重新学习浮点数在表示小数时候的表示方法。
时刻谨记对任意小数,ieee 754表示下,绝大部分是近似值,仅有少数才是精确值。所以,80位到64/32的舍入误差存在且不一定可以忽略。
小数的位权是1/(2^n),不是整数。
对比你的程序用:gcc a.c -mfpmath=387 -march=core2 和 gcc a.c -mfpmath=sse -march=core2 编译、运行的结果。
要求cpu支持sse2,这里选的arch是core2.
-----------------------------------------------------------------------
兄弟你有认真看我的楼主贴么,你有算过舍入误差么,一个基数只有个位的浮点数的运算误差有0.1?
一个80位的浮点数转成32位的有0.1的误差?
80位的浮点数的最小分辨率是多少,32位的浮点数的最小分辨率是多少? 自行考察代码:
#include <stdio.h>
#defineX10
int
main(void) {
volatile int a, b;
volatile floatA = 1.9;
volatile double B = A;
A *= X;
B *= X;
a = (int)A;
b = (int)B;
printf("%.20f%d\n", A, a);
printf("%.20f%d\n", B, b);
}
以上代码,所有浮点数的舍入方式都是一致的;浮点到int的舍入方式都是直接截断;这两点可以参考C标准中的规定。
以上代码就是模拟原始代码中387干的事情,结果的差异源于浮点尾巴小数的那一点差异,那一点不精确。并且可以分析X=10; X=2^n,如16时的异同。
类似的数据有很多,其中一部分是进位临界的情形。
这就是二进制浮点的特点,某些情况下的行为不完全和人直观的思维结果一致,但确实符合一系列的标准规定。否则新的IEEE 754也不会费劲的引入十进制浮点,IBM也不至于花资源在新的PPC的FPU中做十进制浮点的支持。
这个程序已经说明的十分清晰了,over。 嗯发现问题了,我原以为A和B 乘以10以后的整数部分都会小于19.但事实是A的整数部分刚好等于19.
页:
[1]