รากที่สองผกผันอย่างรวดเร็วผิดปกติของ John Carmack (Quake III)


112

John Carmack มีฟังก์ชันพิเศษในซอร์สโค้ด Quake III ซึ่งคำนวณค่ารากที่สองผกผันของการลอยซึ่งเร็วกว่าปกติ 4 เท่า(float)(1.0/sqrt(x))รวมถึง0x5f3759dfค่าคงที่แปลก ๆ ดูโค้ดด้านล่าง ใครช่วยอธิบายทีละบรรทัดว่าเกิดอะไรขึ้นที่นี่และเหตุใดจึงทำงานได้เร็วกว่าการใช้งานปกติมาก

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
มีการเขียนถึง zillions หลายครั้ง ดู: google.com/search?q=0x5f3759df
Greg Hewgill

15
ขอบคุณแม้ว่า นี่เป็นคำถามที่น่าสนใจกว่า "คุณทำให้จำนวนบวกเป็นลบใน C # ได้อย่างไร"
MusiGenesis


7
อึศักดิ์สิทธิ์นี่เป็นเพียงการแฮ็กตามวิธีการของนิวตันไม่ใช่อัลกอริทึมศักดิ์สิทธิ์บางอย่างหยุดพูดถึงเรื่องนี้ได้เลย: P
ldog

คำตอบ:


75

FYI. คาร์แม็คไม่ได้เขียนไว้ Terje Mathisen และ Gary Tarolli ต่างก็ให้เครดิตบางส่วน (และเจียมเนื้อเจียมตัวมาก) รวมทั้งให้เครดิตแหล่งข้อมูลอื่น ๆ

ค่าคงที่ในตำนานได้มานั้นเป็นปริศนาได้อย่างไร

หากต้องการอ้างอิง Gary Tarolli:

ซึ่งจริงๆแล้วกำลังคำนวณทศนิยมเป็นจำนวนเต็ม - ใช้เวลานานในการหาว่าทำไมถึงได้ผลและฉันจำรายละเอียดไม่ได้อีกต่อไป

ค่าคงที่ที่ดีขึ้นเล็กน้อยซึ่งพัฒนาโดยนักคณิตศาสตร์ผู้เชี่ยวชาญ (Chris Lomont) ซึ่งพยายามหาวิธีการทำงานของอัลกอริทึมดั้งเดิม:

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

แม้จะเป็นเช่นนี้ความพยายามครั้งแรกของเขาใน sqrt ของ id ที่ 'เหนือกว่า' ทางคณิตศาสตร์ (ซึ่งเกือบจะมีค่าคงที่เท่ากัน) พิสูจน์แล้วว่าด้อยกว่าที่ Gary พัฒนาขึ้นในตอนแรกแม้ว่าจะมีความ 'บริสุทธิ์กว่า' ในทางคณิตศาสตร์มากก็ตาม เขาไม่สามารถอธิบายได้ว่าทำไม id ถึงยอดเยี่ยมมาก


4
"บริสุทธิ์ทางคณิตศาสตร์" ควรจะหมายถึงอะไร
ธารา

1
ฉันจะนึกภาพออกว่าการคาดเดาครั้งแรกมาจากค่าคงที่ที่สมเหตุสมผลได้จากที่ใดแทนที่จะดูเหมือนเป็นไปตามอำเภอใจ แม้ว่าคุณต้องการคำอธิบายทางเทคนิคคุณสามารถค้นหาได้ ฉันไม่ใช่นักคณิตศาสตร์และการอภิปรายเชิงความหมายเกี่ยวกับคำศัพท์ทางคณิตศาสตร์ไม่ได้อยู่ใน SO
Rushyo

7
นั่นคือว่าเหตุผลที่ผมโพสต์คำในคำพูดที่ทำให้ตกใจ, เพื่อหลีกเลี่ยงการเรียงลำดับของเรื่องไร้สาระนี้ ฉันเดาว่าผู้อ่านคุ้นเคยกับการเขียนภาษาอังกฤษเป็นภาษาพูด คุณคิดว่าสามัญสำนึกน่าจะเพียงพอแล้ว ฉันไม่ได้ใช้คำที่คลุมเครือเพราะฉันคิดว่า "คุณรู้อะไรไหมฉันอยากจะถามใครบางคนที่ไม่ต้องกังวลในการค้นหาแหล่งที่มาดั้งเดิมซึ่งจะใช้เวลาสองวินาทีใน Google"
Rushyo

