Быстрый метод округления двойного к 32-битному int объяснил

169

Читая исходный код Lua , я заметил, что Lua использует macroокругление doubleдо 32-разрядного числа int. Я извлек macro, и это выглядит так:

union i_cast {double d; int i[2]};
#define double2int(i, d, t)  \
    {volatile union i_cast u; u.d = (d) + 6755399441055744.0; \
    (i) = (t)u.i[ENDIANLOC];}

Здесь ENDIANLOCопределяется как endianness , 0для little-endian, 1для big-endian. Луа осторожно обращается с порядком байтов. tобозначает целочисленный тип, например intили unsigned int.

Я провел небольшое исследование, и есть более простой формат, macroкоторый использует ту же мысль:

#define double2int(i, d) \
    {double t = ((d) + 6755399441055744.0); i = *((int *)(&t));}

Или в стиле C ++:

inline int double2int(double d)
{
    d += 6755399441055744.0;
    return reinterpret_cast<int&>(d);
}

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

Я написал небольшую программу для проверки:

int main()
{
    double d = -12345678.9;
    int i;
    double2int(i, d)
    printf("%d\n", i);
    return 0;
}

И это выдает -12345679, как и ожидалось.

Я хотел бы вникнуть в детали, как этот хитрый macroработает. Магическое число 6755399441055744.0на самом деле 2^51 + 2^52или 1.5 * 2^52, и 1.5в двоичном виде может быть представлено как 1.1. Когда к этому магическому числу добавляется любое 32-разрядное целое число, я теряюсь отсюда. Как работает этот трюк?

PS: Это в исходном коде Lua, Llimits.h .

ОБНОВЛЕНИЕ :

  1. Как указывает @Mysticial, этот метод не ограничивается 32-разрядным int, он также может быть расширен до 64-разрядного, intесли число находится в диапазоне 2 ^ 52. ( macroНеобходимы некоторые изменения.)
  2. В некоторых материалах говорится, что этот метод нельзя использовать в Direct3D .
  3. При работе с ассемблером Microsoft для x86 macroнаписано еще быстрее assembly(это также извлечено из исходного кода Lua):

    #define double2int(i,n)  __asm {__asm fld n   __asm fistp i}
  4. Для числа одинарной точности существует похожее магическое число: 1.5 * 2 ^23

Ю Хао
источник
3
"быстрый" по сравнению с чем?
Кори Нельсон
3
@CoryNelson Быстрый по сравнению с простым актером. Этот метод при правильной реализации (со встроенными SSE) буквально в сто раз быстрее, чем приведение. (который вызывает неприятный вызов функции для довольно дорогого кода преобразования)
Mysticial
2
Да, я вижу, что это быстрее, чем ftoi. Но если вы говорите SSE, почему бы просто не использовать одну инструкцию CVTTSD2SI?
Кори Нельсон
3
@tmyklebu Многие случаи использования , которые идут double -> int64в самом деле в пределах 2^52диапазона. Они особенно распространены при выполнении целочисленных сверток с использованием БПФ с плавающей точкой.
Мистиаль
7
@MSalters Не обязательно правда. Приведение должно соответствовать спецификации языка - включая правильную обработку переполнения и случаев NAN. (или что-либо еще, указанное компилятором в случае IB или UB) Эти проверки, как правило, очень дороги. Уловка, упомянутая в этом вопросе, полностью игнорирует такие угловые случаи. Поэтому, если вам нужна скорость, а ваше приложение не заботится (или никогда не сталкивается) с такими угловыми случаями, тогда этот хак совершенно уместен.
Мистика

Ответы:

161

А doubleпредставляется так:

двойное представление

и это можно рассматривать как два 32-битных целых числа; Теперь, intпринято во всех версиях коды (предположу , что это 32-битное int) является один справа на рисунке, так что вы делаете в конце концов, просто принимая самый низкий 32 бит мантиссы.


Теперь к магическому номеру; как вы правильно сказали, 6755399441055744 - 2 ^ 51 + 2 ^ 52; добавление такого числа вынуждает « doubleвойти» в «сладкий диапазон» между 2 ^ 52 и 2 ^ 53, который, как объясняется здесь в Википедии , обладает интересным свойством:

Между 2 52 = 4,503,599,627,370,496 и 2 53 = 9,007,199,254,740,992 представимые числа являются в точности целыми числами

Это следует из того, что мантисса имеет ширину 52 бита.

Другой интересный факт о добавлении 2 51 +2 52 заключается в том, что он влияет на мантиссу только в двух старших битах, которые в любом случае отбрасываются, поскольку мы берем только его младшие 32 бита.


Последнее, но не менее важное: знак.

IEEE 754 с плавающей запятой использует представление величины и знака, в то время как целые числа на "обычных" машинах используют арифметику дополнения 2; как это обрабатывается здесь?

Мы говорили только о натуральных числах; теперь предположим, что мы имеем дело с отрицательным числом в диапазоне, представленном 32-битным int, то есть меньше (в абсолютном значении), чем (-2 ^ 31 + 1); назвать его -a. Такое число, очевидно, становится положительным, добавляя магическое число, и полученное значение равно 2 52 +2 51 + (- a).

Теперь, что мы получим, если интерпретируем мантиссу в представлении дополнения 2? Он должен быть результатом суммы дополнения 2 (2 52 +2 51 ) и (-a). Опять же, первый член влияет только на верхние два бита, а то, что остается в битах 0 ~ 50, является представлением дополнения 2 (-a) (опять же, минус два старших бита).

Поскольку уменьшение числа дополнения до 2 до меньшей ширины выполняется простым вырезанием лишних битов слева, взятие младших 32 битов дает нам правильное значение (-a) в 32-битной арифметике дополнения 2.

Matteo Italia
источник
"" "Другой интересный факт о добавлении 2 ^ 51 + 2 ^ 52 заключается в том, что он влияет на мантиссу только в двух старших битах, которые в любом случае отбрасываются, поскольку мы берем только его младшие 32 бита" "" Что это? Добавление этого может сдвинуть всю мантиссу!
YvesgereY
@John: конечно, весь смысл их добавления состоит в том, чтобы заставить значение находиться в этом диапазоне, что, очевидно, может привести к смещению мантиссы (между другими вещами) относительно исходного значения. Я говорил здесь о том, что, как только вы попадаете в этот диапазон, единственными битами, которые отличаются от соответствующего 53-битного целого числа, являются биты 51 и 52, которые в любом случае отбрасываются.
Matteo Italia
2
Для тех, кто хотел бы перейти на int64_tвас, это можно сделать, сместив мантиссу влево и вправо на 13 бит. Это очистит показатель степени и два бита от «магического» числа, но сохранит и распространит знак на все 64-разрядное целое число со знаком. union { double d; int64_t l; } magic; magic.d = input + 6755399441055744.0; magic.l <<= 13; magic.l >>= 13;
Войцех Мигда
Правильно ли я понимаю, что 2 ^ 51 необходим только для обработки отрицательных значений?
Кентзо