-1.0 和 1.0 之间的 double 有多少位精度?

Posted

技术标签:

【中文标题】-1.0 和 1.0 之间的 double 有多少位精度?【英文标题】:How many bits of precision for a double between -1.0 and 1.0? 【发布时间】:2012-03-31 21:35:52 【问题描述】:

在我一直在查看的一些音频库中,音频样本通常表示为范围为 -1.0 到 1.0 的双精度数或浮点数。在某些情况下,这很容易让分析和综合代码抽象出底层数据类型实际上可能是什么(signed long int、unsigned char 等)。

假设 IEEE 754,我们有不均匀的密度。随着数字接近零,密度增加。这意味着我们对于接近 -1 和 1 的数字的精度较低。

如果我们可以为我们正在转换的基础数据类型表示足够数量的值,那么这种非均匀的数字密度就无关紧要了。

例如,如果底层数据类型是无符号字符,我们只需要介于 -1 和 1(或 8 位)之间的 256 个值——使用双精度显然不是问题。

我的问题是:我有多少位精度?我可以安全地转换为 32 位整数/从 32 位整数转换而不会丢失吗?为了扩展这个问题,值的范围必须是多少才能安全地转换为 32 位整数而不会丢失?

谢谢!

【问题讨论】:

为什么接近零的数字精度较低? @DavidHeffernan:不,根据我对 IEEE 754 标准的理解,随着您的数字接近零,您应该有更高的精度。这是因为与整数不同,数字是以科学记数法存储的。 如何在 32 位整数和 -1 和 1 范围内的浮点值之间进行转换?而且你在不同的尺度上没有更高的精度。不管比例是多少,你只有 53 位。 【参考方案1】:

对于 IEEE 双精度数,您有一个 53 位尾数,足以表示 32 位整数,它们被视为介于 -1 (0x80000000) 和 1 - 2^-31 (0x7FFFFFFF) 之间的定点数。

浮点数有 24 位尾数,这还不够。

【讨论】:

假设二进制补码和固定点的通常映射,-1 应该是0x80000000。否则,就当场了。【参考方案2】:

正如 Alexandre C. 解释的那样,IEEE doubles 有一个 53 位尾数(存储 52 位,隐含最高位),浮点数有 24 位(存储 23 位,隐含最高位)。

编辑:(感谢反馈,我希望这更清楚)

当整数转换为双精度 double f = (double)1024; 时,该数字以适当的指数 (1023+10) 保存,并且 same 位模式有效地存储为原始整数(实际上IEEE二进制浮点不存储最高位。IEEE浮点数被“标准化”为最高位= 1,通过调整指数,然后顶部1被修剪掉,因为它是“隐含”的,这样可以节省一点存储)。

一个 32 位整数将需要一个双精度来完美地保存它的值,而一个 8 位整数将完美地保存在一个浮点数中。那里没有信息丢失。它可以无损失地转换回整数。损失发生在算术和小数值上。

除非代码这样做,否则整数不会映射到 +/-1。当代码将存储为双精度的 32 位整数除以将其映射到范围 +/-1 时,很可能会引入错误。

映射到 +/-1 会降低 53 位精度,但错误只会出现在最低位,远低于原始整数所需的 32 位。后续操作也可能会丢失精度。例如,将两个数字与超过 53 位精度的结果范围相乘会丢失一些位(即,将两个数字与尾数的超过 27 个有效位相乘)。

可能有用的浮点解释是"What Every Computer Scientist Should Know About Floating-Point Arithmetic" 它解释了浮点数的一些反直觉(对我而言)行为。

例如,数字 0.1不能精确地保存在 IEEE 二进制浮点双精度数中。

这个程序可能会帮助您了解正在发生的事情:

/* Demonstrate IEEE 'double' encoding on x86
 * Show bit patterns and 'printf' output for double values
 * Show error representing 0.1, and accumulated error of adding 0.1 many times
 * G Bulmer 2012
 */


#include <stdio.h>

typedef struct 
    unsigned long long mantissa :52; 
    unsigned exponent :11; 
    unsigned sign :1; 
 double_bits;
const unsigned exponent_offset = 1023;

typedef union  double d; unsigned long long l; double_bits b;  Xlate;