2
คุณยังไม่ได้ตอบคำถาม
BJovke

1
สำหรับคนที่อยากรู้ว่าเขาไปเจอที่ไหน: beyond3d.com/content/articles/8
mr5

52

แน่นอนว่าทุกวันนี้มันช้ากว่าการใช้ sqrt ของ FPU (โดยเฉพาะบน 360 / PS3) มากเพราะการสลับระหว่างการลงทะเบียน float และ int ทำให้เกิด load-hit-store ในขณะที่หน่วยทศนิยมสามารถทำสี่เหลี่ยมซึ่งกันและกันได้ รูทในฮาร์ดแวร์

เพียงแค่แสดงให้เห็นว่าการเพิ่มประสิทธิภาพต้องพัฒนาไปอย่างไรตามธรรมชาติของการเปลี่ยนแปลงฮาร์ดแวร์พื้นฐาน


4
มันยังเร็วกว่า std :: sqrt () มาก
Tara

2
คุณมีแหล่งที่มาหรือไม่? ฉันต้องการทดสอบ runtimes แต่ไม่มีชุดพัฒนา Xbox 360
DucRP

31

Greg HewgillและIllidanS4ให้ลิงก์พร้อมคำอธิบายทางคณิตศาสตร์ที่ยอดเยี่ยม ฉันจะพยายามสรุปไว้ที่นี่สำหรับคนที่ไม่ต้องการลงรายละเอียดมากเกินไป

ฟังก์ชันทางคณิตศาสตร์ใด ๆ ที่มีข้อยกเว้นบางประการสามารถแสดงด้วยผลรวมพหุนาม:

y = f(x)

สามารถว่าเปลี่ยนเป็น:

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

ที่ไหน A0, A1, A2, ... มีค่าคงที่ ปัญหาคือสำหรับหลายฟังก์ชันเช่นรากที่สองสำหรับค่าที่แน่นอนผลรวมนี้มีจำนวนสมาชิกไม่สิ้นสุดมันไม่ได้จบที่x ^ nบางตัวบางตัว แต่ถ้าเราหยุดที่ค่าx ^ nเราก็ยังคงได้ผลลัพธ์ที่แม่นยำอยู่

ดังนั้นถ้าเรามี:

y = 1/sqrt(x)

ในกรณีนี้พวกเขาตัดสินใจทิ้งสมาชิกพหุนามทั้งหมดที่อยู่เหนืออันดับสองอาจเป็นเพราะความเร็วในการคำนวณ:

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

และตอนนี้งานได้ลงมาเพื่อคำนวณ a0 และ a1 เพื่อให้ y มีความแตกต่างน้อยที่สุดจากค่าที่แน่นอน พวกเขาคำนวณแล้วว่าค่าที่เหมาะสมที่สุดคือ:

a0 = 0x5f375a86
a1 = -0.5

ดังนั้นเมื่อคุณใส่สิ่งนี้ลงในสมการคุณจะได้รับ:

y = 0x5f375a86 - 0.5*x

ซึ่งเหมือนกับบรรทัดที่คุณเห็นในโค้ด:

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

แก้ไข: ที่นี่จริง y = 0x5f375a86 - 0.5*xไม่เหมือนกับi = 0x5f375a86 - (i >> 1);เนื่องจากการเปลี่ยน float เป็นจำนวนเต็มไม่เพียง แต่หารด้วยสอง แต่ยังหารเลขชี้กำลังด้วยสองและทำให้เกิดสิ่งประดิษฐ์อื่น ๆ แต่ก็ยังคำนวณค่าสัมประสิทธิ์บางอย่าง a0, a1, a2 ...

