Căn bậc hai nghịch đảo nhanh bất thường của John Carmack (Quake III)


112

John Carmack có một chức năng đặc biệt trong mã nguồn Quake III, tính toán căn bậc hai nghịch đảo của một số float, nhanh hơn 4 lần so với thông thường (float)(1.0/sqrt(x)), bao gồm một 0x5f3759dfhằng số lạ . Xem mã bên dưới. Ai đó có thể giải thích từng dòng chính xác những gì đang diễn ra ở đây và tại sao điều này hoạt động nhanh hơn nhiều so với việc triển khai thông thường không?

float Q_rsqrt( float number )
{
  long i;
  float x2, y;
  const float threehalfs = 1.5F;

  x2 = number * 0.5F;
  y  = number;
  i  = * ( long * ) &y;
  i  = 0x5f3759df - ( i >> 1 );
  y  = * ( float * ) &i;
  y  = y * ( threehalfs - ( x2 * y * y ) );

  #ifndef Q3_VM
  #ifdef __linux__
    assert( !isnan(y) );
  #endif
  #endif
  return y;
}


10
Điều này đã được viết về sự thay đổi của thời đại. Xem: google.com/search?q=0x5f3759df
Greg Hewgill

15
Cảm ơn, mặc dù. Đây là một câu hỏi thú vị hơn nhiều so với "làm thế nào để bạn tạo một số dương trong C #?"
MusiGenesis


7
Thánh tào lao, điều này chỉ là một hack dựa trên phương pháp newton, nó không phải một số Chén thánh của thuật toán, dừng nói về nó lời cầu khẩn: P
ldog

Câu trả lời:


75

FYI. Carmack đã không viết nó. Terje Mathisen và Gary Tarolli đều ghi nhận một phần (và rất khiêm tốn) cho nó, cũng như ghi nhận một số nguồn khác.

Làm thế nào hằng số thần thoại được bắt nguồn là một điều gì đó bí ẩn.

Để trích dẫn Gary Tarolli:

Mà thực sự là thực hiện một phép tính dấu phẩy động trong số nguyên - phải mất nhiều thời gian để tìm ra cách thức và lý do tại sao điều này hoạt động, và tôi không thể nhớ chi tiết nữa.

Một hằng số tốt hơn một chút, được phát triển bởi một nhà toán học chuyên nghiệp (Chris Lomont) đang cố gắng tìm ra cách hoạt động của thuật toán ban đầu:

float InvSqrt(float x)
{
    float xhalf = 0.5f * x;
    int i = *(int*)&x;              // get bits for floating value
    i = 0x5f375a86 - (i >> 1);      // gives initial guess y0
    x = *(float*)&i;                // convert bits back to float
    x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy
    return x;
}

Mặc dù vậy, nỗ lực ban đầu của anh ấy về một phiên bản 'cao cấp' về mặt toán học của id's sqrt (gần như cùng một hằng số) đã tỏ ra kém hơn so với phiên bản do Gary phát triển ban đầu mặc dù 'tinh khiết hơn' về mặt toán học. Anh ấy không thể giải thích tại sao id lại xuất sắc đến vậy.


4
"Tinh khiết hơn về mặt toán học" có nghĩa là gì?
Tara

1
Tôi sẽ tưởng tượng nơi mà phỏng đoán đầu tiên có thể được bắt nguồn từ các hằng số hợp lý, thay vì có vẻ tùy tiện. Mặc dù nếu bạn muốn mô tả kỹ thuật, bạn có thể tra cứu nó. Tôi không phải là nhà toán học và một cuộc thảo luận ngữ nghĩa về thuật ngữ toán học không thuộc về SO.
Rushyo

7
Đó chính xác là lý do tôi gói gọn từ đó trong những câu trích dẫn đáng sợ, để ngăn chặn những điều vô nghĩa này. Điều đó giả sử rằng người đọc đã quen với cách viết tiếng Anh thông tục, tôi đoán vậy. Bạn sẽ nghĩ rằng lẽ thường là đủ. Tôi đã không sử dụng một thuật ngữ mơ hồ vì tôi nghĩ "bạn biết không, tôi thực sự muốn được hỏi về điều này bởi một người không bận tâm đến việc tra cứu nguồn gốc sẽ mất hai giây trên Google".
Rushyo

2
Vâng, bạn thực sự chưa trả lời câu hỏi.
BJovke

1
Đối với những người muốn biết nơi ông tìm thấy nó: beyond3d.com/content/articles/8
mr5

52

Tất nhiên những ngày này, nó hóa ra chậm hơn nhiều so với việc chỉ sử dụng sqrt của FPU (đặc biệt là trên 360 / PS3), bởi vì việc hoán đổi giữa các thanh ghi float và int tạo ra một cửa hàng tải lần truy cập, trong khi đơn vị dấu chấm động có thể thực hiện bình phương đối ứng root trong phần cứng.

Nó chỉ cho thấy cách tối ưu hóa phải phát triển khi bản chất của những thay đổi phần cứng cơ bản.


4
Tuy nhiên, nó vẫn nhanh hơn rất nhiều so với std :: sqrt ().
Tara

2
Bạn có nguồn không? Tôi muốn kiểm tra thời gian chạy nhưng tôi không có bộ phát triển Xbox 360.
DucRP

31

Greg HewgillIllidanS4 đã đưa ra một liên kết với lời giải thích toán học tuyệt vời. Tôi sẽ cố gắng tóm tắt nó ở đây cho những ai không muốn đi quá nhiều vào chi tiết.

