Каков хороший способ округления значений двойной точности до (несколько) более низкой точности?

Моя проблема заключается в том, что я должен использовать функцию / алгоритм третьей стороны, которая принимает массив значений двойной точности в качестве входных данных, но, очевидно, может быть чувствительным к очень небольшим изменениям во входных данных. Однако для моего приложения я должен получить идентичные результаты для входных данных, которые (почти) идентичны! В частности, у меня есть два тестовых входных массива, которые идентичны до 5-й позиции после десятичной точки, и все же я получаю разные результаты. Так что причина "проблемы" должна быть после 5-й позиции после десятичной точки.

Теперь моя идея заключалась в том, чтобы округлить входные данные с немного меньшей точностью, чтобы получить идентичные результаты от входных данных, которые очень похожи, но не на 100% идентичны. Поэтому я ищу хороший / эффективный способ округления значений двойной точности с немного меньшей точностью. До сих пор я использую этот код для округления до 9-й позиции после десятичной точки:

double x = original_input();
x = double(qRound(x * 1000000000.0)) / 1000000000.0;

Здесь qRound() - нормальная функция округления чисел от двойного до целого числа из Qt. Этот код работает, и он действительно решил мою проблему с двумя "проблемными" наборами тестов. Но: есть ли более эффективный путь к этому?

И что меня беспокоит: округление до 9-й позиции после десятичной точки может быть целесообразным для входных данных, которые находятся в диапазоне от -100,0 до 100,0 (как в случае с моими текущими входными данными). Но это может быть слишком много (например, слишком большая потеря точности) для входных данных, например, в диапазоне от -0,001 до 0,001. К сожалению, я не знаю, в каком диапазоне мои входные значения будут в других случаях...

В конце концов, я думаю, что мне нужно что-то вроде функции, которая выполняет следующее: путем правильного округления обрезает заданное значение двойной точности X до не более LN позиций после десятичной точки, где L- количество позиций после запятой, которую двойная точность может хранить (представлять) для данного значения; и N является фиксированным, как 3. Это означает, что для "маленьких" значений мы бы допускали больше позиций после десятичной точки, чем для "больших" значений. Другими словами, я хотел бы округлить 64-битное значение с плавающей запятой до (несколько) меньшей точности, например 60-битной или 56-битной, а затем сохранить его до 64-битного двойного значения.

Это имеет смысл для вас? И если да, можете ли вы предложить способ (эффективно) сделать это в C++???

Заранее спасибо!

3 ответа

Если вы посмотрите на двухбитную разметку, вы увидите, как объединить ее с небольшим количеством побитовой магии для реализации быстрого (двоичного) округления с произвольной точностью. У вас есть следующая битовая разметка:

SEEEEEEEEEEEFFFFFFFFFFF.......FFFFFFFFFF

где S это бит знака, Es являются экспонентными битами, а Fs - биты дроби. Вы можете сделать битовую маску, как это:

11111111111111111111111.......1111000000

и поразрядно-и (&) два вместе. Результатом является округленная версия исходного ввода:

SEEEEEEEEEEEFFFFFFFFFFF.......FFFF000000

И вы можете контролировать, сколько данных отсекается, изменяя число конечных нулей. Больше нулей = больше округления; меньше = меньше Вы также получаете другой эффект, который вам нужен: на маленькие входные значения влияют пропорционально меньше, чем на большие входные значения, так как тому, какому "месту" соответствует каждый бит, определяет показатель степени.

Надеюсь, это поможет!

Предостережение: это технически усечение, а не истинное округление (все значения станут ближе к нулю, независимо от того, насколько они близки к другому возможному результату), но, надеюсь, это так же полезно в вашем случае.

Бизнес-сценарий не очевиден из вопроса; Тем не менее, я чувствую, что вы пытаетесь увидеть значения в приемлемом диапазоне. Вместо == вы можете проверить, находится ли второе значение в определенном диапазоне% (скажем, +/- 0,001%)

Если процент диапазона не может быть зафиксирован (среднее значение зависит от длины точности; скажем, для 2 десятичных разрядов это нормально, 0,001 процента - это хорошо, но для 4 десятичных разрядов необходимо 0,000001 процента), тогда вы можете получить его как 1/ мантисса.