เมื่อมาถึงจุดนี้พวกเขาพบว่าความแม่นยำของผลลัพธ์นี้ไม่เพียงพอสำหรับวัตถุประสงค์ ดังนั้นพวกเขาจึงทำเพียงขั้นตอนเดียวของการทำซ้ำของนิวตันเพื่อปรับปรุงความแม่นยำของผลลัพธ์:

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

พวกเขาสามารถทำการวนซ้ำได้มากกว่านี้โดยแต่ละอันจะปรับปรุงผลลัพธ์จนกว่าจะได้ความแม่นยำที่ต้องการ นี่คือวิธีการทำงานใน CPU / FPU!แต่ดูเหมือนว่าการทำซ้ำเพียงครั้งเดียวก็เพียงพอแล้วซึ่งเป็นพรสำหรับความเร็วเช่นกัน CPU / FPU ทำการวนซ้ำได้มากเท่าที่จำเป็นเพื่อให้ได้ความแม่นยำสำหรับจำนวนทศนิยมที่เก็บผลลัพธ์และมีอัลกอริทึมทั่วไปที่ใช้ได้กับทุกกรณี


ดังนั้นในระยะสั้นสิ่งที่พวกเขาทำคือ:

ใช้ (เกือบ) อัลกอริทึมเดียวกับ CPU / FPU ใช้ประโยชน์จากการปรับปรุงเงื่อนไขเริ่มต้นสำหรับกรณีพิเศษของ 1 / sqrt (x) และอย่าคำนวณทุกอย่างเพื่อความแม่นยำของ CPU / FPU จะไป แต่หยุดก่อนหน้านี้ดังนั้น เพิ่มความเร็วในการคำนวณ


2
การแคสต์ตัวชี้ให้ยาวเป็นการประมาณ log_2 (float) การหล่อกลับมีความยาวประมาณ 2 ^ ซึ่งหมายความว่าคุณสามารถสร้างอัตราส่วนโดยประมาณเป็นเส้นตรงได้
wizzwizz4

22

ตามบทความที่ดีนี้เขียนเมื่อย้อนกลับไป ...

ความมหัศจรรย์ของรหัสแม้ว่าคุณจะไม่สามารถทำตามได้ แต่ก็มีความโดดเด่นเช่น i = 0x5f3759df - (i >> 1); ไลน์. แบบง่าย Newton-Raphson เป็นการประมาณที่เริ่มต้นด้วยการเดาและปรับแต่งด้วยการวนซ้ำ การใช้ประโยชน์จากธรรมชาติของโปรเซสเซอร์ x86 32 บิต i ซึ่งเป็นจำนวนเต็มเริ่มแรกจะถูกตั้งค่าเป็นค่าของเลขทศนิยมที่คุณต้องการใช้กำลังสองผกผันโดยใช้การร่ายจำนวนเต็ม จากนั้นฉันจะตั้งค่าเป็น 0x5f3759df ลบตัวเองเลื่อนไปทางขวาหนึ่งบิต การเลื่อนที่ถูกต้องจะลดค่า i ที่มีนัยสำคัญน้อยที่สุดโดยลดลงครึ่งหนึ่ง

เป็นการอ่านที่ดีจริงๆ นี่เป็นเพียงชิ้นส่วนเล็ก ๆ เท่านั้น


19

ฉันอยากรู้อยากเห็นว่าค่าคงที่เป็นลอยเป็นอย่างไรดังนั้นฉันจึงเขียนโค้ดนี้ขึ้นมาและ googled จำนวนเต็มที่โผล่ออกมา

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

ดูเหมือนว่าค่าคงที่คือ "ค่าประมาณจำนวนเต็มกับรากที่สองของ 2 ^ 127 ที่รู้จักกันดีในรูปเลขฐานสิบหกของการแทนค่าทศนิยม 0x5f3759df" https://mrob.com/pub/math/numbers-18.html

ในเว็บไซต์เดียวกันจะอธิบายถึงเรื่องทั้งหมด https://mrob.com/pub/math/numbers-16.html#le009_16


6
สิ่งนี้สมควรได้รับความสนใจมากขึ้น ทุกอย่างสมเหตุสมผลหลังจากที่รู้ว่ามันเป็นแค่รากที่สองของ 2 ^ 127 ...
u8y7541
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.