-1.0和1.0之间的双倍精度是多少?

时间:2012-03-31 21:35:53

标签: c casting double precision

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

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

如果我们可以为我们要转换的基础数据类型表示足够数量的值,则此非均匀数密度无关紧要。

例如,如果底层数据类型是unsigned char,我们只需要介于-1和1(或8位)之间的256个值 - 使用double显然不是问题。

我的问题是:我有多少精度?我可以安全地转换为32位整数,而不会丢失吗?为了扩展这个问题,在没有丢失的情况下,安全地转换为32位整数的值必须是多少?

谢谢!

2 个答案:

答案 0 :(得分:7)

对于IEEE双打,你有一个53位的尾数,这足以表示被认为是-1(0x80000000)和1 - 2 ^ -31(0x7FFFFFFF)之间的固定点数的32位整数。

Floats有24位mantissae,这还不够。

答案 1 :(得分:4)

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

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

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

一个32位整数将需要一个double来保持其值完美,并且一个8位整数将完全保存在 。那里没有信息丢失。它可以转换回整数而不会丢失。算术和小数值会发生损失。

除非代码执行,否则整数不会映射到+/- 1。当代码将32位整数(存储为double)存储为将其映射到+/- 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)时,某些数字不能用双倍小数部分中的有限位数来表示,因此误差将会发生变化in。

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

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

这是一种非常偷偷摸摸的方式,通过在跟踪额外信息的类中包装算术来执行'Automatic Diferentiation'。我认为那里的想法可能激发一种方法。它甚至可以帮助确定丢失精度的位置。