SSE skaler sqrt (x) neden rsqrt (x) * x'ten daha yavaş?


106

Intel Core Duo'da bazı temel matematiğimizin profilini çıkarıyorum ve karekök için çeşitli yaklaşımlara bakarken tuhaf bir şey fark ettim: SSE skaler işlemlerini kullanarak, karşılıklı bir karekök alıp çarpmak daha hızlı sqrt'yi elde etmek için, yerel sqrt opcode kullanmaktan çok!

Şunun gibi bir döngü ile test ediyorum:

inline float TestSqrtFunction( float in );

void TestFunc()
{
  #define ARRAYSIZE 4096
  #define NUMITERS 16386
  float flIn[ ARRAYSIZE ]; // filled with random numbers ( 0 .. 2^22 )
  float flOut [ ARRAYSIZE ]; // filled with 0 to force fetch into L1 cache

  cyclecounter.Start();
  for ( int i = 0 ; i < NUMITERS ; ++i )
    for ( int j = 0 ; j < ARRAYSIZE ; ++j )
    {
       flOut[j] = TestSqrtFunction( flIn[j] );
       // unrolling this loop makes no difference -- I tested it.
    }
  cyclecounter.Stop();
  printf( "%d loops over %d floats took %.3f milliseconds",
          NUMITERS, ARRAYSIZE, cyclecounter.Milliseconds() );
}

Bunu TestSqrtFunction için birkaç farklı vücutla denedim ve gerçekten kafamı kaşıyan bazı zamanlamalarım var. En kötüsü, yerel sqrt () işlevini kullanmak ve "akıllı" derleyicinin "optimize etmesine" izin vermekti. 24ns / float'ta, x87 FPU kullanıldığında bu çok kötüydü:

inline float TestSqrtFunction( float in )
{  return sqrt(in); }

Bir sonraki denediğim şey, derleyiciyi SSE'nin skalar sqrt işlem kodunu kullanmaya zorlamak için bir intrinsic kullanmaktı:

inline void SSESqrt( float * restrict pOut, float * restrict pIn )
{
   _mm_store_ss( pOut, _mm_sqrt_ss( _mm_load_ss( pIn ) ) );
   // compiles to movss, sqrtss, movss
}

Bu 11.9ns / float ile daha iyiydi. Ben de denedim Carmack'in kaçık Newton Raphson yaklaşım tekniği rağmen 2'de 1'lik bir hata ile, 4.3ns / şamandıra, hatta daha iyi donanım daha koştu, 10 (benim amaçlar için çok fazla).

Doozy, karşılıklı karekök için SSE işlemini denediğimde ve daha sonra karekökü (x * 1 / √x = √x) elde etmek için bir çarpma kullandığım zamandı. Bu iki bağımlı işlemleri sürüyor olsa da, en hızlı çözüm ile uzak, 1.24ns / şamandıra de ve doğru 2'ye oldu -14 :

inline void SSESqrt_Recip_Times_X( float * restrict pOut, float * restrict pIn )
{
   __m128 in = _mm_load_ss( pIn );
   _mm_store_ss( pOut, _mm_mul_ss( in, _mm_rsqrt_ss( in ) ) );
   // compiles to movss, movaps, rsqrtss, mulss, movss
}

Benim sorum temelde ne verir ? SSE'nin donanıma yerleşik karekök işlem kodu, onu diğer iki matematik işleminden sentezlemekten neden daha yavaş ?

Eminim bu gerçekten operasyonun kendisinin maliyeti, çünkü doğruladım:

  • Tüm veriler önbelleğe sığar ve erişimler sıralıdır
  • işlevler satır içi
  • Döngüyü açmak fark etmez
  • derleyici bayrakları tam optimizasyona ayarlandı (ve montaj iyi, kontrol ettim)

( düzenleme : stephentyrone, uzun sayı dizilerindeki işlemlerin vektörleştirici SIMD paketlenmiş işlemlerini kullanması gerektiğini doğru bir şekilde belirtir rsqrtps- ancak buradaki dizi veri yapısı yalnızca test amaçlıdır: gerçekten ölçmeye çalıştığım şey , kodda kullanım için skaler performans bu vektörleştirilemez.)


13
x / sqrt (x) = sqrt (x). Veya başka bir şekilde söyleyin: x ^ 1 * x ^ (- 1/2) = x ^ (1 - 1/2) = x ^ (1/2) = sqrt (x)
Crashworks

