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 进行运算才能得到正确结果呢?
1
GeruzoniAnsasu 2017-12-15 15:38:34 +08:00 1
“正确结果”这个说法本来就很模糊
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)差不多是一回事 |
2
yuriko 2017-12-15 16:02:56 +08:00 1
如果 1/3 = 0.3333333333333 是对的话
那么 1 = 0.9999999999999 也是对的 最后就是进度问题罢了,什么精度下才是正确?如果无限循环小数的话,0.999999999 ……= 1 是有数学证明的。 浮点数丢失精度这个也是老生常谈的问题了,需不需要转换,是根据需要各取所需的事情。 |
3
wisej OP 感谢两位回复。重新看了下定点和浮点相关的知识,已经明白出现问题的原因了。
但是我还没想通的是:在求值时,我们难道不是总是期望 4.1*3 =12.3 而不是 丢失精度的 12.299999 么 譬如在 C++中,4.1*3 =12.3.而 python 却还得进行 decimal 操作 |
4
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 |
5
wisej OP @GeruzoniAnsasu 我不理解的是,既然由于 float 内部的存储原理会导致精度丢失,进而导致上述这类问题。为什么会采用这种标准;以及我们(或许只是我)总是认为 4.1*3=12.3,既然如此,语言设计者为什么不内部进行处理返回我们所期望的值 12.3 呢
|
6
GeruzoniAnsasu 2017-12-19 17:07:04 +08:00 1
@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 进制计算小数 |
7
wisej OP @GeruzoniAnsasu 哈哈,谢谢老铁这么认真的回复。可能我太钻牛角尖了
|