Спасибо за вклад до сих пор.

Однако после дополнительного поиска я натолкнулся на функции frexp() и ldexp()! Эти функции дают мне доступ к "мантиссе" и "показателю степени" заданного двойного значения, а также могут преобразовать обратно из показателя мантиссы + в двойное значение. Теперь мне просто нужно округлить мантиссу.

double value = original_input();
static const double FACTOR = 32.0;
int exponent;
double temp = double(round(frexp(value, &exponent) * FACTOR));
value = ldexp(temp / FACTOR, exponent);

Я не знаю, насколько это эффективно, но дает разумные результаты:

0.000010000000000   0.000009765625000
0.000010100000000   0.000010375976563
0.000010200000000   0.000010375976563
0.000010300000000   0.000010375976563
0.000010400000000   0.000010375976563
0.000010500000000   0.000010375976563
0.000010600000000   0.000010375976563
0.000010700000000   0.000010986328125
0.000010800000000   0.000010986328125
0.000010900000000   0.000010986328125
0.000011000000000   0.000010986328125
0.000011100000000   0.000010986328125
0.000011200000000   0.000010986328125
0.000011300000000   0.000011596679688
0.000011400000000   0.000011596679688
0.000011500000000   0.000011596679688
0.000011600000000   0.000011596679688
0.000011700000000   0.000011596679688
0.000011800000000   0.000011596679688
0.000011900000000   0.000011596679688
0.000012000000000   0.000012207031250
0.000012100000000   0.000012207031250
0.000012200000000   0.000012207031250
0.000012300000000   0.000012207031250
0.000012400000000   0.000012207031250
0.000012500000000   0.000012207031250
0.000012600000000   0.000012817382813
0.000012700000000   0.000012817382813
0.000012800000000   0.000012817382813
0.000012900000000   0.000012817382813
0.000013000000000   0.000012817382813
0.000013100000000   0.000012817382813
0.000013200000000   0.000013427734375
0.000013300000000   0.000013427734375
0.000013400000000   0.000013427734375
0.000013500000000   0.000013427734375
0.000013600000000   0.000013427734375
0.000013700000000   0.000013427734375
0.000013800000000   0.000014038085938
0.000013900000000   0.000014038085938
0.000014000000000   0.000014038085938
0.000014100000000   0.000014038085938
0.000014200000000   0.000014038085938
0.000014300000000   0.000014038085938
0.000014400000000   0.000014648437500
0.000014500000000   0.000014648437500
0.000014600000000   0.000014648437500
0.000014700000000   0.000014648437500
0.000014800000000   0.000014648437500
0.000014900000000   0.000014648437500
0.000015000000000   0.000015258789063
0.000015100000000   0.000015258789063
0.000015200000000   0.000015258789063
0.000015300000000   0.000015869140625
0.000015400000000   0.000015869140625
0.000015500000000   0.000015869140625
0.000015600000000   0.000015869140625
0.000015700000000   0.000015869140625
0.000015800000000   0.000015869140625
0.000015900000000   0.000015869140625
0.000016000000000   0.000015869140625
0.000016100000000   0.000015869140625
0.000016200000000   0.000015869140625
0.000016300000000   0.000015869140625
0.000016400000000   0.000015869140625
0.000016500000000   0.000017089843750
0.000016600000000   0.000017089843750
0.000016700000000   0.000017089843750
0.000016800000000   0.000017089843750
0.000016900000000   0.000017089843750
0.000017000000000   0.000017089843750
0.000017100000000   0.000017089843750
0.000017200000000   0.000017089843750
0.000017300000000   0.000017089843750
0.000017400000000   0.000017089843750
0.000017500000000   0.000017089843750
0.000017600000000   0.000017089843750
0.000017700000000   0.000017089843750
0.000017800000000   0.000018310546875
0.000017900000000   0.000018310546875
0.000018000000000   0.000018310546875
0.000018100000000   0.000018310546875
0.000018200000000   0.000018310546875
0.000018300000000   0.000018310546875
0.000018400000000   0.000018310546875
0.000018500000000   0.000018310546875
0.000018600000000   0.000018310546875
0.000018700000000   0.000018310546875
0.000018800000000   0.000018310546875
0.000018900000000   0.000018310546875
0.000019000000000   0.000019531250000
0.000019100000000   0.000019531250000
0.000019200000000   0.000019531250000
0.000019300000000   0.000019531250000
0.000019400000000   0.000019531250000
0.000019500000000   0.000019531250000
0.000019600000000   0.000019531250000
0.000019700000000   0.000019531250000
0.000019800000000   0.000019531250000
0.000019900000000   0.000019531250000
0.000020000000000   0.000019531250000
0.000020100000000   0.000019531250000