6
tabii ki inline float SSESqrt( float restrict fIn ) { float fOut; _mm_store_ss( &fOut, _mm_sqrt_ss( _mm_load_ss( &fIn ) ) ); return fOut; }. Ancak bu kötü bir fikir çünkü CPU kayan sayıları yığına yazıp ardından hemen geri okursa, yük-vurma-depolanma duraklamasına kolayca neden olabilir - özellikle dönüş değeri için vektör yazmacından bir kayan yazmacıya hokkabazlık yaparak kötü haber. Ayrıca, SSE içsellerinin temsil ettiği temel makine işlem kodları yine de adres işlenenlerini alır.
Crashworks

4
Ne kadar LHS'nin önemi, belirli bir x86'nın belirli genine ve adımlamasına bağlıdır: Benim deneyimim, eaxi7'ye kadar olan herhangi bir şeyde, verilerin kayıt kümeleri arasında (örneğin , FPU'dan SSE'ye) taşınması çok kötüyken, xmm0 ile yığın arasında bir gidiş dönüş Intel'in mağaza yönlendirmesi nedeniyle geri gelmiyor. Kesin olarak görmek için kendinize zaman ayırabilirsiniz. Genel olarak, potansiyel LHS'yi görmenin en kolay yolu, yayılan düzeneğe bakmak ve kayıt kümeleri arasında verilerin nerede oynandığını görmektir; derleyiciniz akıllıca bir şey yapabilir veya yapmayabilir. Normalleştirme vektörlere gelince, ben burada benim sonuçlarını yazdı: bit.ly/9W5zoU
Crashworks

2
PowerPC için evet: IBM, LHS'yi ve diğer birçok boru hattı balonunu statik analiz yoluyla tahmin edebilen bir CPU simülatörüne sahiptir. Bazı PPC'lerin ayrıca LHS için sorgulayabileceğiniz bir donanım sayacı vardır. X86 için daha zor; iyi profil oluşturma araçları daha azdır (VTune bu günlerde bir şekilde bozuktur) ve yeniden sıralanan ardışık düzenler daha az belirleyicidir. Donanım performans sayaçlarıyla kesin olarak yapılabilen döngü başına talimatları ölçerek deneysel olarak ölçmeyi deneyebilirsiniz. "Kullanımdan kaldırılan talimatlar" ve "toplam döngü" kayıtları, örneğin PAPI veya PerfSuite ( bit.ly/an6cMt ) ile okunabilir .
Crashworks

2
Ayrıca, bir fonksiyona birkaç permütasyon yazabilir ve özellikle duraklamalardan muzdarip olup olmadığını görmek için bunları zamanlayabilirsiniz. Intel, boru hatlarının çalışma şekli hakkında pek çok ayrıntı yayınlamıyor (LHS'nin kirli bir sır olduğu), bu yüzden öğrendiğim şeylerin çoğu, diğer kemerlerde durmaya neden olan bir senaryoya bakmaktı (örneğin, PPC ) ve ardından x86'nın buna sahip olup olmadığını görmek için kontrollü bir deney oluşturun.
Crashworks

Yanıtlar:


216

sqrtssdoğru şekilde yuvarlatılmış bir sonuç verir. tersine yaklaşık 11 bitlik doğru rsqrtssbir yaklaşım verir .

sqrtssdoğruluk gerektiğinde çok daha doğru bir sonuç üretiyor. rsqrtssbir yaklaşımın yeterli olduğu ancak hızın gerekli olduğu durumlar için mevcuttur. Intel'in belgelerini okursanız, neredeyse tam kesinlik (doğru hatırlıyorsam ~ 23 bitlik doğruluk) veren ve hala bir yönerge dizisi (karşılıklı karekök yaklaşımı ve ardından tek bir Newton-Raphson adımı) bulacaksınız. daha hızlı sqrtss.

edit: Eğer hız kritikse ve bunu gerçekten birçok değer için bir döngü içinde çağırıyorsanız, bu komutların vektörleştirilmiş versiyonlarını rsqrtpsveya sqrtpsher ikisi de komut başına dört kayan nokta işleyen her ikisini de kullanıyor olmalısınız .


3
N / r adımı size 22 bitlik doğruluk verir (ikiye katlar); 23 bit tam doğruluk olacaktır.
Jasper Bekkers

7
@Jasper Bekkers: Hayır, olmaz. İlk olarak, şamandıranın 24 bitlik hassasiyeti vardır. İkinci olarak, sqrtssbir doğru yuvarlatılmış 50 bit yuvarlama önce ~ gerektirir ve tek hassas, basit bir N / R, yineleme kullanarak elde edilemeyen.
Stephen Canon

1
Sebep kesinlikle budur. Bu sonucu genişletmek için: Intel'in Embree projesi ( software.intel.com/en-us/articles/… ), matematiği için vektörleştirmeyi kullanır. Kaynağı bu bağlantıdan indirebilir ve 3/4 D Vektörlerini nasıl yaptıklarına bakabilirsiniz. Onların vektör normalizasyonu rsqrt ve ardından newton-raphson yinelemesini kullanır, bu daha sonra çok doğru ve hala 1 / ssqrt'den daha hızlıdır!
Brandon Pelfrey

7
Küçük bir uyarı: x rsqrt (x), x sıfır veya sonsuzsa NaN ile sonuçlanır. 0 * rsqrt (0) = 0 * INF = NaN. INF rsqrt (INF) = INF * 0 = NaN. Bu nedenle, NVIDIA GPU'lardaki CUDA, hem karşılıklı hem de karşılıklı karekök için hızlı bir yaklaşıklık sağlayan donanımla, yaklaşık tek duyarlıklı karekökleri rec olarak hesaplar (rsqrt (x)). Açıkçası, iki özel durumu ele alan açık kontroller de mümkündür (ancak GPU'da daha yavaş olacaktır).
njuffa

