疑难杂症小记 - 浮点运算的精度问题

2018-08-02 16:48:34 浏览数 (1)

先上一段C#代码,有兴趣的朋友可以人脑执行一遍~

代码语言:javascript复制
    int num = 160;
    float test = 1.3f;

    float result = num * test;
    int result_1 = (int)result;
    int result_2 = (int)(num * test);
    int result_3 = (int)(float)(num * test);

    Console.WriteLine("{0} {1} {2} {3}", result, result_1, result_2, result_3);

代码看上去非常简单,运行结果却出人意料 : 208 208 207 208 !

result_2 = (int)(num * test) = (int)(160 * 1.3) = 208, 为什么程序会输出 207, WTF ? 更加诡异的是, result_3 的计算方式与 result_2 一模一样,只是中间多了一步float的转换,然后计算结果便正确了, WTF ??

SO上请教了一下,自己也去了解了一些相关知识,大抵弄清楚了原因,这里一步步的讲下,算作笔记了~

二进制小数无法精确表达十进制小数

拿上面的 test 为例,虽然代码中我们将他初始化为了十进制小数 1.3f, 但实际上,由于二进制小数无法精确表达十进制小数 1.3f, 所以浮点数 test 实际表达的是 1.3 的近似值. (细节来讲, test 的二进制表示为 0 01111111 01001100110011001100110,实际表示的数值为 1.29999995231628)

浮点数乘法可能是以高精度执行的

考虑上面的代码 float result = num * test, 实际的运算过程可能是在 double 精度下(或者更高精度下)进行的,翻译成代码,大概是这个样子:

代码语言:javascript复制
float result = (float)((double)num * (double)test);

首先我们来计算 (double)num * (double)test = 160 * 1.29999995231628 = 207.9999923706048, 其二进制表示为 0 10000000110 1001111111111111111111110000000000000000 接着我们要将计算结果转为 float, 由于 float 精度有限(23位精度),但是计算结果需要更高精度(24位精度),所以转化使结果被近似到了(0舍1入?)0 10000110 10100000000000000000000 (即208)

浮点数转整数采用的是截断方式

承接上面的说明, 我们计算出了高精度下的乘法数值 (double)num * (double)test = 160 * 1.29999995231628 = 207.9999923706048 (二进制表示为 0 10000000110 1001111111111111111111110000000000000000) 不同于浮点数转化,整数转换采用的是截断方式: 首先将上述结果的二进制转换为定点二进制小数 11001111.11111111111111111, 然后直接截断小数部分,得到: 11001111 (即207)

综合上面所述的三点原因,我们就可以解释上面的遗留问题了:

result_2 = (int)(num * test) = (int)(160 * 1.3) = 208, 为什么程序会输出 207 ? 因为 1.3 的实际二进制表示为 1.29999995231628,与 160 相乘后结果为 207.9999923706048,转换为整数时进行了截断,所以 result_2 的结果为 207

result_3 的计算方式与 result_2 一模一样,只是中间多了一步float的转换,为什么计算结果便正确了? 因为 1.3 的实际二进制表示为 1.29999995231628,与 160 相乘后结果为 207.9999923706048,转换为浮点数是采用了近似方式,得到了208,之后再转化为整数自然仍然是 208

如果你大抵明白了上面的讲述,试一试这道练习题吧,想想最后的输出会是什么:

代码语言:javascript复制
    int num = 160;
    double test = 1.3f;

    double result = num * test;
    int result_1 = (int)result;
    int result_2 = (int)(num * test);
    int result_3 = (int)(double)(num * test);

    Console.WriteLine("{0} {1} {2} {3}", result, result_1, result_2, result_3);

0 人点赞