ufbycd 发表于 2011-9-30 11:31:20

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       }

ufbycd 发表于 2011-9-30 12:56:52

s1 = (short)(f * 10);
f *= 10;
s2 = (short)f;
printf("s1 %s s2\n", (s1 == s2)? "==" : "!=");

诸位有什么感想

dr2001 发表于 2011-9-30 13:53:15

浮点舍入误差吧。

默认用FPU,那个东西中间结果是80Bit的浮点类型。写回内存需要转换成32/64Bit。
所以FPU直接转换和写回内存后读出来再转,对于小数来说可能是完全不同的两个数,足以导致误差的发生。

如果优化开的高,比如O2,可能就又不出现了,因为O0是一定要写回的,别的就有优化。

GCC可以开mfpmath=sse这样的参数,强制用SSE,不用FPU算浮点。但要CPU支持。

ufbycd 发表于 2011-9-30 16:16:07

回复【2楼】dr2001
-----------------------------------------------------------------------
即便是32位的浮点数的有效位数也有24位,相比80位的浮点数在数值如此小的情况下不可能
因为精度不同而产生如此大的误差!

我感觉那两都不同是因为两者的舍入方式不一样,一个是截断的舍入、一个是四舍五入的舍入。

dr2001 发表于 2011-10-9 11:48:11

回复【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.

ufbycd 发表于 2011-10-9 20:38:52

回复【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位的浮点数的最小分辨率是多少?

dr2001 发表于 2011-10-10 15:13:04

自行考察代码:

#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。

ufbycd 发表于 2011-10-10 15:43:19

嗯发现问题了,我原以为A和B 乘以10以后的整数部分都会小于19.但事实是A的整数部分刚好等于19.
页: [1]
查看完整版本: Win32 GCC4.6.1 C语言 下对浮点数强制类型转换竟出意外!