J'ai été profilage certains de nos noyau de calcul sur un processeur Intel Core Duo, et en regardant les diverses approches de la racine carrée, j'ai remarqué quelque chose d'étrange: à l'aide de l'ESS scalaire opérations, il est plus rapide de prendre une réciprocité de racine carrée et de le multiplier pour obtenir la racine carrée, c'est d'utiliser le natif sqrt opcode!
Je suis en essais avec une boucle quelque chose comme:
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() );
}
J'ai essayé avec différents organismes pour la TestSqrtFunction, et j'ai quelques timings qui sont vraiment à me gratter la tête. Le pire de tous, de loin, l'aide la native sqrt() de la fonction et de laisser le "smart" compilateur "optimiser". Au 24ns/float, à l'aide de la FPU x87 c'était pathétiquement mauvais:
inline float TestSqrtFunction( float in )
{ return sqrt(in); }
La prochaine chose que j'ai essayé a l'aide d'un intrinsèques pour forcer le compilateur à utiliser de l'ESS scalaire sqrt opcode:
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
}
C'était mieux, à 11,9 ns/float. J'ai aussi essayé Carmack est loufoque Newton-Rhapson rapprochement technique, qui a couru encore mieux que le matériel, à 4,3 ns/float, mais avec une erreur de 1 à 210 (qui est beaucoup trop pour mes besoins).
La rude bataille a été quand j'ai essayé de l'ESS op pour la réciproque de la racine carrée, et ensuite utilisé un de se multiplier pour obtenir la racine carrée ( x * 1/√x = √x ). Même si cela prend deux opérations dépendantes, c'était la solution la plus rapide, et de loin, à 1,24 ns/float et précises à 2-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
}
Ma question est fondamentalement ce qui donne? Pourquoi est-ESS intégré-à-matériel racine carrée de l'opcode plus lent que la synthèse de la sortir de deux autres opérations mathématiques?
Je suis sûr que c'est vraiment le coût de l'op elle-même, parce que j'ai vérifié:
- Toutes les données s'inscrit dans le cache, et accès séquentiel
- les fonctions sont intégrées
- dérouler la boucle ne fait aucune différence
- les drapeaux de compilation sont définies pour une optimisation complète (et l'ensemble est bonne, j'ai vérifié)
(edit: stephentyrone souligne à juste titre que les opérations sur les longues chaînes de chiffres devraient utiliser la vectorisation SIMD paniers ops, comme rsqrtps
— mais la matrice de structure de données est ici à des fins de test uniquement: ce que je suis vraiment en essayant de mesure scalaire de la performance pour une utilisation dans le code qui ne peut pas être vectorisé.)