J'ai profilé certaines de nos mathématiques de base sur un Intel Core Duo, et en examinant diverses approches de la racine carrée, j'ai remarqué quelque chose d'étrange: en utilisant les opérations scalaires SSE, il est plus rapide de prendre une racine carrée réciproque et de la multiplier. pour obtenir le sqrt, que pour utiliser l'opcode sqrt natif!
Je le teste 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é cela avec quelques corps différents pour la TestSqrtFunction, et j'ai des timings qui me grattent vraiment la tête. Le pire de tous était de loin d'utiliser la fonction native sqrt () et de laisser le compilateur «intelligent» «optimiser». À 24ns / float, en utilisant le FPU x87, c'était pathétiquement mauvais:
inline float TestSqrtFunction( float in )
{ return sqrt(in); }
La prochaine chose que j'ai essayée était d'utiliser un intrinsèque pour forcer le compilateur à utiliser l'opcode scalar sqrt de SSE:
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 / flotteur. J'ai aussi essayé la technique d'approximation farfelue Newton-Raphson de Carmack , qui fonctionnait encore mieux que le matériel, à 4,3 ns / flottant, bien qu'avec une erreur de 1 sur 2 10 (ce qui est trop pour mes besoins).
Le problème, c'était quand j'ai essayé l'opération SSE pour la racine carrée réciproque , puis utilisé une multiplication pour obtenir la racine carrée (x * 1 / √x = √x). Même si cela nécessite deux opérations dépendantes, c'était de loin la solution la plus rapide, à 1,24 ns / flottant et précise à 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 essentiellement ce qui donne ? Pourquoi l'opcode racine carrée intégré au matériel de SSE est- il plus lent que de le synthétiser à partir de deux autres opérations mathématiques?
Je suis sûr que c'est vraiment le coût de l'opération elle-même, car j'ai vérifié:
- Toutes les données tiennent dans le cache et les accès sont séquentiels
- les fonctions sont intégrées
- dérouler la boucle ne fait aucune différence
- les indicateurs du compilateur sont définis sur l'optimisation complète (et l'assemblage est bon, j'ai vérifié)
( modifier : la stephentyrone souligne correctement que les opérations sur de longues chaînes de nombres devraient utiliser les opérations de vectorisation SIMD compactées, comme rsqrtps
- mais la structure de données du tableau ici est uniquement à des fins de test: ce que j'essaie vraiment de mesurer, ce sont les performances scalaires à utiliser dans le code qui ne peut pas être vectorisé.)
inline float SSESqrt( float restrict fIn ) { float fOut; _mm_store_ss( &fOut, _mm_sqrt_ss( _mm_load_ss( &fIn ) ) ); return fOut; }
. Mais c'est une mauvaise idée car cela peut facilement induire un décrochage du magasin de chargement si le processeur écrit les flottants dans la pile puis les lit immédiatement - jonglant du registre vectoriel à un registre flottant pour la valeur de retour en particulier est une mauvaise nouvelle. En outre, les opcodes machine sous-jacents que les intrinsèques SSE représentent prennent de toute façon des opérandes d'adresse.
eax
) est très mauvais, tandis qu'un aller-retour entre xmm0 et pile et le retour ne l'est pas, à cause du transfert de magasin d'Intel. Vous pouvez le chronométrer vous-même pour en être sûr. Généralement, le moyen le plus simple de voir le potentiel LHS est de regarder l'assemblage émis et de voir où les données sont jonglées entre les ensembles de registres; votre compilateur peut faire la chose intelligente, ou pas. En ce qui concerne la normalisation des vecteurs, j'ai écrit mes résultats ici: bit.ly/9W5zoU