@BrandonPelfrey Newton Rhapson adımını hangi dosyada buldunuz?
fredoverflow

7

Bu, bölünme için de geçerlidir. MULSS (a, RCPSS (b)) DIVSS'den (a, b) çok daha hızlıdır. Aslında, bir Newton-Raphson yinelemesiyle hassasiyetini artırdığınızda bile daha hızlıdır.

Intel ve AMD, bu tekniği optimizasyon kılavuzlarında tavsiye ediyor. IEEE-754 uyumluluğu gerektirmeyen uygulamalarda div / sqrt kullanmanın tek nedeni kod okunabilirliğidir.


1
Broadwell ve daha sonra daha iyi FP bölme performansına sahiptir, bu nedenle clang gibi derleyiciler son CPU'larda skaler için karşılıklı + Newton kullanmamayı tercih eder, çünkü genellikle daha hızlı değildir . Çoğu döngüde, divtek işlem tek işlem değildir, bu nedenle toplam uop verimi genellikle bir divpsveya olduğunda bile darboğazdır divss. Bkz kayan nokta çarpma vs noktası bölümü Kayan cevabım neden bir bölüm vardır, rcppsartık kazanmak üretilen iş bir değil. (Veya bir gecikme kazancı) ve bölme işlem hacmi / gecikme sayıları.
Peter Cordes

Doğruluk gereksinimleriniz bir Newton yinelemesini atlayabilecek kadar düşükse, o zaman evet a * rcpss(b)daha hızlı olabilir, ancak yine de bundan daha zahmetlidir a/b!
Peter Cordes

5

Aslında yanlış olabilecek bir cevap vermek yerine (ayrıca önbellek ve diğer şeyleri kontrol etmeyeceğim veya tartışmayacağım, diyelim ki bunların aynı olduğunu söyleyelim) Sizi sorunuza cevap verebilecek kaynağı göstermeye çalışacağım.
Fark, sqrt ve rsqrt'nin nasıl hesaplandığına bağlı olabilir. Daha fazla bilgiyi burada http://www.intel.com/products/processor/manuals/ okuyabilirsiniz . Kullanmakta olduğunuz işlemci işlevleri hakkında okumaya başlamanızı öneririm, özellikle rsqrt hakkında bazı bilgiler var (cpu, sonucu elde etmeyi çok daha basit hale getiren büyük bir yaklaşımla dahili arama tablosu kullanıyor). Görünüşe göre rsqrt, sqrt'den çok daha hızlıdır, 1 ek çoklu işlem (ki bu maliyetli değildir) buradaki durumu değiştirmeyebilir.

