草庐IT

floating-point - 为什么 float 不正确?

coder 2024-07-09 原文

为什么有些数字存储为浮点数时会失去准确性?

例如,十进制数9.2可以精确地表示为两个十进制整数(92/10)的比率,两个整数都可以精确地以二进制(0b1011100/0b1010)表示。但是,存储为浮点数的相同比率永远不会完全等于9.2:

32-bit "single precision" float: 9.19999980926513671875
64-bit "double precision" float: 9.199999999999999289457264239899814128875732421875

这样一个看似简单的数字如何在存储的 64位中过大?

最佳答案

在大多数编程语言中,浮点数非常类似于scientific notation表示:具有指数和尾数(也称为有效位数)。一个很简单的数字,比如9.2,实际上就是这个分数:

5179139571476070 * 2 -49

其中指数为-49,尾数为5179139571476070。用这种方式无法表示一些十进制数字的原因是,指数和尾数都必须是整数。换句话说,所有浮点数必须是整数乘以2的整数次方。
9.2可能只是92/10,但是如果 n 限于整数值,则 10 不能表示为 2n

看到数据

首先,使用一些函数来查看组成32位和64位float的组件。如果只关心输出,则可以查看以下内容(Python示例):

def float_to_bin_parts(number, bits=64):
    if bits == 32:          # single precision
        int_pack      = 'I'
        float_pack    = 'f'
        exponent_bits = 8
        mantissa_bits = 23
        exponent_bias = 127
    elif bits == 64:        # double precision. all python floats are this
        int_pack      = 'Q'
        float_pack    = 'd'
        exponent_bits = 11
        mantissa_bits = 52
        exponent_bias = 1023
    else:
        raise ValueError, 'bits argument must be 32 or 64'
    bin_iter = iter(bin(struct.unpack(int_pack, struct.pack(float_pack, number))[0])[2:].rjust(bits, '0'))
    return [''.join(islice(bin_iter, x)) for x in (1, exponent_bits, mantissa_bits)]

该函数背后有很多复杂性,并且很容易解释,但是如果您有兴趣,则对于我们而言,重要的资源是struct模块。

