Является ли жесткое кодирование наименее значимого байта двойника хорошей стратегией округления? - PullRequest
0 голосов
/ 18 января 2019

У меня есть функция, выполняющая некоторые математические вычисления и возвращающая double. Это приводит к разным результатам в Windows и Android из-за различий в реализации std::exp ( Почему я получаю специфичный для платформы результат для std :: exp? ). Разница округления e-17 распространяется, и, в конце концов, это не просто разница округления (результаты могут измениться с 2,36 до 2,47 в конце). Поскольку я сравниваю результат с некоторыми ожидаемыми значениями, я хочу, чтобы эта функция возвращала одинаковый результат на всех платформах.

Так что мне нужно округлить мой результат. Самым простым решением для этого, по-видимому (насколько я мог найти в Интернете), является std::ceil(d*std::pow<double>(10,precision))/std::pow<double>(10,precision). Тем не менее, я чувствую, что это может привести к различным результатам в зависимости от платформы (и, кроме того, трудно решить, каким должен быть precision).

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

Этот быстрый тест, похоже, показывает, что "да":

#include <iostream>
#include <iomanip>

double roundByCast( double d )
{
    double rounded = d;
    unsigned char* temp = (unsigned char*) &rounded;
    // changing least significant byte to be always the same
    temp[0] = 128;
    return rounded;
}

void showRoundInfo( double d, double rounded )
{
    double diff = std::abs(d-rounded);
    std::cout << "cast: " << d << " rounded to " << rounded << " (diff=" << diff << ")" << std::endl;
}

void roundIt( double d )
{
    showRoundInfo( d, roundByCast(d) );
}

int main( int argc, char* argv[] )
{
    roundIt( 7.87234042553191493141184764681 );
    roundIt( 0.000000000000000000000184764681 );
    roundIt( 78723404.2553191493141184764681 );
}

Это выводит:

cast: 7.87234 rounded to 7.87234 (diff=2.66454e-14)
cast: 1.84765e-22 rounded to 1.84765e-22 (diff=9.87415e-37)
cast: 7.87234e+07 rounded to 7.87234e+07 (diff=4.47035e-07)

Мой вопрос:

  • Безопасно ли unsigned char* temp = (unsigned char*) &rounded или здесь неопределенное поведение и почему?
  • Если UB отсутствует (или если есть лучший способ сделать это без UB), является ли такая круглая функция безопасной и точной для всего ввода?

Примечание: я знаю, что числа с плавающей точкой неточны. Пожалуйста, не отмечайте как дубликат Математика с плавающей запятой не работает? или Почему числа с плавающей запятой неточны? . Я понимаю, почему результаты отличаются, я просто ищу способ сделать их идентичными на всех целевых платформах.


Редактировать, я могу переформулировать свой вопрос, поскольку люди спрашивают, почему у меня разные ценности и почему я хочу, чтобы они были одинаковыми.

Допустим, вы получаете double от вычисления, которое может привести к другому значению из-за реализаций, специфичных для платформы (например, std::exp). Если вы хотите исправить эти разные double, чтобы в конечном итоге они имели одинаковое представление памяти (1) на всех платформах, и вы хотите потерять как можно меньшую точность, то является ли исправление младшего значащего байта хорошим подходом? (потому что я чувствую, что округление до произвольной заданной точности может привести к потере большего количества информации, чем этот трюк).

(1) Под «одним и тем же представлением» я имею в виду, что если вы преобразуете его в std::bitset, вы захотите увидеть одинаковую последовательность битов для всех платформ.

Ответы [ 4 ]

0 голосов
/ 19 января 2019

То, что вы делаете, совершенно неверно.

Ваша проблема не в том, что вы получаете разные результаты (2,36 против 2,47). Ваша проблема в том, что, по крайней мере, один из этих результатов, и, вероятно, оба, имеют серьезные ошибки. Ваши результаты для Windows и Android не просто отличаются, они НЕПРАВИЛЬНЫ. (По крайней мере, один из них, и вы не знаете, какой из них).

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

То, что вы пытаетесь, просто увеличивает ошибки округления в 256 раз, и если два разных результата заканчиваются в шестнадцатеричном .... 1ff и .... 200, то вы меняете их на .... 180 и ... 0,280, так что даже разница между немного разными числами может возрасти в 256 раз.

И на бигендовской машине твой код просто пойдет kaboom !!!

0 голосов
/ 18 января 2019

Ваша функция не будет работать из-за псевдонимов.

double roundByCast( double d )
{
    double rounded = d;
    unsigned char* temp = (unsigned char*) &rounded;
    // changing least significant byte to be always the same
    temp[0] = 128;
    return rounded;
}

Приведение к unsigned char * для temp разрешено, поскольку приведение char * является исключением из правил наложения имен. Это необходимо для таких функций, как read, write, memcpy и т. Д., Чтобы они могли копировать значения в представления байтов и из них.

Однако вам не разрешено писать в temp [0], а затем предполагать, что округленное изменилось. Вы должны создать новую двойную переменную (в стеке все нормально) и memcpy temp вернуться к ней.

0 голосов
/ 19 января 2019

Является ли unsigned char * temp = (unsigned char *) и округлено ли безопасно, или здесь есть неопределенное поведение, и почему?

Это хорошо определено, так как псевдоним через unsigned char является разрешенным .

является ли такая круглая функция безопасной и точной для всего ввода?

Нет. Вы не можете идеально решить эту проблему с усечением / округлением. Учтите, что одна реализация дает 0x.....0ff, а другая 0x.....100. Установка lsb на 0x00 приведет к тому, что исходная разница в 1 ульп составит 256 ульпов.

Никакой алгоритм округления не может это исправить.

У вас есть два варианта:

  • не использовать плавающую точку, используйте другой способ (например, с фиксированной точкой)
  • встраивает библиотеку с плавающей запятой в ваше приложение, которое использует только базовую арифметику с плавающей запятой (+, -, *, /, sqrt) и не использует -ffast-math или любую другую эквивалентную опцию. Таким образом, если вы работаете на платформе, совместимой с IEEE-754, результаты с плавающей запятой должны быть такими же, как в IEEE-754, согласно которым базовые операции должны рассчитываться «идеально». Это означает, что операция рассчитана с бесконечной точностью, а затем округлена до полученного представления.

Кстати, если входная 1e-17 разница означает огромную выходную разницу, то ваша проблема / алгоритм плохо обусловлены , чего обычно следует избегать, так как обычно оно не дает значимых результатов. .

0 голосов
/ 18 января 2019

Нет, округление не является стратегией для устранения мелких ошибок или гарантии соответствия с вычислениями, выполненными с ошибками.

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

В вашем конкретном случае жесткого кодирования младшего байта, самые близкие значения

0x1.mmmmmmm100

и

0x1.mmmmmmm0ff

имеют отклонение только одного ULP ... но после вашего округления они отличаются на 256 ULP. Oops!

...