Düzenleme: Bahsetmeye değer birkaç gerçek:
1. Bir zamanlar grafik kütüphanem için bazı mikro optimizasyonlar yapıyordum ve vektörlerin uzunluklarını hesaplamak için rsqrt kullandım. (sqrt yerine, karelerimin karesini rsqrt ile çarptım, testlerinizde yaptığınız tam olarak buydu) ve daha iyi performans gösterdi.
2. Basit arama tablosu kullanarak rsqrt'yi hesaplamak daha kolay olabilir, rsqrt için, x sonsuza gittiğinde, 1 / sqrt (x) 0'a gider, bu nedenle küçük x'ler için fonksiyon değerleri değişmez (çok), oysa sqrt - sonsuza gider, bu yüzden bu kadar basit;).

Ayrıca açıklama: Bağladığım kitaplarda onu nerede bulduğumdan emin değilim, ancak rsqrt'nin bazı arama tablosu kullandığını okuduğuma eminim ve yalnızca sonuç olduğunda kullanılmalıdır tam olmasına gerek yok - bir süre önce olduğu gibi ben de yanılıyor olabilirim :).


4

Newton-Raphson , türevin nerede olduğuna f(x)eşit artışları kullanarak sıfıra yakınsar .-f/f'f'

İçin x=sqrt(y), sen çözmeyi deneyebilirsiniz f(x) = 0için xkullanarak f(x) = x^2 - y;

O zaman artış: dx = -f/f' = 1/2 (x - y/x) = 1/2 (x^2 - y) / x içinde yavaş bir bölünmeye sahip.

Diğer işlevleri deneyebilirsiniz (gibi f(x) = 1/y - 1/x^2), ancak bunlar da aynı derecede karmaşık olacaktır.

Şimdi bakalım 1/sqrt(y). Deneyebilirsiniz f(x) = x^2 - 1/y, ancak aynı derecede karmaşık olacaktır: dx = 2xy / (y*x^2 - 1)örneğin. Şunlar için açık olmayan alternatif bir seçenek f(x)şudur:f(x) = y - 1/x^2

Sonra: dx = -f/f' = (y - 1/x^2) / (2/x^3) = 1/2 * x * (1 - y * x^2)

Ah! Bu önemsiz bir ifade değil, ama içinde sadece çarpımlar var, bölünme yok. => Daha hızlı!

Ve: tam güncelleme adımı new_x = x + dxdaha sonra okur:

x *= 3/2 - y/2 * x * x ki bu da kolaydır.


2

Buna birkaç yıl öncesine ait başka cevaplar da var. İşte fikir birliğinin doğru olduğu:

  • Rsqrt * komutları karşılıklı karekök yaklaşık 11-12 bit arası iyi bir yaklaşım hesaplar.
  • Mantis tarafından indekslenen bir arama tablosu (yani bir ROM) ile gerçekleştirilir. (Aslında, eski matematik tablolarına benzer, transistörlerden tasarruf etmek için düşük sıralı bitlerde ayarlamalar kullanan sıkıştırılmış bir arama tablosu.)
  • Kullanılabilir olmasının nedeni, FPU tarafından "gerçek" karekök algoritması için kullanılan ilk tahmin olmasıdır.
  • Ayrıca yaklaşık bir karşılıklı talimat var, rcp. Bu talimatların her ikisi de, FPU'nun karekök ve bölmeyi nasıl uyguladığına dair bir ipucudur.

İşte fikir birliğinin yanlış yaptığı şey:

  • SSE dönemi FPU'ları, karekökleri hesaplamak için Newton-Raphson kullanmazlar. Bu, yazılımda harika bir yöntemdir, ancak bunu donanımda bu şekilde uygulamak bir hata olur.

Karşılıklı karekök hesaplamak için NR algoritması, diğerlerinin de belirttiği gibi şu güncelleme adımına sahiptir:

x' = 0.5 * x * (3 - n*x*x);

Bu, çok fazla veriye bağlı çarpma ve bir çıkarma.

Aşağıda, modern FPU'ların gerçekte kullandığı algoritma yer almaktadır.

Verilen b[0] = nbiz sayı dizisi bulabilirsiniz varsayalım Y[i]böyle b[n] = b[0] * Y[0]^2 * Y[1]^2 * ... * Y[n]^21. Sonra düşünün yaklaşımlar:

x[n] = b[0] * Y[0] * Y[1] * ... * Y[n]
y[n] = Y[0] * Y[1] * ... * Y[n]

Açıkça x[n]yaklaşımlar sqrt(n)ve y[n]yaklaşımlar 1/sqrt(n).