Bất kỳ hàm toán học nào, với một số ngoại lệ, đều có thể được biểu diễn bằng một tổng đa thức:

y = f(x)

có thể được chuyển đổi chính xác thành:

y = a0 + a1*x + a2*(x^2) + a3*(x^3) + a4*(x^4) + ...

Trong đó a0, a1, a2, ... là các hằng số . Vấn đề là đối với nhiều hàm, như căn bậc hai, đối với giá trị chính xác, tổng này có vô số phần tử, nó không kết thúc ở một số x ^ n . Nhưng, nếu chúng ta dừng lại ở một số x ^ n, chúng ta vẫn sẽ có một kết quả chính xác đến mức nào đó.

Vì vậy, nếu chúng ta có:

y = 1/sqrt(x)

Trong trường hợp cụ thể này, họ quyết định loại bỏ tất cả các phần tử đa thức ở trên thứ hai, có thể là do tốc độ tính toán:

y = a0 + a1*x + [...discarded...]

Và nhiệm vụ bây giờ là tính toán a0 và a1 để y có sự khác biệt nhỏ nhất so với giá trị chính xác. Họ đã tính toán rằng các giá trị thích hợp nhất là:

a0 = 0x5f375a86
a1 = -0.5

Vì vậy, khi bạn đặt điều này vào phương trình, bạn sẽ nhận được:

y = 0x5f375a86 - 0.5*x

Dòng nào giống với dòng bạn thấy trong mã:

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

Chỉnh sửa: thực ra ở đây y = 0x5f375a86 - 0.5*xkhông giống như i = 0x5f375a86 - (i >> 1);vì chuyển float thành số nguyên không chỉ chia cho hai mà còn chia mũ cho hai và gây ra một số hiện vật khác, nhưng nó vẫn đi xuống để tính một số hệ số a0, a1, a2 ....

Tại thời điểm này, họ phát hiện ra rằng độ chính xác của kết quả này là không đủ cho mục đích. Vì vậy, họ cũng chỉ thực hiện một bước lặp lại Newton để cải thiện độ chính xác của kết quả:

x = x * (1.5f - xhalf * x * x)

Họ có thể đã thực hiện thêm một số lần lặp trong một vòng lặp, mỗi lần một cải thiện kết quả, cho đến khi đáp ứng được độ chính xác cần thiết. Đây chính xác là cách nó hoạt động trong CPU / FPU! Nhưng có vẻ như chỉ một lần lặp lại là đủ, đó cũng là một may mắn cho tốc độ. CPU / FPU thực hiện nhiều lần lặp lại nếu cần để đạt được độ chính xác cho số dấu phẩy động trong đó kết quả được lưu trữ và nó có thuật toán chung hơn hoạt động cho mọi trường hợp.


Tóm lại, những gì họ đã làm là:

Sử dụng (gần như) cùng một thuật toán với CPU / FPU, khai thác việc cải thiện các điều kiện ban đầu cho trường hợp đặc biệt của 1 / sqrt (x) và không tính toán tất cả các cách để CPU / FPU chính xác sẽ đi đến nhưng dừng sớm hơn, do đó tăng tốc độ tính toán.


2
Truyền con trỏ đến long là một xấp xỉ của log_2 (float). Truyền nó trở lại có độ dài xấp xỉ 2 ^. Điều này có nghĩa là bạn có thể làm cho tỷ lệ gần như tuyến tính.
wizzwizz 4

22

Theo bài báo tốt đẹp này được viết một thời gian trở lại ...

Điều kỳ diệu của mã, ngay cả khi bạn không thể theo dõi nó, nổi bật là i = 0x5f3759df - (i >> 1); hàng. Đơn giản hóa, Newton-Raphson là một phép gần đúng bắt đầu bằng một dự đoán và tinh chỉnh nó với sự lặp lại. Tận dụng tính chất của bộ xử lý 32-bit x86, i, một số nguyên, ban đầu được đặt thành giá trị của số dấu phẩy động mà bạn muốn lấy bình phương nghịch đảo của nó, bằng cách sử dụng ép kiểu số nguyên. sau đó tôi được đặt thành 0x5f3759df, trừ đi chính nó đã dịch chuyển một chút sang bên phải. Sự dịch chuyển bên phải làm giảm bit quan trọng nhất của i, về cơ bản là giảm một nửa.

Đó là một bài đọc thực sự tốt. Đây chỉ là một phần nhỏ của nó.


19

Tôi tò mò muốn xem hằng số là gì dưới dạng float nên tôi chỉ cần viết đoạn mã này và truy cập vào số nguyên xuất hiện trên Google.

    long i = 0x5F3759DF;
    float* fp = (float*)&i;
    printf("(2^127)^(1/2) = %f\n", *fp);
    //Output
    //(2^127)^(1/2) = 13211836172961054720.000000

Có vẻ như hằng số là "Một số nguyên gần đúng với căn bậc hai của 2 ^ 127 được biết đến nhiều hơn ở dạng thập lục phân của biểu diễn dấu phẩy động, 0x5f3759df" https://mrob.com/pub/math/numbers-18.html

Trên cùng một trang web, nó giải thích toàn bộ điều. https://mrob.com/pub/math/numbers-16.html#le009_16


6
Điều này đáng được quan tâm hơn. Tất cả đều có ý nghĩa sau khi nhận ra rằng nó chỉ là căn bậc hai của 2 ^ 127 ...
u8y7541
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.