欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

浮点数精度上误差

程序员文章站 2022-06-27 20:26:46
在我刚接触编程的时候, 那时候面试小题目很喜欢问下面这几类问题 1' 浮点数如何和零比较大小? 2' 浮点数如何转为整型? 然后过了七八年后这类问题应该很少出现在面试中了吧. 刚好最近我遇到线上 bug, 同大家交流科普下 问题最小现场 #include int main(voi ......

  在我刚接触编程的时候, 那时候面试小题目很喜欢问下面这几类问题 

               1'  浮点数如何和零比较大小?

               2'  浮点数如何转为整型?

然后过了七八年后这类问题应该很少出现在面试中了吧.  刚好最近我遇到线上 bug,  同大家交流科普下

 

问题最小现场

#include <stdio.h>

int main(void) {
    float a = 2.01f;
    double b = 2.01;

    printf("a1 : 2.01 * 1000 = %f\n", a * 1000);             // a1 : 2.01 * 1000      = 2010.000000
    printf("a2 : int(2.01 * 1000) = %d\n", (int)(a * 1000)); // a2 : int(2.01 * 1000) = 2010

    printf("b1 : 2.01 * 1000 = %lf\n", b * 1000);            // b1 : 2.01 * 1000      = 2010.000000
    printf("b2 : int(2.01 * 1000) = %d\n", (int)(b * 1000)); // b2 : int(2.01 * 1000) = 2009
}

(用 go java 效果是一样的, 绝大部分实现都是严格遵循 ieee754 标准

 

问题解答

其中 a1 和 b1 在 c 中 等价于下面的代码

float a = 2.01f;
double b = 2.01;

printf("a1 : 2.01 * 1000 = %f\n", (double)(a * 1000));

printf("b1 : 2.01 * 1000 = %f\n", b * 1000);

其中 printf float 其实相当于 printf (double) 去处理的. 具体可以看这类源码 

#define parse_float_va_arg(info)                          \
  do                                          \
    {                                          \
      info.is_binary128 = 0;                              \
      if (is_long_double)                              \
    the_arg.pa_long_double = va_arg (ap, long double);              \
      else                                      \
    the_arg.pa_double = va_arg (ap, double);                  \
    }                                          \
  while (0)

其次二者输出打印的数据内容一样. 本质原因是, double 尾数的高23位和float的尾数23位一样.

如果你用 %.8f 可能就不一样了.  

(float : 1 + 8 +23, 小数点后精度 6-7)

(double : 1 + 11 + 52, 小数点后精度 15-16)

简单的, 我们可以用下面代码去验证 

#include <stdio.h>

static void print_byte(unsigned char byte) {
    printf("%d%d%d%d%d%d%d%d"
        , ((byte >> 7) & 1) 
        , ((byte >> 6) & 1)
        , ((byte >> 5) & 1)
        , ((byte >> 4) & 1)
        , ((byte >> 3) & 1)
        , ((byte >> 2) & 1)
        , ((byte >> 1) & 1)
        , ((byte >> 0) & 1)
    );
}

static void print_number(const void * data, size_t n) {
    const unsigned char * bytes = data;

# if __byte_order__ == __order_little_endian__
    for (size_t i = n; i > 0; i--) {
        print_byte(bytes[i-1]);
    }
# else
    for (size_t i = 0; i < n; i++) {
        print_byte(bytes[i]);
    }
# endif
}

static void print_float(float num) {
    printf(" float = ");
    print_number(&num, sizeof num);
    printf("\n");
}

static void print_double(double num) {
    printf("double = ");
    print_number(&num, sizeof num);
    printf("\n");
}

int main(void) {
    float a = 2.01f;
    double b = 2.01;

    print_float(a);
    print_double(b);

    printf(" float 2.01f + %%.%df = %.*f\n",  8, 8, a);
    printf("double 2.01  + %%.%df = %.*lf\n", 8, 8, b);
}

 

在 window 和 ubuntu 得到的测试数据如下 

/*
  float = 01000000000000001010001111010111
 double = 0100000000000000000101000111101011100001010001111010111000010100

 float  2.01f = 0 10000000    00000001010001111010111
 double 2.01  = 0 10000000000 00000001010001111010111 00001010001111010111000010100

  float 2.01f + %.6f = 2.010000
 double 2.01  + %.6f = 2.010000

 float 2.01f + %.7f = 2.0100000
double 2.01  + %.7f = 2.0100000

 float 2.01f + %.8f = 2.00999999
double 2.01  + %.8f = 2.01000000

 float 2.01f + %.10f = 2.0099999905
double 2.01  + %.10f = 2.0100000000

 float 2.01f + %.15f = 2.009999990463257
double 2.01  + %.15f = 2.010000000000000

 float 2.01f + %.16f = 2.0099999904632568
double 2.01  + %.16f = 2.0099999999999998

 float 2.01f + %.17f = 2.00999999046325684
double 2.01  + %.17f = 2.00999999999999979
 */

明显可以看出来 a = 2.01f 和 b = 2.01 在内存中二者是不一样的. 即 a != b, a * 1000 != b * 1000. 有兴趣的可以自行去实验. 

 

问题解答继续

这里说说 a2 和 b2 case 造成的原因.

printf("a2 : int(2.01 * 1000) = %d\n", (int)(a * 1000)); // a2 : int(2.01 * 1000) = 2010

printf("b2 : int(2.01 * 1000) = %d\n", (int)(b * 1000)); // b2 : int(2.01 * 1000) = 2009

 

我们首先获取其内存布局 

 float 2010.0f = 0 10001001    11110110100000000000000
double 2010.0  = 0 10000001001 1111011001111111111111111111111111111111111111111111

 

随后借助场外信息, 引述 <<深入理解计算机系统-第三版>> 部分舍入概念

浮点数精度上误差

 误差来自浮点数无法精确表示和转换过程中舍入起的效果. 

 

问题反思

这类问题, 或多或少遇到过, 希望我们这里对这类问题做个了结 ~  

此刻不知道有心人会不会着急下结论,

那以后的业务中还是别用 float 了, 或者直接用 double, 或者定点小数, 或者整数替代 float 等等 ...

这么考虑很不错, 在大多数领域是完全没有问题的. 也是值得推荐的. 

补充下, 也有些领域例如嵌入式, 他们还是会用 float, 因为对他们而言 double 有的时候太浪费内存了,

还存在着地址对齐等问题. 

虽然不同领域(场景)会有不同方式方法,  但有一点需要大家一块遵守, 没有特殊情况别混着用

希望以上能帮助朋友们对这类问题知其所以然 ~

 

后记 - 再见, 祝好运 ~

  错误是难免的, 欢迎交流指正, 当找个乐子 ~ 哈哈哈 ~

 

summer