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

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

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

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

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

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

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

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

Спасибо заранее!

Ответ 1

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

SEEEEEEEEEEEFFFFFFFFFFF.......FFFFFFFFFF

где S - знаковый бит, E - это биты экспоненты, а F - бит бит. Вы можете сделать такую ​​маску следующим образом:

11111111111111111111111.......1111000000

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

SEEEEEEEEEEEFFFFFFFFFFF.......FFFF000000

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

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

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

Ответ 2

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

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

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

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

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

Ответ 3

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

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

Ответ 4

Я знаю, что этот вопрос довольно старый, но я также искал подход к округлению 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 бит обеспечивает "точность" двух десятичных (!) Позиций после плавающей запятой.

В общем, ваша мантисса состоит из целой и дробной частей. Позвольте назвать их длины 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. В противном случае вы будете влиять на вашу неотъемлемую часть мантиссы.