Có thể viết hàm InvSqrt () nhanh của Quake trong Rust không?


101

Đây chỉ là để thỏa mãn sự tò mò của riêng tôi.

Có thực hiện điều này:

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;
}

ở Rust? Nếu nó tồn tại, gửi mã.

Tôi đã thử nó và thất bại. Tôi không biết cách mã hóa số float bằng định dạng số nguyên. Đây là nỗ lực của tôi:

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
}

Tham khảo:
1. Nguồn gốc của InvSqrt nhanh của Quake3 () - Trang 1
2. Tìm hiểu căn bậc hai nghịch đảo nhanh của Quake
3. TÌM KIẾM NHANH CHÓNG ROOT.pdf
4. mã nguồn: q_math.c # L552-L572



4
Theo tôi hiểu, mã này là UB trong C do vi phạm quy tắc răng cưa nghiêm ngặt . Cách may mắn tiêu chuẩn để thực hiện loại pucky này là với a union.
trentcl

4
@trentcl: Tôi cũng không nghĩ unionlà có tác dụng. memcpychắc chắn hoạt động, mặc dù nó dài dòng.
Matthieu M.

14
@MatthieuM. Gõ pucky với unions là C hoàn toàn hợp lệ , nhưng C ++ không hợp lệ.
Moira

4
Tôi cho rằng câu hỏi này là tốt từ góc độ tò mò thuần túy, nhưng xin vui lòng hiểu rằng thời gian đã thay đổi. Trên x86, rsqrtssvà các rsqrtpshướng dẫn, được giới thiệu với Pentium III năm 1999, nhanh hơn và chính xác hơn mã này. ARM NEON có vrsqrtetương tự. Và bất cứ tính toán nào Quake III sử dụng điều này có lẽ sẽ được thực hiện trên GPU ngày nay.
benrg

Câu trả lời:


87

Tôi không biết cách mã hóa số float bằng định dạng số nguyên.

Có một chức năng cho rằng: f32::to_bitstrả về một u32. Ngoài ra còn có chức năng cho hướng khác: f32::from_bitslấy tham u32số làm đối số. Các chức năng này được ưa thích hơn mem::transmutelà chức năng sau unsafevà khó sử dụng.

Cùng với đó, đây là việc thực hiện 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)
}

( Sân chơi )


Hàm này biên dịch thành cụm sau trên 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

Tôi chưa tìm thấy bất kỳ hội nghị tham khảo nào (nếu bạn có, xin vui lòng cho tôi biết!), Nhưng nó có vẻ khá tốt với tôi. Tôi chỉ không chắc chắn tại sao float được chuyển vào eaxchỉ để thực hiện phép trừ và thay đổi số nguyên. Có lẽ các thanh ghi SSE không hỗ trợ các hoạt động đó?

clang 9.0 với -O3biên dịch mã C về cơ bản giống nhau . Vì vậy, đó là một dấu hiệu tốt.


Điều đáng nói là nếu bạn thực sự muốn sử dụng điều này trong thực tế: xin đừng. Như benrg đã chỉ ra trong các bình luận , CPU x86 hiện đại có một hướng dẫn chuyên biệt cho chức năng này nhanh hơn và chính xác hơn bản hack này. Thật không may, 1.0 / x.sqrt() dường như không tối ưu hóa theo hướng dẫn đó . Vì vậy, nếu bạn thực sự cần tốc độ, sử dụng các _mm_rsqrt_psintrinsics có lẽ là con đường để đi. Điều này, tuy nhiên, một lần nữa yêu cầu unsafemã. Tôi sẽ không đi sâu vào chi tiết trong câu trả lời này, vì một số ít các lập trình viên sẽ thực sự cần nó.


4
Theo Hướng dẫn nội bộ của Intel, không có hoạt động dịch chuyển số nguyên mà chỉ dịch chuyển 32 bit thấp nhất của thanh ghi tương tự 128 bit sang addsshoặc mulss. Nhưng nếu 96 bit khác của xmm0 có thể bị bỏ qua thì người ta có thể sử dụng psrldhướng dẫn. Tương tự với phép trừ số nguyên.
fsasm

Tôi sẽ thừa nhận không biết gì về rỉ sét, nhưng về cơ bản không phải là "không an toàn" là thuộc tính cốt lõi của fast_inv_sqrt? Với sự thiếu tôn trọng đối với các kiểu dữ liệu và như vậy.
Gloweye

12
@Gloweye Đó là một loại "không an toàn" khác mà chúng ta nói đến. Một xấp xỉ nhanh mà nhận được một giá trị xấu quá xa điểm ngọt, so với một cái gì đó chơi nhanh và lỏng lẻo với hành vi không xác định.
Ded repeatator