Кажется, мне нравится то, что я искал в конце концов:

http://img833.imageshack.us/img833/9055/clipboard09.png

Теперь мне просто нужно найти хорошее значение ФАКТОРА для моей функции....

Любые комментарии или предложения?

Я знаю, что этот вопрос довольно старый, но я также искал подход к раунду doubleзначения с меньшей точностью. Может быть, этот ответ кому-то поможет.

Представьте себе число с плавающей запятой в двоичном представлении. Например1101.101. Биты1101 представляют собой целую часть числа и взвешиваются с 2^3, 2^2, 2^1, 2^0слева направо. Биты101 на дробной части взвешиваются с 2^-1, 2^-2, 2^-3, что равно 1/2, 1/4, 1/8.

Так что же такое десятичная ошибка, которую вы производите, когда отрезаете свое число на два бита после десятичной точки? это0.125в этом примере, поскольку бит установлен. Если бит не был установлен, ошибка0. Итак, ошибка<= 0.125.

Теперь подумайте в более общем плане: если бы у вас была бесконечно длинная мантисса, дробная часть сходится к 1 (см. Здесь). На самом деле у вас всего 52 бита (см. Здесь), поэтому сумма "почти" равна 1. Таким образом, отключение всех дробных битов вызовет ошибку<= 1что не удивительно! (Имейте в виду, что ваша неотъемлемая часть также занимает пространство мантиссы! Но если вы примете число вроде1.5который 1.1 в двоичном представлении мантисса хранит только часть после десятичной точки.)

Поскольку отсечение всех дробных битов приводит к ошибке <= 1, отсечение всего, кроме первого бита справа от десятичной запятой, вызывает ошибку <= 1/2 потому что этот бит взвешен 2^-1. Сохранение большего количества бит уменьшает вашу ошибку до<= 1/4.

Это можно описать функцией f(x) = 1/2^(52-x) где x количество отрезанных бит, отсчитываемых с правой стороны и y = f(x) это верхняя граница вашей результирующей ошибки.

Округление на два знака после десятичной точки означает "группировку" чисел по общим сотым. Это можно сделать с помощью указанной выше функции:1/100 >= 1/2^(52-x). Это означает, что ваша результирующая ошибка ограничена сотой долей при отсечении x бит. Решение этого неравенства по x дает:52-log2(100) >= x где 52-log2(100) является 45.36. Это означает, что отрезать не более45 bits обеспечивает "точность" двух десятичных (!) позиций после числа с плавающей запятой.

В общем, ваша мантисса состоит из целой и дробной части. Назовем их длинуi а также f. Положительные показатели описываютi. Кроме того52=f+iдержит. Решение приведенного выше неравенства меняется на:52-i-log2(10^n) >= xпотому что после того, как ваша дробная часть закончится, вы должны перестать отрезать мантиссу! (n здесь десятичная точность.)

Применяя правила логарифма, вы можете вычислить количество максимально разрешенных битов, которые нужно отсечь, следующим образом:

x = f - (uint16_t) ceil(n / 0.3010299956639812); где константа представляет log10(2). Затем можно выполнить усечение с помощью:

mantissa >>= x; mantissa <<= x;

Если x больше чем f, не забудьте переключаться только на f. В противном случае вы повлияете на неотъемлемую часть своей мантиссы.

Другие вопросы по тегам