void print_xlate(Xlate val) 
    const long long IMPLIED = (1LL<<52);
    if (val.b.exponent == 0)  /* zero? */
        printf("val: d: %19lf  bits: %016llX [sign: %u exponent: zero=%u mantissa: %llX]\n", 
               val.d, val.l, val.b.sign, val.b.exponent, val.b.mantissa);
     else 
        printf("val: d: %19lf  bits: %016llX [sign: %u exponent: 2^%4-d mantissa: %llX]\n", 
               val.d, val.l, val.b.sign, ((int)val.b.exponent)-exponent_offset, 
                                                    (IMPLIED|val.b.mantissa));
    



double add_many(double d, int many) 
    double accum = 0.0;
    while (many-- > 0)     /* only works for +d */
        accum += d;
    
    return accum;


int main (int argc, const char * argv[]) 
    Xlate val;
    val.b.sign = 0;
    val.b.exponent = exponent_offset+1;
    val.b.mantissa = 0;

    print_xlate(val);

    val.d = 1.0;                        print_xlate(val);

    val.d = 0.0;                        print_xlate(val);

    val.d = -1.0;                       print_xlate(val);

    val.d = 3.0;                        print_xlate(val);

    val.d = 7.0;                        print_xlate(val);

    val.d = (double)((1LL<<31)-1LL);    print_xlate(val);

    val.d = 2147483647.0;               print_xlate(val);

    val.d = 10000.0;                    print_xlate(val);

    val.d = 100000.0;                   print_xlate(val);

    val.d = 1000000.0;                  print_xlate(val);

    val.d = 0.1;                        print_xlate(val);

    val.d = add_many(0.1, 100000);
    print_xlate(val);

    val.d = add_many(0.1, 1000000);
    print_xlate(val);

    val.d = add_many(0.1, 10000000);
    print_xlate(val);

    val.d = add_many(0.1,10);           print_xlate(val);
    val.d *= 2147483647.0;              print_xlate(val);
    int i = val.d;                      printf("int i=truncate(d)=%d\n", i);
    int j = lround(val.d);              printf("int i=lround(d)=%d\n", j);

    val.d = add_many(0.0001,1000)-0.1;  print_xlate(val);

    return 0;

输出是:

val: d:            2.000000  bits: 4000000000000000 [sign: 0 exponent: 2^1    mantissa: 10000000000000]
val: d:            1.000000  bits: 3FF0000000000000 [sign: 0 exponent: 2^0    mantissa: 10000000000000]
val: d:            0.000000  bits: 0000000000000000 [sign: 0 exponent: zero=0 mantissa: 0]
val: d:           -1.000000  bits: BFF0000000000000 [sign: 1 exponent: 2^0    mantissa: 10000000000000]
val: d:            3.000000  bits: 4008000000000000 [sign: 0 exponent: 2^1    mantissa: 18000000000000]
val: d:            7.000000  bits: 401C000000000000 [sign: 0 exponent: 2^2    mantissa: 1C000000000000]
val: d:   2147483647.000000  bits: 41DFFFFFFFC00000 [sign: 0 exponent: 2^30   mantissa: 1FFFFFFFC00000]
val: d:   2147483647.000000  bits: 41DFFFFFFFC00000 [sign: 0 exponent: 2^30   mantissa: 1FFFFFFFC00000]
val: d:        10000.000000  bits: 40C3880000000000 [sign: 0 exponent: 2^13   mantissa: 13880000000000]
val: d:       100000.000000  bits: 40F86A0000000000 [sign: 0 exponent: 2^16   mantissa: 186A0000000000]
val: d:      1000000.000000  bits: 412E848000000000 [sign: 0 exponent: 2^19   mantissa: 1E848000000000]
val: d:            0.100000  bits: 3FB999999999999A [sign: 0 exponent: 2^-4   mantissa: 1999999999999A]
val: d:        10000.000000  bits: 40C388000000287A [sign: 0 exponent: 2^13   mantissa: 1388000000287A]
val: d:       100000.000001  bits: 40F86A00000165CB [sign: 0 exponent: 2^16   mantissa: 186A00000165CB]
val: d:       999999.999839  bits: 412E847FFFEAE4E9 [sign: 0 exponent: 2^19   mantissa: 1E847FFFEAE4E9]
val: d:            1.000000  bits: 3FEFFFFFFFFFFFFF [sign: 0 exponent: 2^-1   mantissa: 1FFFFFFFFFFFFF]
val: d:   2147483647.000000  bits: 41DFFFFFFFBFFFFF [sign: 0 exponent: 2^30   mantissa: 1FFFFFFFBFFFFF]
int i=truncate(d)=2147483646
int i=lround(d)=2147483647
val: d:            0.000000  bits: 3CE0800000000000 [sign: 0 exponent: 2^-49  mantissa: 10800000000000]

