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.)
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.
eax
i7'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