十进制浮点数相乘问题

2017-12-15 14:39:35 +08:00
 wisej
Python 3.5.0 (v3.5.0:374f501f4567, Sep 13 2015, 02:27:37) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> 1.2*3
3.5999999999999996
>>> 1.2*3.0
3.5999999999999996

今天写程序时,碰到类似上面这样的十进制浮点数与另一个数相乘,结果跟上面类似。

后来经过查证,首先了解到浮点数在计算机中是通过二进制数来表示的。从而导致类似 0.1 这样的数是无法用二进制数精确的表示的。具体的可以查看 https://docs.python.org/3/tutorial/floatingpoint.html

正确的办法是 decimal 模块,转换成 Decimal 对象。

我的疑惑是,这是否意味着对于所有的不能用二进制精确表示的浮点数,都应该转换成 Decimal 进行运算才能得到正确结果呢?

2434 次点击
所在节点    Python
7 条回复
GeruzoniAnsasu
2017-12-15 15:38:34 +08:00
“正确结果”这个说法本来就很模糊
1/3=0.33333333...是正确的
1.2*3.0=5.9999999...也是正确的

不管是定点还是浮点,用小数来表示除不尽的分数怎样都会丢失精度,所以在涉及浮点的运算从来都是判断结果是否小于最小精度目标的,这应该是常识

用 decimal 模块只是以 10 进制的习惯思路去计算而已,你看习惯了 0.33333 自然不觉得有什么问题,但实际上十进制 if(1/3==0.3) 和二进制 if(1.2*3==3.6)差不多是一回事
yuriko
2017-12-15 16:02:56 +08:00
如果 1/3 = 0.3333333333333 是对的话
那么 1 = 0.9999999999999 也是对的
最后就是进度问题罢了,什么精度下才是正确?如果无限循环小数的话,0.999999999 ……= 1 是有数学证明的。


浮点数丢失精度这个也是老生常谈的问题了,需不需要转换,是根据需要各取所需的事情。
wisej
2017-12-19 10:08:25 +08:00
感谢两位回复。重新看了下定点和浮点相关的知识,已经明白出现问题的原因了。

但是我还没想通的是:在求值时,我们难道不是总是期望 4.1*3 =12.3 而不是 丢失精度的 12.299999 么

譬如在 C++中,4.1*3 =12.3.而 python 却还得进行 decimal 操作
GeruzoniAnsasu
2017-12-19 10:23:33 +08:00
@wisej c++里并不是 12.3 仍然是 12.299999,只是在输出的时候有些额外的 workaround


------------------------------------源码----------------------------------------
#include <stdio.h>
int flg = 0;
int main()
{
double v1 = 4.1;
double v2 = 3;
printf("%lf",v1*v2);
}
---------------------------------------------------------------------------------
----------------------------------编译结果------------------------------------

flg:
.zero 4
.LC2:
.string "%lf"
main:
push rbp
mov rbp, rsp
sub rsp, 16
movsd xmm0, QWORD PTR .LC0[rip]
movsd QWORD PTR [rbp-8], xmm0
movsd xmm0, QWORD PTR .LC1[rip]
movsd QWORD PTR [rbp-16], xmm0
movsd xmm0, QWORD PTR [rbp-8]
mulsd xmm0, QWORD PTR [rbp-16]
mov edi, OFFSET FLAT:.LC2
mov eax, 1
call printf
mov eax, 0
leave
ret
.LC0:
.long 1717986918
.long 1074816614
.LC1:
.long 0
.long 1074266112

