เป็นไปได้ไหมที่จะเขียนฟังก์ชัน InvSqrt () ของ Quake ใน 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. ต้นกำเนิดของ Fast InvSqrt () ของ Quake3 - หน้า 1
2. ทำความเข้าใจกับรากที่สองของ Quake ได้อย่างรวดเร็ว
3. FAST INVERSE SQUARE ROOT.pdf
4. ซอร์สโค้ด: q_math.c # L552-L572



4
ตามที่ผมเข้าใจมันรหัสนี้ UB ใน C เนื่องจากการละเมิดกฎ aliasing เข้มงวด unionวิธีมาตรฐานมีความสุขในการดำเนินการชนิดของประเภทเล่นสำนวนนี้อยู่ด้วย
trentcl

4
@trentcl: ฉันไม่คิดว่าจะunionทำงานอย่างใดอย่างหนึ่ง memcpyใช้งานได้ดีแม้ว่ามันจะเป็น verbose
Matthieu M.

14
@MatthieuM การสะกดคำด้วยสหภาพแรงงานประเภทนี้ใช้ได้อย่างสมบูรณ์ Cแต่ไม่ใช่ C ++ ที่ถูกต้อง
Moira

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 ไม่สนับสนุนการดำเนินการเหล่านั้น

เสียงดังกราว 9.0 -O3compiles รหัส C เพื่อพื้นการชุมนุมเดียวกัน นั่นเป็นสัญญาณที่ดี


เป็นมูลค่าชี้ให้เห็นว่าถ้าคุณต้องการใช้สิ่งนี้ในทางปฏิบัติ: โปรดอย่า benrg ชี้ให้เห็นในความคิดเห็นซีพียู x86 ที่ทันสมัยมีคำสั่งพิเศษสำหรับฟังก์ชั่นนี้ซึ่งเร็วกว่าและแม่นยำกว่าแฮ็คนี้ น่าเสียดายที่1.0 / x.sqrt() ดูเหมือนจะไม่ได้ปรับการเรียนการสอนให้เหมาะสม ดังนั้นหากคุณต้องการความเร็วจริงๆใช้intrinsicsน่าจะเป็นวิธีที่จะไป อย่างไรก็ตามสิ่งนี้ต้องการรหัสอีกครั้ง ฉันจะไม่ลงรายละเอียดมากนักในคำตอบนี้เนื่องจากโปรแกรมเมอร์ส่วนน้อยจะต้องการมัน_mm_rsqrt_psunsafe


4
ตามที่อินเทล Intrinsics คู่มือไม่มีการดำเนินการเปลี่ยนแปลงจำนวนเต็มเท่านั้นที่จะเลื่อนต่ำสุด 32 บิต 128 บิตทะเบียนอนาล็อกหรือaddss mulssแต่ถ้าละเว้นอีก 96 บิตของ xmm0 จะสามารถใช้psrldคำสั่งนั้นได้ กันไปสำหรับการลบจำนวนเต็ม
fsasm

ฉันจะยอมรับว่าไม่รู้เรื่องการเป็นสนิม แต่ไม่ใช่ "ไม่ปลอดภัย" โดยทั่วไปเป็นคุณสมบัติหลักของ fast_inv_sqrt ด้วยความไม่เคารพทั้งหมดสำหรับประเภทข้อมูลและเช่น
Gloweye

12
@Gloweye มันแตกต่างจาก "ไม่ปลอดภัย" ที่เราพูดถึง การประมาณที่รวดเร็วซึ่งได้รับค่าที่ไม่ดีไกลจากจุดที่น่าสนใจเมื่อเทียบกับสิ่งที่เล่นอย่างรวดเร็วและหลวมด้วยพฤติกรรมที่ไม่ได้กำหนด
Deduplicator

