John Carmack'in Olağandışı Hızlı Ters Kare Kökü (Quake III)


112

John Carmack, Quake III kaynak kodunda, bir float'ın ters karekökünü normalden 4 kat daha hızlı (float)(1.0/sqrt(x)), garip bir 0x5f3759dfsabit dahil olmak üzere hesaplayan özel bir işleve sahiptir . Aşağıdaki koda bakın. Biri satır satır burada tam olarak neler olup bittiğini ve bunun neden normal uygulamadan çok daha hızlı çalıştığını açıklayabilir mi?

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
Bu, milyonlarca kez yazılmıştır. Bakınız: google.com/search?q=0x5f3759df
Greg Hewgill

15
Yine de teşekkürler. Bu, "C # 'da pozitif bir sayıyı nasıl negatif yaparsınız?" Sorusundan çok daha ilginç bir soruydu.
MusiGenesis


7
Kutsal bok, bu sadece
Newton'un

Yanıtlar:


75

Bilginize. Carmack yazmadı. Terje Mathisen ve Gary Tarolli, hem kısmen (hem de çok mütevazı) kredi alıyorlar ve diğer bazı kaynaklara da itibar ediyorlar.

Efsanevi sabitin nasıl türetildiği bir gizemdir.

Gary Tarolli'den alıntı yapacak olursak:

Aslında tamsayıda bir kayan nokta hesaplaması yapıyor - bunun nasıl ve neden çalıştığını anlamak uzun zaman aldı ve artık ayrıntıları hatırlayamıyorum.

Orijinal algoritmanın nasıl çalıştığını çözmeye çalışan uzman bir matematikçi (Chris Lomont) tarafından geliştirilen biraz daha iyi bir sabit :

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

Buna rağmen, id's sqrt'sinin matematiksel olarak 'üstün' bir versiyonunun (neredeyse aynı sabite ulaşan) ilk girişimi, matematiksel olarak çok daha 'saf' olmasına rağmen, Gary tarafından başlangıçta geliştirilenden daha düşük olduğunu kanıtladı. Kimliklerin neden bu kadar mükemmel olduğunu açıklayamadı iirc.


4
"Matematiksel olarak daha saf" ne anlama geliyor?
Tara

1
İlk tahminin görünüşte keyfi olmaktan ziyade gerekçelendirilebilir sabitlerden nereden türetilebileceğini hayal ediyorum. Teknik bir açıklama istiyorsanız, buna bakabilirsiniz. Ben bir matematikçi değilim ve matematiksel terminoloji hakkındaki anlambilimsel bir tartışma SO'ya ait değil.
Rushyo

7
Bu tür saçmalıklardan kaçınmak için bu kelimeyi korkutucu alıntılarla özetlememin tam nedeni buydu . Bu, okuyucunun günlük İngilizce yazıya aşina olduğunu varsayar sanırım. Sağduyunun yeterli olacağını düşünürsünüz. Belirsiz bir terim kullanmadım çünkü "Google'da iki saniye sürecek olan orijinal kaynağa bakmaktan rahatsız olmayacak biri tarafından gerçekten bu konuda sorgulanmak istiyorum" diye düşündüm.
Rushyo

2
Aslında soruya cevap vermedin.
BJovke

1
Onu nerede bulduğunu bilmek isteyenler için: beyond3d.com/content/articles/8
mr5

52