(可以直接在 https://gcc.godbolt.org/实时查看代码段在不同编译器下的结果)




---------------------------------------GDB-----------------------------------------


[----------------------------------registers-----------------------------------]
RAX: 0x400526 (<main>: push rbp)
RBX: 0x0
RCX: 0x0
RDX: 0x7fffffffddc8 --> 0x7fffffffe1b6 ("XDG_SEAT=seat0")
RSI: 0x7fffffffddb8 --> 0x7fffffffe1ab ("/tmp/a.out")
RDI: 0x1
RBP: 0x7fffffffdcd0 --> 0x400570 (<__libc_csu_init>: push r15)
RSP: 0x7fffffffdcc0 --> 0x4008000000000000
RIP: 0x400552 (<main+44>: mov edi,0x4005f8)
R8 : 0x4005e0 (<__libc_csu_fini>: repz ret)
R9 : 0x7ffff7de7ab0 (<_dl_fini>: push rbp)
R10: 0x846
R11: 0x7ffff7a2d740 (<__libc_start_main>: push r14)
R12: 0x400430 (<_start>: xor ebp,ebp)
R13: 0x7fffffffddb0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x400543 <main+29>: movsd QWORD PTR [rbp-0x8],xmm0
0x400548 <main+34>: movsd xmm0,QWORD PTR [rbp-0x10]
0x40054d <main+39>: mulsd xmm0,QWORD PTR [rbp-0x8]
=> 0x400552 <main+44>: mov edi,0x4005f8
0x400557 <main+49>: mov eax,0x1
0x40055c <main+54>: call 0x400400 <printf@plt>
0x400561 <main+59>: mov eax,0x0
0x400566 <main+64>: leave
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdcc0 --> 0x4008000000000000
0008| 0x7fffffffdcc8 --> 0x4010666666666666
0016| 0x7fffffffdcd0 --> 0x400570 (<__libc_csu_init>: push r15)
0024| 0x7fffffffdcd8 --> 0x7ffff7a2d830 (<__libc_start_main+240>: mov edi,eax)
0032| 0x7fffffffdce0 --> 0x0
0040| 0x7fffffffdce8 --> 0x7fffffffddb8 --> 0x7fffffffe1ab ("/tmp/a.out")
0048| 0x7fffffffdcf0 --> 0x100000000
0056| 0x7fffffffdcf8 --> 0x400526 (<main>: push rbp)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0000000000400552 in main ()
gdb-peda$ p $xmm0
$1 = {
v4_float = {-1.58818668e-23, 2.63437486, 0, 0},
v2_double = {12.299999999999999, 0},
v16_int8 = {0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x28, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
v8_int16 = {0x9999, 0x9999, 0x9999, 0x4028, 0x0, 0x0, 0x0, 0x0},
v4_int32 = {0x99999999, 0x40289999, 0x0, 0x0},
v2_int64 = {0x4028999999999999, 0x0},
uint128 = 0x00000000000000004028999999999999
}
gdb-peda$


可以很清楚地看到算出来就是 12.29999999999999,这是机器码已经决定了的。只是在 printf 后被%lf 重新格式化成了 12.300000
wisej
2017-12-19 11:22:33 +08:00
@GeruzoniAnsasu 我不理解的是,既然由于 float 内部的存储原理会导致精度丢失,进而导致上述这类问题。为什么会采用这种标准;以及我们(或许只是我)总是认为 4.1*3=12.3,既然如此,语言设计者为什么不内部进行处理返回我们所期望的值 12.3 呢
GeruzoniAnsasu
2017-12-19 17:07:04 +08:00
@wisej 首先再强调一次,你期望的“精确的 12.300 ”在浮点运算硬件中是无法存在的,在浮点寄存器中无法存放无法表示,想要精确表示 12.3000,只能有两种办法

1. 不采用浮点数,而用分数表示,也就是这个数就记录为 int(12)+int(3)/int(10)。注意这个方法还是可硬件实现的,只是没有这样的硬件而已。
2. 不采用硬件浮点运算单元,用软件模拟,以 10 进制小数习惯进行运算,4.10e0*3.0e0==1.23e1


第一种方法如果用硬件实现,表示一个数需要 3 部分存储单元而且无法表示无理数,除非为了些莫名其妙的目的专门造否则不可能做这样的硬件。如果软件模拟,则其实跟第二种方法差不多。

那么现在就只剩第二种方法了,软件模拟计算。

软件模拟!

都软件模拟了,还要多说吗?当年 8087 协处理器是用来干啥的,不就是解决 8086 算浮点太慢的问题嘛,没有任何一个现代 CPU 是不带浮点运算单元的,因为实在太重要。


“为什么会采用这种标准” 是一脉相承的,首先有了开关,然后有了二进制和晶体管,然后有了数字电路,然后有了集成电路和定点运算 cpu,然后有了浮点协处理器,然后才有了现代自带浮点单元的 CPU。整个计算机世界的所有标准都是从那个二进制开关传承下来的,如果人们发现的那个可以作为开关的三极管有三个可控稳定态,那么现在数字世界的编码方式很可能就是三进制的了

扯远了,总之你的问题,你以为的因果是 12.29999→数是以二进制表示的→无法精确;但实际上的因果关系是,数字电路必定是二进制的→IEEE 标准浮点→浮点运算器→你看到的结果。



“语言设计者为什么不内部进行处理返回我们所期望的值 12.3 呢” ← 所以,为了能产出不那么反直觉的结果,c/++在输出的时候做了额外处理,使其能重新还原成人们熟悉的小数结果,而 python 默认没这么做,而是另外提供 decimal 模块半软件半硬件地来以人类直觉 10 进制计算小数
wisej
2017-12-19 20:04:55 +08:00
@GeruzoniAnsasu 哈哈,谢谢老铁这么认真的回复。可能我太钻牛角尖了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/415039

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX