Швидкий метод округлення дубля до 32-бітного int пояснив


169

Читаючи вихідний код Lua , я помітив, що Lua використовує a macroдля округлення a 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визначається як ендіанство , 0для маленького ендіана, 1для великого ендіана. Луа дбайливо поводиться з витримкою. 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
"швидкий" порівняно з чим?
Cory Nelson

3
@CoryNelson Швидкий порівняно з простим акторським складом. Цей метод при правильному впровадженні (із внутрішніми характеристиками SSE) досить буквально в сто разів швидший, ніж у ролях. (що викликає неприємний виклик функції до досить дорогого коду перетворення)
Mysticial

2
Правильно - я бачу, що це швидше, ніж ftoi. Але якщо ви говорите про SSE, то чому б просто не використовувати єдину інструкцію CVTTSD2SI?
Cory Nelson

3
@tmyklebu Багато випадків використання, double -> int64які дійсно є, дійсно знаходяться в 2^52межах. Вони особливо часто зустрічаються при виконанні цілочислових згортків, використовуючи ПНП з плаваючою комою.
Містичний

7
@MSalters Не обов'язково правда. Лист повинен відповідати мовним вимогам - включаючи належне поводження зі справами переповнення та NAN. (або все, що вказав компілятор у випадку IB або UB) Ці перевірки, як правило, дуже дорогі. Згаданий у цьому питанні трюк повністю ігнорує такі кутові випадки. Тож якщо ви хочете швидкості, і ваша програма не хвилює (або ніколи не стикається) з такими кутовими кейсами, тоді цей злом цілком підходить.
Містичний

Відповіді:


161

A 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 + (- а).

Тепер, що ми отримуємо, якщо інтерпретувати мантію у представленні доповнення 2? Він повинен бути результатом суми доповнення 2 (2 52 +2 51 ) і (-a). Знову ж таки, перший додаток стосується лише верхніх двох бітів, що залишається у бітах 0 ~ 50 - це доповнення 2-го доповнення (-a) (знову ж таки, мінус два верхніх біта).

Оскільки зменшення числа доповнення 2 на меншу ширину відбувається лише шляхом відрізання зайвих біт зліва, взяття нижнього 32 біта дає нам правильно (-а) 32-бітну, арифметичну доповнення 2-х.


"" "Іншим цікавим фактом щодо додавання 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;
Войцех Мігда
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.