8
@Gloweye: ศาสตร์ส่วนสุดท้ายของการว่าfast_inv_sqrtเป็นเพียงหนึ่งในขั้นตอนการทำซ้ำ Newton-Raphson inv_sqrtจะหาประมาณที่ดีขึ้นของ ไม่มีอะไรที่ไม่ปลอดภัยเกี่ยวกับส่วนนั้น เล่ห์เหลี่ยมอยู่ในส่วนแรกซึ่งพบการประมาณที่ดี ใช้งานได้เพราะมันทำการหารด้วย 2 ในส่วนเลขชี้กำลังของ float และแน่นอน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คัดลอก reg ก่อนpsrldบายพาส latency จากคำสั่ง FP ที่ส่งต่อไปยังจำนวนเต็มและในทางกลับกันจะถูกซ่อนไว้โดยmulsslatency #
Peter Cordes

37

สิ่งนี้ถูกนำไปใช้งานโดยที่ไม่ค่อยมีคนรู้จักunionในสนิม:

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ลังบนกล่อง x86-64 Linux น่าประหลาดใจที่ Rust เป็นของตัวเอง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 เป็นคำสั่งเดียวและดำเนินไปอย่างรวดเร็ว Doom ถูกเขียนขึ้นในวันที่มันไม่ปลอดภัยที่จะสมมติว่ามีจุดลอยตัวของฮาร์ดแวร์อยู่และฟังก์ชั่นที่ยอดเยี่ยมเช่น sqrt จะเป็นซอฟต์แวร์อย่างแน่นอน +1 สำหรับการวัดประสิทธิภาพ
Martin Bonner สนับสนุน Monica

4
สิ่งที่ทำให้ฉันประหลาดใจtransmuteคือเห็นได้ชัดว่าแตกต่างจากto_และfrom_bits- ฉันคาดหวังว่าสิ่งเหล่านั้นจะเทียบเท่าคำสั่งก่อนที่จะปรับให้เหมาะสม
trentcl

2
@ มาร์ตินโบนเนอร์ (ไม่ว่ามันจะสำคัญ แต่ sqrt ไม่ใช่ฟังก์ชั่นที่ยอดเยี่ยม )
30909

4
@MartinBonner: ฮาร์ดแวร์ FPU ใด ๆ ที่สนับสนุนการหารโดยปกติจะรองรับ sqrt ด้วย จำเป็นต้องใช้การดำเนินการ "พื้นฐาน" (+ - * / sqrt) เพื่อสร้างผลลัพธ์ที่ถูกต้อง นั่นเป็นเหตุผลที่ SSE ให้การดำเนินการทั้งหมด แต่ไม่เปิดเผยบาปหรืออะไรก็ตาม ในความเป็นจริงโดยทั่วไปแล้วการหารและ sqrt จะทำงานในหน่วยการดำเนินการเดียวกันซึ่งออกแบบในลักษณะเดียวกัน ดูHW div / รายละเอียดหน่วย อย่างไรก็ตามพวกเขายังไม่เร็วเมื่อเทียบกับการคูณโดยเฉพาะในเวลาแฝง
Peter Cordes

1
อย่างไรก็ตาม Skylake มีการวางท่อสำหรับ div / sqrt ได้ดีกว่า uarches รุ่นก่อน ๆ อย่างมีนัยสำคัญ ดูหมวดจุดลอยเทียบกับการคูณจำนวนจุดลอยตัวสำหรับสารสกัดบางส่วนจากตารางของ Agner Fog หากคุณไม่ได้ทำงานอื่น ๆ อีกมากในการวนรอบดังนั้น sqrt + div เป็นคอขวดคุณอาจต้องการใช้ HW เร็วซึ่งกันและกัน sqrt (แทนการแฮ็คแผ่นดินไหว) + การทำซ้ำนิวตัน โดยเฉพาะอย่างยิ่งกับ FMA ที่ดีสำหรับปริมาณงานถ้าไม่ใช่เวลาแฝง vectorized rsqrt อย่างรวดเร็วและส่วนกลับที่มี SSE / AVX ขึ้นอยู่กับความแม่นยำ
Peter Cordes

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ควรหลีกเลี่ยงที่นี่เพราะมันไม่จำเป็น
Lukas Kalbertodt
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.