Использование встроенных AVX вместо SSE не улучшает скорость - почему?

Я использую встроенные функции Intel SSE в течение некоторого времени с хорошей производительностью. Следовательно, я ожидал, что возможности AVX еще больше ускорят мои программы. К сожалению, до сих пор это было не так. Наверное, я делаю глупую ошибку, поэтому я был бы очень благодарен, если бы кто-нибудь мог мне помочь.

Я использую Ubuntu 11.10 с g++ 4.6.1. Я скомпилировал свою программу (см. Ниже) с помощью

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

В тестовой системе установлен процессор Intel i7-2600.

Вот код, который иллюстрирует мою проблему. В моей системе я получаю вывод

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

Обратите внимание, что вычисление sqrt (sqrt (sqrt (x))) было выбрано только для обеспечения того, чтобы полоса пропускания памяти не ограничивала скорость выполнения; это просто пример.

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;
}

Любая помощь приветствуется!

Ответы

Ответ 1

Это потому, что VSQRTPS (инструкция AVX) занимает ровно в два раза больше циклов, чем SQRTPS (инструкция SSE) на процессоре Sandy Bridge. См. Руководство по оптимизации Agner Fog: таблицы инструкций, стр. 88.

Инструкции, такие как квадратный корень и разделение, не имеют преимущества от AVX. С другой стороны, дополнения, умножения и т.д. Делают.

Ответ 2

Если вы заинтересованы в увеличении производительности квадратного корня, вместо VSQRTPS вы можете использовать формулу VRSQRTPS и Newton-Raphson:

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

Сам VRSQRTPS не использует AVX, но другие вычисления делают.

Используйте его, если вам достаточно 23 бит точности.

Ответ 3

Просто для полноты. Реализация Newton-Raphson (NR) для операций, таких как деление или квадратный корень, будет полезной только в том случае, если у вас ограниченное количество этих операций в вашем коде. Это связано с тем, что если вы использовали эти альтернативные методы, вы будете оказывать большее давление на другие порты, такие как порты умножения и добавления. Это в основном причина, почему архитектуры x86 имеют специальный аппаратный блок для обработки этих операций вместо альтернативных программных решений (например, NR). Я цитирую Intel 64 и IA-32 Справочное руководство по оптимизации архитектуры стр .556:

"В некоторых случаях, когда операции с делением или квадратным корнем являются частью более крупного алгоритма, который скрывает некоторую задержку этих операций, аппроксимация с Newton-Raphson может замедлить выполнение.

Поэтому будьте осторожны при использовании NR в больших алгоритмах. На самом деле, у меня была моя магистерская диссертация по этому вопросу, и я оставлю ссылку на нее здесь для дальнейшего использования, как только она будет опубликована.

Также для людей, как всегда удивляться пропущению и задержке некоторых инструкций, посмотрите IACA. Это очень полезный инструмент, предоставляемый корпорацией Intel для статического анализа эффективности выполнения кода в ядре.

отредактированный вот ссылка на тезис для тех, кто заинтересован тезис

Ответ 4

В зависимости от вашего процессорного оборудования инструкции AVX могут быть эмулированы в аппаратном обеспечении в виде инструкций SSE. Вам нужно будет найти номер своего процессора, чтобы получить точные спецификации на нем, но это одно из основных различий между процессорами Intel для младших и высокопроизводительных процессоров, количеством специализированных исполнительных блоков и аппаратной эмуляцией.