Чи можна записати швидку функцію Quake InvSqrt () в Rust?


101

Це просто для задоволення власної цікавості.

Чи є реалізація цього:

float InvSqrt (float x)
{
   float xhalf = 0.5f*x;
   int i = *(int*)&x;
   i = 0x5f3759df - (i>>1);
   x = *(float*)&i;
   x = x*(1.5f - xhalf*x*x);
   return x;
}

в Іржі? Якщо він існує, опублікуйте код.

Я спробував це і не вдалося. Я не знаю, як кодувати число з плаваючою ланкою за допомогою цілого формату. Ось моя спроба:

fn main() {
    println!("Hello, world!");
    println!("sqrt1: {}, ",sqrt2(100f64));
}

fn sqrt1(x: f64) -> f64 {
    x.sqrt()
}

fn sqrt2(x: f64) -> f64 {
    let mut x = x;
    let xhalf = 0.5*x;
    let mut i = x as i64;
    println!("sqrt1: {}, ", i);

    i = 0x5f375a86 as i64 - (i>>1);

    x = i as f64;
    x = x*(1.5f64 - xhalf*x*x);
    1.0/x
}

Довідка:
1. Походження швидкого InvSqrt Quake3 () - Сторінка 1
2. Розуміння швидкого зворотного квадратного кореня Quake
3. ШВИДКОГО ВПРОВАДЖЕНОГО КОРОБУ .pdf
4. вихідний код: q_math.c # L552-L572



4
Як я розумію, цей код є UB в C через порушення суворого правила псевдоніму . Стандартно-блаженним способом виконання такого типу покарань є а union.
trentcl

4
@trentcl: Я також не думаю, що unionпрацює. memcpyбезумовно, працює, хоча це багатослівно.
Матьє М.

14
@MatthieuM. Тип покарання союзами цілком справедливий C , але не дійсний C ++.
Мойра

4
Я вважаю, що це питання добре з точки зору чистої цікавості, але, будь ласка, розумійте, що часи змінилися. На x86, інструкції rsqrtssта rsqrtpsінструкції, представлені Pentium III у 1999 році, швидші та точніші, ніж цей код. ARM NEON має vrsqrteсхоже. І які б обчислення не використовували Quake III, напевно, це було б зроблено на GPU сьогодні.
benrg

Відповіді:


87

Я не знаю, як кодувати число з плаваючою ланкою за допомогою цілого формату.

Для цього є функція: f32::to_bitsяка повертає u32. Існує також функція для іншого напрямку: f32::from_bitsяка бере u32аргумент. Ці функції переважніші, ніж mem::transmuteостанні, unsafeі складні у використанні.

З цим, ось реалізація InvSqrt:

fn inv_sqrt(x: f32) -> f32 {
    let i = x.to_bits();
    let i = 0x5f3759df - (i >> 1);
    let y = f32::from_bits(i);

    y * (1.5 - 0.5 * x * y * y)
}

( Дитячий майданчик )


Ця функція компілюється до наступної збірки на x86-64:

.LCPI0_0:
        .long   3204448256        ; f32 -0.5
.LCPI0_1:
        .long   1069547520        ; f32  1.5
example::inv_sqrt:
        movd    eax, xmm0
        shr     eax                   ; i << 1
        mov     ecx, 1597463007       ; 0x5f3759df
        sub     ecx, eax              ; 0x5f3759df - ...
        movd    xmm1, ecx
        mulss   xmm0, dword ptr [rip + .LCPI0_0]    ; x *= 0.5
        mulss   xmm0, xmm1                          ; x *= y
        mulss   xmm0, xmm1                          ; x *= y
        addss   xmm0, dword ptr [rip + .LCPI0_1]    ; x += 1.5
        mulss   xmm0, xmm1                          ; x *= y
        ret

Я не знайшов жодної довідкової збірки (якщо у вас є, будь ласка, скажіть мені!), Але мені це здається досить гарним. Я просто не впевнений, чому поплавок був переміщений в eaxпросто, щоб зробити зсув і ціле віднімання. Можливо регістри SSE не підтримують ці операції?

clang 9.0 з -O3компілює код C в основному для тієї ж збірки . Тож це хороший знак.


Варто зазначити, що якщо ви насправді хочете використовувати це на практиці: будь ласка, не робіть цього. Як зазначив Бенгр у коментарях , сучасні процесори x86 мають спеціалізовану інструкцію щодо цієї функції, яка є швидшою та точнішою, ніж цей злом. На жаль, 1.0 / x.sqrt() не здається оптимізувати цю інструкцію . Так що, якщо вам дійсно потрібна швидкість, використовуючи самі _mm_rsqrt_psвбудовані функції , ймовірно, шлях. Це, однак, знову вимагає unsafeкод. Я не буду надто детально описуватись у цій відповіді, оскільки меншість програмістів справді потребуватиме її.


4
Згідно з Інструкцією Intel Intrinsics, немає жодної операції зсуву цілих чисел, яка зміщує лише найнижчий 32-розрядний аналог 128-бітного регістру на addssабо mulss. Але якщо інші 96 біт xmm0 можна ігнорувати, то можна скористатися psrldінструкцією. Те саме стосується цілого віднімання.
фазам

Я визнаю, що майже нічого не знаю про іржу, але чи не є "небезпечним" в основному основною властивістю fast_inv_sqrt? Із цілковитою неповагою до типів даних тощо.
Gloweye

12
@Gloweye Ми говоримо про інший тип "небезпечних". Швидке наближення, яке отримує погану цінність занадто далеко від солодкого місця, навпаки, щось грає швидко і вільно з невизначеною поведінкою.
Deduplicator

8
@Gloweye: Математично остання частина цього fast_inv_sqrtлише один ітераційний крок Ньютона-Рафсона, щоб знайти краще наближення inv_sqrt. У цій частині немає нічого небезпечного. Хитрість полягає в першій частині, яка знаходить гарне наближення. Це працює, тому що він робить ціле ділення на 2 на експонентну частину поплавця, і справдіsqrt(pow(0.5,x))=pow(0.5,x/2)
MSalters

1
@fsasm: Це правильно; movdдо EAX і назад - це пропущена оптимізація поточними компіляторами. (І так, виклики конвенцій передають скаляр / повертають скаляр floatу низькому елементі XMM і дозволяють високим бітам бути сміттям. Але зауважте, що якщо він буде розширений нулем, він може легко залишитися таким: правильне зміщення не вводить не- нульові елементи, і ні віднімання _mm_set_epi32(0,0,0,0x5f3759df), тобто movdнавантаження. Вам потрібно movdqa xmm1,xmm0буде скопіювати регістр раніше psrld. Обхід затримки з переадресації інструкцій FP на ціле число і навпаки прихований mulssзатримкою.
Пітер Кордес

37

Цей реалізований з менш відомими unionв Rust:

union FI {
    f: f32,
    i: i32,
}

fn inv_sqrt(x: f32) -> f32 {
    let mut u = FI { f: x };
    unsafe {
        u.i = 0x5f3759df - (u.i >> 1);
        u.f * (1.5 - 0.5 * x * u.f * u.f)
    }
}

Зробив декілька мікро-орієнтирів за допомогою criterionящика на коробці Linux x86-64. Дивно власне Руст sqrt().recip()- це найшвидше. Але звичайно, будь-який результат мікро-орієнтації слід брати із зерном солі.

inv sqrt with transmute time:   [1.6605 ns 1.6638 ns 1.6679 ns]
inv sqrt with union     time:   [1.6543 ns 1.6583 ns 1.6633 ns]
inv sqrt with to and from bits
                        time:   [1.7659 ns 1.7677 ns 1.7697 ns]
inv sqrt with powf      time:   [7.1037 ns 7.1125 ns 7.1223 ns]
inv sqrt with sqrt then recip
                        time:   [1.5466 ns 1.5488 ns 1.5513 ns]

22
Я не принаймні здивований sqrt().inv(), що найшвидший. І sqrt, і inv - це поодинокі інструкції, і проходять досить швидко. Дум був написаний в ті часи, коли не можна було б припустити, що взагалі існує апаратна плаваюча точка, а трансцендентальні функції, такі як sqrt, безумовно, були б програмними. +1 для орієнтирів.
Мартін Боннер підтримує Моніку

4
Що мене дивує, це те, що transmute, мабуть, відрізняється від - to_і from_bitsя очікую, що вони будуть еквівалентними інструкціям ще до оптимізації.
trentcl

2
@MartinBonner (Також не те, що це має значення, але sqrt не є трансцендентальною функцією .)
benrg

4
@MartinBonner: Будь-який апаратний FPU, який підтримує поділ, зазвичай також підтримує sqrt. "Основні" операції IEEE (+ - * / sqrt) необхідні для отримання результату, правильно округленого; ось чому SSE забезпечує всі ці операції, але не exp, sin чи будь-що інше. Насправді, ділення та sqrt зазвичай працюють на одній і тій же одиниці виконання, розробленій аналогічно. Дивіться деталі одиниці HW div / sqrt . У всякому разі, вони все ще не швидкі в порівнянні з множенням, особливо затримки.
Пітер Кордес

1
У будь-якому випадку, Skylake має значно кращий конвеєр для div / sqrt, ніж попередні уарчі. Дивіться поділ з плаваючою комою на множення з плаваючою точкою для деяких витягів із таблиці Agner Fog. Якщо ви не займаєтеся певною роботою в циклі, тому sqrt + div є вузьким місцем, ви можете скористатися швидким зворотним sqrt HW (замість злому хакесу) + ітерацією Ньютона. Особливо з FMA, що добре для пропускної здатності, якщо не затримки. Швидкий векторизований rsqrt та зворотній з SSE / AVX залежно від точності
Пітер Кордес

10

Ви можете використовувати std::mem::transmuteдля здійснення необхідної конверсії:

fn inv_sqrt(x: f32) -> f32 {
    let xhalf = 0.5f32 * x;
    let mut i: i32 = unsafe { std::mem::transmute(x) };
    i = 0x5f3759df - (i >> 1);
    let mut res: f32 = unsafe { std::mem::transmute(i) };
    res = res * (1.5f32 - xhalf * res * res);
    res
}

Ви можете шукати живий приклад тут: тут


4
З небезпечним немає нічого поганого, але є спосіб зробити це без явного небезпечного блоку, тому я б запропонував переписати цю відповідь за допомогою f32::to_bitsі f32::from_bits. Він також несе в собі намір явно не схожий на трансмутацію, яку, мабуть, більшість людей сприймають як "магію".
Sahsahae

5
@Sahsahae Я щойно опублікував відповідь за допомогою двох згаданих вами функцій :) І я згоден, тут unsafeслід уникати, оскільки це не потрібно.
Лукас Калберттт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.