Использование встроенных 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;
}

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

4 ответа

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам