Сколько бит точности для удвоения между -1,0 и 1,0? - PullRequest
3 голосов
/ 01 апреля 2012

В некоторых аудиобиблиотеках, на которые я смотрел, сэмпл аудио часто представляется в виде двойного числа или числа с плавающей точкой в ​​диапазоне от -1,0 до 1,0. В некоторых случаях это позволяет легко анализировать и синтезировать код, чтобы абстрагироваться от того, каким может быть базовый тип данных (подписанный long int, беззнаковый char и т. Д.).

Предполагая IEEE 754, мы имеем неоднородную плотность. Когда число приближается к нулю, плотность увеличивается. Это означает, что у нас меньше точности для чисел, приближающихся к -1 и 1.

Эта неоднородная плотность чисел не имеет значения, если мы можем представить достаточное количество значений для базового типа данных, который мы конвертируем в / из.

Например, если базовый тип данных был беззнаковым символом, нам нужно только 256 значений от -1 до 1 (или 8 бит) - использование двойного символа явно не проблема.

Мой вопрос: сколько у меня битов точности? Могу ли я безопасно конвертировать в / из 32-битного целого числа без потерь? Чтобы расширить вопрос, каким должен быть диапазон значений для безопасного преобразования в / из 32-разрядного целого числа без потерь?

Спасибо!

Ответы [ 2 ]

7 голосов
/ 01 апреля 2012

Для двойников IEEE у вас есть 53-битная мантисса, которой достаточно для представления 32-битных целых чисел, рассматриваемых как числа с фиксированной запятой между -1 (0x80000000) и 1 - 2 ^ -31 (0x7FFFFFFF).

У поплавков есть 24-битные мантиссы, чего недостаточно.

4 голосов
/ 01 апреля 2012

Как объясняет Александр С., IEEE удваивается , имеет 53-битную мантиссу (52 хранятся и подразумевается старший бит), а float имеет 24 бита (хранится 23 бита и подразумевается старший бит).

Редактировать: (Спасибо за отзыв, надеюсь, это понятнее)

Когда целое число преобразуется в двойное double f = (double)1024;, число сохраняется с соответствующим показателем степени (1023 + 10), и тот же самый битовый шаблон эффективно сохраняется как исходное целое число (Фактически IEEE двоичная с плавающей запятой не хранит верхний бит. Числа с плавающей запятой IEEE «нормализуются», чтобы иметь верхний бит = 1, путем корректировки показателя степени, тогда верхний 1 обрезается, потому что он «подразумевается», что сохраняет немного хранение). * * +1010

32-битное целое число потребует, чтобы удвоенное число содержало его значение идеально , а 8-битное целое число будет удерживаться идеально в float. Там нет потери информации там. Он может быть преобразован обратно в целое число без потерь. Потеря происходит с арифметическими и дробными значениями.

Целое число не отображается на +/- 1, если код не делает это. Когда код делит 32-битное целое число, сохраненное как двойное, чтобы отобразить его в диапазоне +/- 1, очень вероятно, что возникнет ошибка.

Это отображение на +/- 1 потеряет часть 53-битной точности, но ошибка будет только в младших битах, что значительно ниже 32 бит, необходимых для исходного целого числа. Последующие операции могут также потерять точность. Например, умножение двух чисел с результирующим диапазоном точности более 53 битов приведет к потере некоторых битов (т.е. умножение двух чисел на более чем 27 значащих битов мантиссы).

Объяснение с плавающей запятой, которое может быть полезным, это «Что должен знать каждый компьютерный специалист об арифметике с плавающей запятой» Это объясняет некоторые противоречивые (для меня) поведения чисел с плавающей запятой.

Например, число 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 распечатать его. Это показывает, что исходное целое число может быть восстановлено округлением, но не присваиванием, потому что присваивание усекается. Это показывает, что операция вычитания может «усиливать» ошибку (все, что остается после этого вычитания, является ошибкой), и, следовательно, арифметику следует тщательно анализировать.

Чтобы поместить это в контекст музыки, где частота дискретизации может быть 96 кГц. Для того, чтобы ошибка накапливалась достаточно, чтобы ошибки составляли более 1 бита, потребовалось бы более 10 секунд сложений.

Далее. Кристофер «Монти» Монтгомери, который создал Ogg и Vorbis, утверждает, что 24 статьи должно быть более чем достаточно для аудио в статье о музыке, частоте дискретизации и разрешении семплов 24/192 загрузок музыки ... и почему они не имеют смысла

Краткое описание
double отлично держит 32-битные целые числа Существуют рациональные десятичные числа в форме N / M (где M и N могут быть представлены 32-битным целым числом), которые могут , а не быть представленной конечной последовательностью битов двоичной дроби. Таким образом, когда целое число отображается в диапазон +/- 1 и, следовательно, преобразуется в рациональное число (N / M), некоторые числа не могут быть представлены конечным числом битов в дробной части двойного числа, поэтому ошибки будут ползти дюйма

Эти ошибки, как правило, очень малы, в младших битах, следовательно, значительно ниже старших 32 бит.Таким образом, они могут быть преобразованы назад и вперед между целым числом и двойным с использованием округления, и ошибка двойного представления не приведет к тому, что целое число будет неправильным.НО, арифметика может изменить ошибку.Неправильно построенная арифметика может привести к быстрому росту ошибок и может возрасти до величины, при которой искажено исходное целочисленное значение.

Другие соображения: если точность критична, вы можете использовать другие способыдвойники.Ни один из них не так удобен, как отображение на +/- 1.Все, что я могу придумать, потребует отслеживания арифметических операций, что лучше всего делать с использованием классов-оболочек C ++.Это может значительно замедлить вычисления, поэтому может быть бессмысленным.

Это очень хитрый способ сделать 'Автоматическое дифференцирование' , заключив арифметику в классы, которые отслеживаютДополнительная информация.Я думаю, что идеи там могут вдохновить подход.Это может даже помочь определить, где потеряна точность.

...