8
@Gloweye: Về mặt toán học, phần cuối cùng fast_inv_sqrtchỉ là một bước lặp Newton-Raphson để tìm ra một xấp xỉ tốt hơn inv_sqrt. Không có gì không an toàn về phần đó. Các mánh khóe là trong phần đầu tiên, trong đó tìm thấy một xấp xỉ tốt. Điều đó hoạt động vì nó thực hiện chia số nguyên cho 2 trên phần số mũ của số float và thực sựsqrt(pow(0.5,x))=pow(0.5,x/2)
MSalters

1
@fsasm: Đúng vậy; movdđến EAX và trở lại là một tối ưu hóa bị bỏ lỡ bởi các trình biên dịch hiện tại. (Và vâng, gọi các quy ước truyền / trả lại vô hướng floattrong phần tử thấp của XMM và cho phép các bit cao trở thành rác. Nhưng lưu ý rằng nếu nó được mở rộng bằng 0, nó có thể dễ dàng giữ nguyên như vậy: dịch chuyển phải không giới thiệu không Các phần tử bằng 0 và không trừ đi _mm_set_epi32(0,0,0,0x5f3759df), tức là movdtải. Bạn sẽ cần movdqa xmm1,xmm0sao chép reg trước đó psrld. Bỏ qua độ trễ từ chuyển tiếp lệnh FP sang số nguyên và ngược lại bị ẩn bởi mulssđộ trễ.
Peter Cordes

37

Điều này được thực hiện với ít được biết đến uniontrong 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)
    }
}

Có một số điểm chuẩn vi mô bằng cách sử dụng criterionthùng trên hộp Linux x86-64. Đáng ngạc nhiên là riêng của Rust sqrt().recip()là nhanh nhất. Nhưng tất nhiên, bất kỳ kết quả điểm chuẩn vi mô nên được thực hiện với một hạt muối.

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
Tôi không ngạc nhiên sqrt().inv()nhất là nhanh nhất. Cả sqrt và inv đều là những hướng dẫn duy nhất trong những ngày này, và đi khá nhanh. Doom được viết vào thời mà không an toàn khi cho rằng có điểm nổi phần cứng, và các chức năng siêu việt như sqrt chắc chắn sẽ là phần mềm. +1 cho điểm chuẩn.
Martin Bonner hỗ trợ Monica

4
Điều làm tôi ngạc nhiên là nó transmuterõ ràng khác với to_from_bits- tôi hy vọng những thứ đó tương đương với hướng dẫn ngay cả trước khi tối ưu hóa.
trentcl

2
@MartinBonner (Ngoài ra, không phải là vấn đề, nhưng sqrt không phải là một chức năng siêu việt .)
benrg

4
@MartinBonner: Bất kỳ FPU phần cứng nào hỗ trợ phân chia thông thường cũng sẽ hỗ trợ sqrt. Các hoạt động "cơ bản" của IEEE (+ - * / sqrt) được yêu cầu để tạo ra kết quả được làm tròn chính xác; đó là lý do tại sao SSE cung cấp tất cả các hoạt động đó nhưng không phải là exp, sin hay bất cứ điều gì. Trong thực tế, chia và sqrt thường chạy trên cùng một đơn vị thực thi, được thiết kế theo cách tương tự. Xem chi tiết đơn vị CT div / sqrt . Dù sao, chúng vẫn không nhanh so với nhân, đặc biệt là về độ trễ.
Peter Cordes

1
Dù sao, Skylake có đường ống cho div / sqrt tốt hơn đáng kể so với các uarch trước đó. Xem Phân chia điểm nổi so với phép nhân điểm nổi cho một số trích đoạn từ bảng của Agner Fog. Nếu bạn không thực hiện nhiều công việc khác trong một vòng lặp thì sqrt + div là một nút cổ chai, bạn có thể muốn sử dụng sqrt đối ứng nhanh (thay vì hack trận động đất) + lặp lại Newton. Đặc biệt với FMA, điều đó tốt cho thông lượng, nếu không nói là độ trễ. Rsqrt được vector hóa nhanh và đối ứng với SSE / AVX tùy thuộc vào độ chính xác
Peter Cordes

10

Bạn có thể sử dụng std::mem::transmuteđể thực hiện chuyển đổi cần thiết:

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
}

Bạn có thể tìm một ví dụ trực tiếp tại đây: tại đây


4
Không có gì sai với không an toàn, nhưng có một cách để làm điều này mà không có khối không an toàn rõ ràng, vì vậy tôi khuyên bạn nên viết lại câu trả lời này bằng cách sử dụng f32::to_bitsf32::from_bits. Nó cũng mang ý định rõ ràng không giống như chuyển đổi, mà hầu hết mọi người có thể xem là "ma thuật".
Sahsahae

5
@Sahsahae Tôi vừa đăng câu trả lời bằng hai chức năng bạn đã đề cập :) Và tôi đồng ý, unsafenên tránh ở đây, vì nó không cần thiết.
Lukas Kalbertodt
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.