web-dev-qa-db-fra.com

Utiliser intrinsèque AVX au lieu de SSE n'améliore pas la vitesse - pourquoi?

J'utilise Intel SSE intrinsèques depuis un certain temps avec de bons gains de performances. Par conséquent, je m'attendais à ce que les intrinsèques AVX accélèrent davantage mes programmes. Malheureusement, ce n'était pas le cas avant Je fais probablement une erreur stupide, donc je serais très reconnaissant si quelqu'un pouvait m'aider.

J'utilise Ubuntu 11.10 avec g ++ 4.6.1. J'ai compilé mon programme (voir ci-dessous) avec

g++ simpleExample.cpp -O3 -march=native -o simpleExample

Le système de test possède un processeur Intel i7-2600.

Voici le code qui illustre mon problème. Sur mon système, j'obtiens la sortie

98.715 ms, b[42] = 0.900038 // Naive
24.457 ms, b[42] = 0.900038 // SSE
24.646 ms, b[42] = 0.900038 // AVX

Notez que le calcul sqrt (sqrt (sqrt (x))) a été choisi uniquement pour garantir que la bande passante mémoire ne limite pas la vitesse d'exécution; ce n'est qu'un exemple.

simpleExample.cpp:

#include <immintrin.h>
#include <iostream>
#include <math.h> 
#include <sys/time.h>

using namespace std;

// -----------------------------------------------------------------------------
// This function returns the current time, expressed as seconds since the Epoch
// -----------------------------------------------------------------------------
double getCurrentTime(){
  struct timeval curr;
  struct timezone tz;
  gettimeofday(&curr, &tz);
  double tmp = static_cast<double>(curr.tv_sec) * static_cast<double>(1000000)
             + static_cast<double>(curr.tv_usec);
  return tmp*1e-6;
}

// -----------------------------------------------------------------------------
// Main routine
// -----------------------------------------------------------------------------
int main() {

  srand48(0);            // seed PRNG
  double e,s;            // timestamp variables
  float *a, *b;          // data pointers
  float *pA,*pB;         // work pointer
  __m128 rA,rB;          // variables for SSE
  __m256 rA_AVX, rB_AVX; // variables for AVX

  // define vector size 
  const int vector_size = 10000000;

  // allocate memory 
  a = (float*) _mm_malloc (vector_size*sizeof(float),32);
  b = (float*) _mm_malloc (vector_size*sizeof(float),32);

  // initialize vectors //
  for(int i=0;i<vector_size;i++) {
    a[i]=fabs(drand48());
    b[i]=0.0f;
  }

// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Naive implementation
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  s = getCurrentTime();
  for (int i=0; i<vector_size; i++){
    b[i] = sqrtf(sqrtf(sqrtf(a[i])));
  }
  e = getCurrentTime();
  cout << (e-s)*1000 << " ms" << ", b[42] = " << b[42] << endl;

// -----------------------------------------------------------------------------
  for(int i=0;i<vector_size;i++) {
    b[i]=0.0f;
  }
// -----------------------------------------------------------------------------

// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// SSE2 implementation
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  pA = a; pB = b;

  s = getCurrentTime();
  for (int i=0; i<vector_size; i+=4){
    rA   = _mm_load_ps(pA);
    rB   = _mm_sqrt_ps(_mm_sqrt_ps(_mm_sqrt_ps(rA)));
    _mm_store_ps(pB,rB);
    pA += 4;
    pB += 4;
  }
  e = getCurrentTime();
  cout << (e-s)*1000 << " ms" << ", b[42] = " << b[42] << endl;

// -----------------------------------------------------------------------------
  for(int i=0;i<vector_size;i++) {
    b[i]=0.0f;
  }
// -----------------------------------------------------------------------------

// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// AVX implementation
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  pA = a; pB = b;

  s = getCurrentTime();
  for (int i=0; i<vector_size; i+=8){
    rA_AVX   = _mm256_load_ps(pA);
    rB_AVX   = _mm256_sqrt_ps(_mm256_sqrt_ps(_mm256_sqrt_ps(rA_AVX)));
    _mm256_store_ps(pB,rB_AVX);
    pA += 8;
    pB += 8;
  }
  e = getCurrentTime();
  cout << (e-s)*1000 << " ms" << ", b[42] = " << b[42] << endl;

  _mm_free(a);
  _mm_free(b);

  return 0;
}

Toute aide est appréciée!

44
user1158218

En effet, VSQRTPS (instruction AVX) prend exactement deux fois plus de cycles que SQRTPS (instruction SSE) sur un processeur Sandy Bridge. Voir le guide d'optimisation d'Agner Fog: tableaux d'instructions , page 88.

Les instructions comme la racine carrée et la division ne bénéficient pas d'AVX. D'un autre côté, les ajouts, les multiplications, etc., le font.

43
Norbert P.

Si vous souhaitez augmenter les performances de la racine carrée, au lieu de VSQRTPS, vous pouvez utiliser la formule VRSQRTPS et Newton-Raphson:

x0 = vrsqrtps(a)
x1 = 0.5 * x0 * (3 - (a * x0) * x0)

VRSQRTPS lui-même ne bénéficie pas d'AVX, mais d'autres calculs le font.

Utilisez-le si 23 bits de précision vous suffisent.

10
Evgeny Kluev

Juste pour être complet. L'implémentation de Newton-Raphson (NR) pour des opérations comme la division ou la racine carrée ne sera bénéfique que si vous avez un nombre limité de ces opérations dans votre code. En effet, si vous avez utilisé ces méthodes alternatives, vous générerez plus de pression sur d'autres ports tels que les ports de multiplication et d'addition. C'est essentiellement la raison pour laquelle les architectures x86 ont une unité matérielle spéciale pour gérer ces opérations au lieu des solutions logicielles alternatives (comme NR). Je cite Intel 64 et IA-32 Architectures Optimization Reference Manual p.556:

"Dans certains cas, lorsque les opérations de division ou de racine carrée font partie d'un algorithme plus large qui masque une partie de la latence de ces opérations, l'approximation avec Newton-Raphson peut ralentir l'exécution."

Soyez donc prudent lorsque vous utilisez NR dans de grands algorithmes. En fait, j'ai eu ma thèse de maîtrise autour de ce point et je vais laisser un lien ici pour référence future, une fois qu'il sera publié.

Aussi, pour les gens qui se demandent toujours le débit et la latence de certaines instructions, jetez un œil à IACA . Il s'agit d'un outil très utile fourni par Intel pour analyser statiquement les performances d'exécution in-core des codes.

édité voici un lien vers la thèse pour ceux qui sont intéressés thèse

7
Salah Saleh

Selon le matériel de votre processeur, les instructions AVX peuvent être émulées dans le matériel sous la forme SSE instructions. Vous devrez rechercher le numéro de pièce de votre processeur pour obtenir des spécifications exactes, mais celle-ci en est une des principales différences entre les processeurs Intel bas de gamme et haut de gamme, le nombre d'unités d'exécution spécialisées par rapport à l'émulation matérielle.

6
SoapBox