Python的float是64位双精度数字。在其他语言(例如C,C++,Java和C#)中,双精度具有单独的double类型,通常将其实现为64位。

当我们使用示例9.2调用该函数时,得到的是:
>>> float_to_bin_parts(9.2)
['0', '10000000010', '0010011001100110011001100110011001100110011001100110']

解释数据

您会看到我将返回值分为三个部分。这些组件是:
  • 签名
  • 指数
  • 尾数(也称为有效数或分数)

  • 标志

    该符号作为单个位存储在第一个组件中。很容易解释:0表示浮点数为正数; 1表示否定。因为9.2为正,所以我们的符号值为0

    指数

    指数以11位存储在中间组件中。在我们的例子中,是0b10000000010。以十进制表示,代表值1026。该组件的一个怪癖是必须减去等于 2(位数)-1-1 的数字才能得到真实的指数。在我们的例子中,这意味着减去0b1111111111(十进制数1023)以获得真实的指数0b00000000011(十进制数3)。

    尾数

    尾数作为52位存储在第三部分中。但是,此组件也有一个怪癖。要理解这一怪异现象,请考虑用科学计数法表示的数字,如下所示:

    6.0221413x1023

    尾数为6.0221413。回想一下,科学计数法中的尾数始终以单个非零数字开头。二进制也是如此,只不过二进制只有两位数字:01。因此,二进制尾数始终以1开头!当存储浮点数时,将省略二进制尾数前面的1以节省空间。我们必须将其放回第三个元素的前面,以获取真实的尾数:

    1.0010011001100110011001100110011001100110011001100110110

    这不仅仅涉及简单的加法,因为存储在我们的第三部分中的位实际上代表了radix point右边的尾数的小数部分。

    在处理十进制数时,我们通过乘以10的乘方或除以“移动小数点”。在二进制中,我们可以通过乘以2的乘方或除以进行相同的操作。由于我们的第三个元素有52位,因此我们除以通过 252 将其向右移动52个位置:

    0.0010011001100110011001100110011001100110011001100110110

    用十进制表示法,与将675539944105574除以4503599627370496以获得0.1499999999999999相同。 (这是一个比率的示例,该比率可以精确地用二进制表示,但只能近似用十进制表示;有关更多详细信息,请参见:675539944105574 / 4503599627370496。)

    现在,我们已经将第三个分量转换为分数,添加1可以得到真实的尾数。

    重新盖上组件
  • 符号(第一个组件):0表示正,1表示负
  • 指数(中间部分):减去 2(位数)-1-1 以获取真实的指数
  • 尾数(最后一个部分):除以 2(位数)并添加1以获取真实的尾数


  • 计算数字

    将所有三个部分放在一起,我们得到这个二进制数:

    1.0010011001100110011001100110011001100110011001100110 x 1011

    然后我们可以将其从二进制转换为十进制:

    1.1499999999999999 x 23(不精确!)

    并乘以显示我们存储为浮点值后以(9.2)开头的数字的最终表示形式:

    9.1999999999999993


    表示为分数

    9.2

    现在我们已经构建了数字,可以将其重构为一个简单的分数:

    1.0010011001100110011001100110011001100110011001100110 x 1011

    将尾数转换为整数:

    10010011001100110011001100110011001100110011001100110110 x 1011-110100

    转换为十进制:

    5179139571476070 x 23-52

    减去指数:

    5179139571476070 x 2-49

    将负指数转化为除法:

    5179139571476070/249

    相乘指数:

    5179139571476070/562949953421312

    等于:

    9.1999999999999993

    9.5
    >>> float_to_bin_parts(9.5)
    ['0', '10000000010', '0011000000000000000000000000000000000000000000000000']
    

    您已经可以看到尾数只有4位数字,后面跟着很多零。但是,让我们逐步进行。

    汇编二进制科学符号:

    1.0011 x 1011

    移动小数点:

    10011 x 1011-100

    减去指数:

    10011 x 10-1

    二进制到十进制:

    19 x 2-1

    负除法指数:

    19/21

    相乘指数:

    19/2

    等于:

    9.5



    进一步阅读
  • The Floating-Point Guide: What Every Programmer Should Know About Floating-Point Arithmetic, or, Why don’t my numbers add up?(floating-point-gui.de)
  • What Every Computer Scientist Should Know About Floating-Point Arithmetic(Goldberg 1991)
  • IEEE Double-precision floating-point format(维基百科)
  • Floating Point Arithmetic: Issues and Limitations(docs.python.org)
  • Floating Point Binary
  • 关于floating-point - 为什么 float 不正确?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50456745/

    有关floating-point - 为什么 float 不正确?的更多相关文章

    1. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

      类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

    2. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

      我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

    3. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

      我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

    4. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

      我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

    5. ruby - 为什么 4.1%2 使用 Ruby 返回 0.0999999999999996?但是 4.2%2==0.2 - 2

      为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返

    6. ruby-on-rails - 如何使用 instance_variable_set 正确设置实例变量? - 2

      我正在查看instance_variable_set的文档并看到给出的示例代码是这样做的:obj.instance_variable_set(:@instnc_var,"valuefortheinstancevariable")然后允许您在类的任何实例方法中以@instnc_var的形式访问该变量。我想知道为什么在@instnc_var之前需要一个冒号:。冒号有什么作用? 最佳答案 我的第一直觉是告诉你不要使用instance_variable_set除非你真的知道你用它做什么。它本质上是一种元编程工具或绕过实例变量可见性的黑客攻击

    7. ruby - ruby 中的 TOPLEVEL_BINDING 是什么? - 2

      它不等于主线程的binding,这个toplevel作用域是什么?此作用域与主线程中的binding有何不同?>ruby-e'putsTOPLEVEL_BINDING===binding'false 最佳答案 事实是,TOPLEVEL_BINDING始终引用Binding的预定义全局实例,而Kernel#binding创建的新实例>Binding每次封装当前执行上下文。在顶层,它们都包含相同的绑定(bind),但它们不是同一个对象,您无法使用==或===测试它们的绑定(bind)相等性。putsTOPLEVEL_BINDINGput

    8. ruby - Infinity 和 NaN 的类型是什么? - 2

      我可以得到Infinity和NaNn=9.0/0#=>Infinityn.class#=>Floatm=0/0.0#=>NaNm.class#=>Float但是当我想直接访问Infinity或NaN时:Infinity#=>uninitializedconstantInfinity(NameError)NaN#=>uninitializedconstantNaN(NameError)什么是Infinity和NaN?它们是对象、关键字还是其他东西? 最佳答案 您看到打印为Infinity和NaN的只是Float类的两个特殊实例的字符串

    9. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

      如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

    10. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

      关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

    随机推荐