V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
wisej
V2EX  ›  Python

十进制浮点数相乘问题

  •  
  •   wisej · 2017-12-15 14:39:35 +08:00 · 2531 次点击
    这是一个创建于 2568 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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 进行运算才能得到正确结果呢?

    7 条回复    2017-12-19 20:04:55 +08:00
    GeruzoniAnsasu
        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)差不多是一回事
    yuriko
        2
    yuriko  
       2017-12-15 16:02:56 +08:00   ❤️ 1
    如果 1/3 = 0.3333333333333 是对的话
    那么 1 = 0.9999999999999 也是对的
    最后就是进度问题罢了,什么精度下才是正确?如果无限循环小数的话,0.999999999 ……= 1 是有数学证明的。


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

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

    譬如在 C++中,4.1*3 =12.3.而 python 却还得进行 decimal 操作
    GeruzoniAnsasu
        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
    wisej
        5
    wisej  
    OP
       2017-12-19 11:22:33 +08:00
    @GeruzoniAnsasu 我不理解的是,既然由于 float 内部的存储原理会导致精度丢失,进而导致上述这类问题。为什么会采用这种标准;以及我们(或许只是我)总是认为 4.1*3=12.3,既然如此,语言设计者为什么不内部进行处理返回我们所期望的值 12.3 呢
    GeruzoniAnsasu
        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 进制计算小数
    wisej
        7
    wisej  
    OP
       2017-12-19 20:04:55 +08:00
    @GeruzoniAnsasu 哈哈,谢谢老铁这么认真的回复。可能我太钻牛角尖了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5797 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 06:11 · PVG 14:11 · LAX 22:11 · JFK 01:11
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.