这表明完整的 32 位 int 被精确表示,而 0.1 则不是。它表明 printf 不精确打印浮点数,而是四舍五入或截断(要警惕的事情)。它还说明了 0.1 表示中的错误量在 1,000,000 次加法操作中没有累积到足够大的值以导致 printf 打印它。它表明可以通过舍入来恢复原始整数,但不能通过赋值来恢复,因为赋值会截断。它表明减法运算可以“放大”错误(减法后剩下的都是错误),因此应该仔细分析算术。

把它放到音乐环境中,采样率可能是 96KHz。在错误累积到足以导致前 32 位包含超过 1 位错误之前,需要 10 秒以上的添加。

进一步。创建 Ogg 和 Vorbis 的 Christopher “Monty” Montgomery 在一篇关于音乐、采样率和采样分辨率的文章中认为,对于音频来说,24 位应该绰绰有余24/192 Music Downloads ...and why they make no sense

总结 double 完美地保存 32 位整数。有 N/M 形式的有理十进制数(其中 M 和 N 可以用 32 位整数表示)可以用二进制小数位的有限序列表示。因此,当一个整数映射到范围 +/-1 并因此转换为有理数 (N/M) 时,某些数字不能用双精度小数部分中的有限位数表示,因此错误会蔓延在。

这些错误通常非常小,在最低位中,因此远低于高 32 位。所以它们可以通过四舍五入在整数和双精度之间来回转换,双精度表示的错误不会导致整数错误。但是,算术可以改变错误。不正确构造的算术会导致错误迅速增长,并可能增长到原始整数值已损坏的程度。

其他想法: 如果精度很重要,还有其他方法可以使用双精度数。它们都不像映射到 +/-1 那样方便。我能想到的一切都需要跟踪算术运算,这最好使用 C++ 包装类来完成。这会大大减慢计算速度,因此可能毫无意义。

这是一种非常狡猾的'Automatic Diferentiation' 方法,方法是将算术包装在跟踪额外信息的类中。我认为那里的想法可能会激发一种方法。它甚至可能有助于确定精度丢失的位置。

【讨论】:

伙计们 _ 我真的很想知道为什么这个答案被否决了。我认为有两个不同的问题。保存的位数,以及整数如何映射到表示。将适合表示尾数的整数保持不变。丢失信息的是到 +/-1 的映射。所以我认为这是需要注意的。 +1 我同意。双尾数有 53 位的事实在这里确实有帮助。定点(即 int 表示)和浮点之间存在巨大的脱节。 @David Heffernan - 非常感谢您的 +1。冒着碰运气的风险,也许您的经验和“新鲜的眼睛”可以说明为什么我的回答被否决了?我才来这里几个星期,所以我知道我不知道很多东西。我没有在常见问题解答或谷歌搜索中找到建议。有什么建议吗? 我不确定。有时,当人们不理解您时,您会被否决。坦率地说,你的回答有点不透明。我想我知道你在说什么,但我不是 100% 确定。 @David Heffernan - 谢谢!这一个有用的观察。我又读了一遍,我想我明白你的意思了。我需要学习写得更清楚更简洁。假设太多,不够简单 :-) 感谢您抽出宝贵时间回复。

以上是关于-1.0 和 1.0 之间的 double 有多少位精度?的主要内容,如果未能解决你的问题,请参考以下文章

关于解决double类型数据显示问题的解决

UVALive 6911Double Swords 树状数组

在double,int,long和short类型中,有多少可以填充空白以使此代码输出0? [重复]

double和char之间怎样转换?谢谢!

Single与Double分别能保留都少位小数?多少位数? 如题

C语言中float与double在程序中输出的分别是多少位?