web-dev-qa-db-fra.com

Pourquoi SSE scalaire sqrt (x) plus lent que rsqrt (x) * x?

J'ai profilé certaines de nos mathématiques de base sur un Intel Core Duo, et en regardant différentes approches de la racine carrée, j'ai remarqué quelque chose d'étrange: en utilisant les opérations scalaires SSE, c'est plus rapide de prendre une racine carrée réciproque et de la multiplier pour obtenir le sqrt, que d'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 font vraiment peur. De loin, le pire était 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 scalaire 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 également essayé la technique d'approximation farfelue de Newton-Raphson de Carmack , qui fonctionnait encore mieux que le matériel, à 4,3 ns/flottant, bien qu'avec une erreur de 1 sur 2dix (ce qui est trop pour mes besoins).

Le doozy a été lorsque j'ai essayé la racine carrée SSE pour réciproque, puis j'ai utilisé une multiplication pour obtenir la racine carrée (x * 1/√x = √x). Même si cela prend 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 qu'est-ce qui donne? Pourquoi l'opcode racine carrée intégré au matériel de SSE - 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 drapeaux du compilateur sont définis sur une optimisation complète (et l'assemblage est bon, j'ai vérifié)

( edit : stephentyrone souligne correctement que les opérations sur de longues chaînes de nombres doivent utiliser les opérations compactées SIMD vectorisantes, comme rsqrtps - mais le La structure des données du tableau est uniquement à des fins de test: ce que j'essaie vraiment de mesurer est scalaire les performances à utiliser dans du code qui ne peut pas être vectorisé.)

102
Crashworks

sqrtss donne un résultat correctement arrondi. rsqrtss donne une approximation à l'inverse, précise à environ 11 bits.

sqrtss génère un résultat beaucoup plus précis, lorsque la précision est requise. rsqrtss existe pour les cas où une approximation suffit, mais la vitesse est requise. Si vous lisez la documentation d'Intel, vous trouverez également une séquence d'instructions (approximation réciproque de racine carrée suivie d'une seule étape de Newton-Raphson) qui donne une précision presque complète (~ 23 bits de précision, si je me souviens bien), et est encore quelque peu plus rapide que sqrtss.

edit: Si la vitesse est critique et que vous l'appelez vraiment en boucle pour de nombreuses valeurs, vous devez utiliser les versions vectorisées de ces instructions, rsqrtps ou sqrtps, qui traitent tous les deux quatre flottants par instruction.

208
Stephen Canon

Cela vaut également pour la division. MULSS (a, RCPSS (b)) est bien plus rapide que DIVSS (a, b). En fait, c'est encore plus rapide même lorsque vous augmentez sa précision avec une itération de Newton-Raphson.

Intel et AMD recommandent tous deux cette technique dans leurs manuels d'optimisation. Dans les applications qui ne nécessitent pas la conformité IEEE-754, la seule raison d'utiliser div/sqrt est la lisibilité du code.

7
Spat

Au lieu de fournir une réponse, cela pourrait en fait être incorrect (je ne vais pas non plus vérifier ou argumenter sur le cache et d'autres choses, disons qu'elles sont identiques) Je vais essayer de vous indiquer la source qui peut répondre à votre question.
La différence pourrait résider dans la façon dont sqrt et rsqrt sont calculés. Vous pouvez en savoir plus ici http://www.intel.com/products/processor/manuals/ . Je suggère de commencer par lire les fonctions du processeur que vous utilisez, il y a quelques informations, en particulier sur rsqrt (le processeur utilise une table de recherche interne avec une énorme approximation, ce qui rend beaucoup plus simple l'obtention du résultat). Il peut sembler que rsqrt est tellement plus rapide que sqrt, qu'une opération mul supplémentaire (qui n'est pas trop coûteuse) pourrait ne pas changer la situation ici.

Edit: Quelques faits qui méritent d'être mentionnés:
1. Une fois, je faisais des micro-optimisations pour ma bibliothèque graphique et j'ai utilisé rsqrt pour calculer la longueur des vecteurs. (au lieu de sqrt, j'ai multiplié ma somme de carrés par rsqrt, ce qui est exactement ce que vous avez fait dans vos tests), et cela a mieux fonctionné.
2. Le calcul de rsqrt à l'aide d'une table de recherche simple pourrait être plus facile, comme pour rsqrt, lorsque x passe à l'infini, 1/sqrt (x) passe à 0, donc pour les petits x, les valeurs de la fonction ne changent pas (beaucoup), tandis que pour sqrt - ça va à l'infini, c'est donc ce cas simple;).

Aussi, clarification: je ne sais pas où je l'ai trouvé dans les livres que j'ai liés, mais je suis presque sûr d'avoir lu que rsqrt utilise une table de recherche, et elle ne devrait être utilisée que lorsque le résultat n'a pas besoin d'être exact, bien que - je puisse me tromper aussi, comme c'était il y a quelque temps :).

5
Marcin Deptuła

Newton-Raphson converge vers le zéro de f(x) en utilisant des incréments égaux à -f/f'f' Est la dérivée.

Pour x=sqrt(y), vous pouvez essayer de résoudre f(x) = 0 pour x en utilisant f(x) = x^2 - y;

L'incrément est alors: dx = -f/f' = 1/2 (x - y/x) = 1/2 (x^2 - y) / x qui comporte une lente division.

Vous pouvez essayer d'autres fonctions (comme f(x) = 1/y - 1/x^2) mais elles seront tout aussi compliquées.

Voyons maintenant 1/sqrt(y). Vous pouvez essayer f(x) = x^2 - 1/y, mais ce sera tout aussi compliqué: dx = 2xy / (y*x^2 - 1) par exemple. Un autre choix non évident pour f(x) est: f(x) = y - 1/x^2

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

Ah! Ce n'est pas une expression banale, mais vous n'avez que des multiplications, pas de division. => Plus vite!

Et: l'étape de mise à jour complète new_x = x + dx Se lit alors:

x *= 3/2 - y/2 * x * x Ce qui est facile aussi.

3
skal