Использование встроенных AVX вместо SSE не улучшает скорость - почему? - PullRequest
44 голосов
/ 19 января 2012

Я давно использую встроенные функции 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 ]

43 голосов
/ 19 января 2012

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

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

10 голосов
/ 19 января 2012

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

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

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

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

7 голосов
/ 17 марта 2015

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

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

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

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

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

6 голосов
/ 19 января 2012

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

...