Elbette bu günlerde, sadece bir FPU'nun sqrt'sini kullanmaktan (özellikle 360 ​​/ PS3'te) çok daha yavaş olduğu ortaya çıkıyor, çünkü float ve int yazmaçları arasında geçiş, bir yük vuruşu deposu oluştururken, kayan nokta birimi karşılıklı kare yapabilir donanımda kök.

Yalnızca temel donanım değiştikçe optimizasyonların nasıl gelişmesi gerektiğini gösterir.


4
Yine de std :: sqrt () 'den çok daha hızlı.
Tara

2
Kaynağınız var mı? Çalışma zamanlarını test etmek istiyorum ancak bir Xbox 360 geliştirme kitim yok.
DucRP

31

Greg Hewgill ve IllidanS4 mükemmel matematiksel açıklamalarla bir bağlantı kurdu. Ayrıntılara çok fazla girmek istemeyenler için burada özetlemeye çalışacağım.

Bazı istisnalar dışında herhangi bir matematiksel fonksiyon, bir polinom toplamı ile temsil edilebilir:

y = f(x)

edilebilir aynen dönüştü:

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

A0, a1, a2, ... sabittir . Sorun şu ki, karekök gibi pek çok işlev için, tam değer için bu toplamın sonsuz sayıda üyesi vardır, bazı x ^ n'de bitmez . Ancak, x ^ n'de durursak, yine de kesinliğe kadar bir sonuca sahip oluruz.

Yani, eğer sahipsek:

y = 1/sqrt(x)

Bu özel durumda, muhtemelen hesaplama hızı nedeniyle, saniyenin üzerindeki tüm polinom üyelerini atmaya karar verdiler:

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

Ve şimdi, y'nin kesin değerden en az farka sahip olması için a0 ve a1'i hesaplamak için görev geldi. En uygun değerlerin şunlar olduğunu hesaplamışlardır:

a0 = 0x5f375a86
a1 = -0.5

Yani bunu denkleme koyduğunuzda şunu elde edersiniz:

y = 0x5f375a86 - 0.5*x

Kodda gördüğünüz satırla aynı olan:

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

Düzenleme: Aslında burada y = 0x5f375a86 - 0.5*xaynı şey değildir i = 0x5f375a86 - (i >> 1);ama aynı zamanda bölme işlemlerini üs ikişer ikişer tamsayı olarak sadece böler şamandıra kayması beri ve diğer bazı eserler neden olur, ancak hala bazı katsayıları, a2 A1 A0 hesaplama aşağı gelir ....

Bu noktada, bu sonucun kesinliğinin amaç için yeterli olmadığını anladılar. Böylece sonuç doğruluğunu iyileştirmek için ayrıca Newton'un yinelemesinin yalnızca bir adımını yaptılar:

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

Gerekli doğruluk sağlanana kadar, her biri sonucu iyileştiren bir döngüde daha fazla yineleme yapabilirlerdi. CPU / FPU'da tam olarak böyle çalışır! Ancak görünen o ki, tek bir yineleme yeterliydi, bu da hız için bir nimetti. CPU / FPU, sonucun saklandığı kayan nokta sayısının doğruluğuna ulaşmak için gerektiği kadar yineleme yapar ve tüm durumlar için çalışan daha genel bir algoritmaya sahiptir.


Kısacası yaptıkları şuydu:

CPU / FPU ile (neredeyse) aynı algoritmayı kullanın, 1 / sqrt (x) özel durumu için başlangıç ​​koşullarının iyileştirilmesinden yararlanın ve CPU / FPU'nun gideceği kesinliğe kadar tüm yolu hesaplamayın, ancak daha önce durun, bu nedenle hesaplama hızında kazanç.


2
İşaretçiyi uzun süre çevirmek, log_2 (float) değerinin bir tahminidir. Onu geri çevirmek yaklaşık 2 ^ uzunluğundadır. Bu, oranı yaklaşık olarak doğrusal yapabileceğiniz anlamına gelir.
wizzwizz4

22

Bir süre önce yazılan bu güzel makaleye göre ...

Kodun büyüsü, onu takip edemeseniz bile, i = 0x5f3759df - (i >> 1); hat. Basitleştirilmiş, Newton-Raphson bir tahminle başlayan ve onu yinelemeyle rafine eden bir yaklaşımdır. 32-bit x86 işlemcilerin doğasından yararlanarak, bir tam sayı olan i, başlangıçta bir tamsayı çevrim kullanarak ters karesini almak istediğiniz kayan nokta sayısının değerine ayarlanır. i sonra 0x5f3759df olarak ayarlandı, eksi kendisi bir bit sağa kaydırıldı. Sağ kaydırma, en önemsiz i parçasını düşürür ve esasen onu yarıya indirir.

Gerçekten iyi bir okuma. Bu sadece küçük bir parçası.


19

Değişken olarak sabitin ne olduğunu merak ediyordum, bu yüzden basitçe bu kod parçasını yazdım ve ortaya çıkan tamsayıyı googledim.

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

Sabit, "0x5f3759df kayan nokta gösteriminin onaltılık biçimi tarafından daha iyi bilinen 2 ^ 127'nin kareköküne tam sayı yaklaşımı" https://mrob.com/pub/math/numbers-18.html gibi görünüyor

Aynı sitede her şeyi açıklıyor. https://mrob.com/pub/math/numbers-16.html#le009_16


6
Bu daha fazla ilgiyi hak ediyor. Bunun sadece 2 ^ 127'nin karekökü olduğunun farkına
vardıktan
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.