İyi bir sonuç elde etmek için, karşılıklı karekök için Newton-Raphson güncelleme adımını kullanabiliriz Y[i]:

b[i] = b[i-1] * Y[i-1]^2
Y[i] = 0.5 * (3 - b[i])

Sonra:

x[0] = n Y[0]
x[i] = x[i-1] * Y[i]

ve:

y[0] = Y[0]
y[i] = y[i-1] * Y[i]

Bir sonraki önemli gözlem şudur b[i] = x[i-1] * y[i-1]. Yani:

Y[i] = 0.5 * (3 - x[i-1] * y[i-1])
     = 1 + 0.5 * (1 - x[i-1] * y[i-1])

Sonra:

x[i] = x[i-1] * (1 + 0.5 * (1 - x[i-1] * y[i-1]))
     = x[i-1] + x[i-1] * 0.5 * (1 - x[i-1] * y[i-1]))
y[i] = y[i-1] * (1 + 0.5 * (1 - x[i-1] * y[i-1]))
     = y[i-1] + y[i-1] * 0.5 * (1 - x[i-1] * y[i-1]))

Yani, başlangıçtaki x ve y verildiğinde, aşağıdaki güncelleme adımını kullanabiliriz:

r = 0.5 * (1 - x * y)
x' = x + x * r
y' = y + y * r

Ya da meraklısı bile ayarlayabiliriz h = 0.5 * y. Bu başlatmadır:

Y = approx_rsqrt(n)
x = Y * n
h = Y * 0.5

Ve bu güncelleme adımı:

r = 0.5 - x * h
x' = x + x * r
h' = h + h * r

Bu Goldschmidt'in algoritmasıdır ve bunu donanımda uyguluyorsanız çok büyük bir avantajı vardır: "iç döngü" üç çarpmalı toplamadır ve başka hiçbir şey değildir ve bunlardan ikisi bağımsızdır ve ardışık düzenlenebilir.

1999'da, FPU'lar zaten bir ardışık düzenlenmiş toplama / çıkarma devresine ve ardışık düzenlenmiş bir çoğaltma devresine ihtiyaç duyuyordu, aksi takdirde SSE çok "akış" olmazdı. 1999 yılında, bu iç döngüyü, yalnızca karekök üzerinde çok fazla donanım israfına neden olmadan, tamamen boru hatlı bir şekilde uygulamak için her devreden yalnızca birine ihtiyaç duyuldu.

Bugün, elbette, programcıya maruz kalan çarpma-toplamayı birleştirdik. Yine, iç döngü, karekökleri hesaplamasanız bile (yine) genel olarak yararlı olan üç ardışık düzenlenmiş FMA'dır.


1
İlgili: GCC'nin sqrt () 'si derlendikten sonra nasıl çalışır? Hangi kök yöntemi kullanılır? Newton-Raphson? donanım div / sqrt yürütme birimi tasarımlarına bazı bağlantıları vardır. Hassaslığa bağlı olarak SSE / AVX ile hızlı vektörleştirilmiş rsqrt ve karşılıklı - _mm256_rsqrt_psHaswell perf analizi ile birlikte kullanılmak üzere FMA ile veya FMA olmadan yazılımda bir Newton yineleme . Genellikle, döngüde başka bir işiniz yoksa ve bölücü veriminde büyük darboğaz oluşturacaksanız iyi bir fikirdir. HW sqrt tek bir uop olduğundan, diğer işlerle karıştırılır.
Peter Cordes

-2

Daha hızlıdır çünkü bu talimatlar yuvarlama modlarını yok sayar ve kayan nokta istisnalarını veya normalize olmayan sayıları işlemez. Bu nedenlerden ötürü, diğer fp komutlarını sıraya dizmek, speküle etmek ve yürütmek çok daha kolaydır.


Açıkçası yanlış. FMA, geçerli yuvarlama moduna bağlıdır, ancak Haswell ve sonraki sürümlerde saat başına iki işlem hacmine sahiptir. Tamamen ardışık düzenlenmiş iki FMA ünitesi ile Haswell, aynı anda 10 adede kadar FMA'ya sahip olabilir. Doğru cevap rsqrt'ın çok bir başlangıç tahmin almak için bir masa arama sonrasında çok daha az yapacak işi (veya hiç hiçbiri?) Anlamına alt doğruluğu,.
Peter Cordes
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.