AVX2 медленнее, чем SSE на Haswell

У меня есть следующий код (обычный, SSE и AVX):

int testSSE(const aligned_vector & ghs, const aligned_vector & lhs) {
    int result[4] __attribute__((aligned(16))) = {0};
    __m128i vresult = _mm_set1_epi32(0);
    __m128i v1, v2, vmax;

    for (int k = 0; k < ghs.size(); k += 4) {
        v1 = _mm_load_si128((__m128i *) & lhs[k]);
        v2 = _mm_load_si128((__m128i *) & ghs[k]);
        vmax = _mm_add_epi32(v1, v2);
        vresult = _mm_max_epi32(vresult, vmax);
    }
    _mm_store_si128((__m128i *) result, vresult);
    int mymax = result[0];
    for (int k = 1; k < 4; k++) {
        if (result[k] > mymax) {
            mymax = result[k];
        }
    }
    return mymax;
}

 int testAVX(const aligned_vector & ghs, const aligned_vector & lhs) {
    int result[8] __attribute__((aligned(32))) = {0};
    __m256i vresult = _mm256_set1_epi32(0);
    __m256i v1, v2, vmax;

    for (int k = 0; k < ghs.size(); k += 8) {
        v1 = _mm256_load_si256((__m256i *) & ghs[ k]);
        v2 = _mm256_load_si256((__m256i *) & lhs[k]);
        vmax = _mm256_add_epi32(v1, v2);
        vresult = _mm256_max_epi32(vresult, vmax);
    }
    _mm256_store_si256((__m256i *) result, vresult);
    int mymax = result[0];
    for (int k = 1; k < 8; k++) {
        if (result[k] > mymax) {
            mymax = result[k];
        }
    }
    return mymax;
}

int testNormal(const aligned_vector & ghs, const aligned_vector & lhs) {
    int max = 0;
    int tempMax;
    for (int k = 0; k < ghs.size(); k++) {
        tempMax = lhs[k] + ghs[k];
        if (max < tempMax) {
            max = tempMax;
        }
    }
    return max;
}

Все эти функции тестируются с помощью следующего кода:

void alignTestSSE() {
    aligned_vector lhs;
    aligned_vector ghs;

    int mySize = 4096;
    int FinalResult;
    int nofTestCases = 1000;
    double time, time1, time2, time3;
    vector<int> lhs2;
    vector<int> ghs2;

    lhs.resize(mySize);
    ghs.resize(mySize);
    lhs2.resize(mySize);
    ghs2.resize(mySize);

    srand(1);
    for (int k = 0; k < mySize; k++) {
        lhs[k] = randomNodeID(1000000);
        lhs2[k] = lhs[k];
        ghs[k] = randomNodeID(1000000);
        ghs2[k] = ghs[k];
    }
    /* Warming UP */
    for (int k = 0; k < nofTestCases; k++) {
        FinalResult = testNormal(lhs, ghs);
    }

    for (int k = 0; k < nofTestCases; k++) {
        FinalResult = testSSE(lhs, ghs);
    }

    for (int k = 0; k < nofTestCases; k++) {
        FinalResult = testAVX(lhs, ghs);
    }

    cout << "===========================" << endl;
    time = timestamp();
    for (int k = 0; k < nofTestCases; k++) {
        FinalResult = testSSE(lhs, ghs);
    }
    time = timestamp() - time;
    time1 = time;
    cout << "SSE took " << time << " s" << endl;
    cout << "SSE Result: " << FinalResult << endl;

    time = timestamp();
    for (int k = 0; k < nofTestCases; k++) {
        FinalResult = testAVX(lhs, ghs);
    }
    time = timestamp() - time;
    time3 = time;
    cout << "AVX took " << time << " s" << endl;
    cout << "AVX Result: " << FinalResult << endl;



    time = timestamp();
    for (int k = 0; k < nofTestCases; k++) {
        FinalResult = testNormal(lhs, ghs);
    }
    time = timestamp() - time;
    cout << "Normal took " << time << " s" << endl;
    cout << "Normal Result: " << FinalResult << endl;
    cout << "SpeedUP SSE= " << time / time1 << " s" << endl;
    cout << "SpeedUP AVX= " << time / time3 << " s" << endl;
    cout << "===========================" << endl;
    ghs.clear();
    lhs.clear();
}

Где

inline double timestamp() {
    struct timeval tp;
    gettimeofday(&tp, NULL);
    return double(tp.tv_sec) + tp.tv_usec / 1000000.;
}

А также

typedef vector<int, aligned_allocator<int, sizeof (int)> > aligned_vector;

представляет собой выровненный вектор, использующий AlignedAllocator https://gist.github.com/donny-dont/1471329

У меня есть Intel-i7 Haswell 4771 и последняя версия Ubuntu 14.04 64bit и gcc 4.8.2. Все обновлено. Я скомпилировал с -march=native -mtune=native -O3 -m64.

Результаты:

SSE took 0.000375986 s
SSE Result: 1982689
AVX took 0.000459909 s
AVX Result: 1982689
Normal took 0.00315714 s
Normal Result: 1982689
SpeedUP SSE= 8.39696 s
SpeedUP AVX= 6.8647 s

Что показывает, что тот же самый код на AVX2 на 22% медленнее, чем SSE. Я что-то не так делаю или это нормальное поведение?


person Alexandros    schedule 06.05.2014    source источник
comment
Смешивание инструкций AVX и SSE приводит к накладным расходам, поскольку чипу приходится обнулять старшую половину регистров. Я настоятельно рекомендую переместить ваши тесты AVX в файл, который вы компилируете с помощью -mavx, а затем использовать встроенный вызов vzeroall перед началом любой плавающей запятой в этом файле.   -  person Mgetz    schedule 06.05.2014
comment
Пока вы компилируете с -mavx2 и используете только встроенные функции (а не встроенную сборку), вы не должны подвергаться штрафу за переключение AVX-SSE.   -  person Paul R    schedule 06.05.2014
comment
Да это так. Даже полное удаление кода SSE и компиляция с параметром -mavx2 не ускоряют код. Я также пробовал _mm256_zeroall(); перед использованием инструкций AVX.   -  person Alexandros    schedule 06.05.2014


Ответы (3)


Я преобразовал ваш код в более ванильный C++ (простые массивы, без векторов и т. д.), очистил его и протестировал с отключенной автоматической векторизацией и получил разумные результаты:

#include <iostream>
using namespace std;

#include <sys/time.h>
#include <cstdlib>
#include <cstdint>

#include <immintrin.h>

inline double timestamp() {
    struct timeval tp;
    gettimeofday(&tp, NULL);
    return double(tp.tv_sec) + tp.tv_usec / 1000000.;
}

int testSSE(const int32_t * ghs, const int32_t * lhs, size_t n) {
    int result[4] __attribute__((aligned(16))) = {0};
    __m128i vresult = _mm_set1_epi32(0);
    __m128i v1, v2, vmax;

    for (int k = 0; k < n; k += 4) {
        v1 = _mm_load_si128((__m128i *) & lhs[k]);
        v2 = _mm_load_si128((__m128i *) & ghs[k]);
        vmax = _mm_add_epi32(v1, v2);
        vresult = _mm_max_epi32(vresult, vmax);
    }
    _mm_store_si128((__m128i *) result, vresult);
    int mymax = result[0];
    for (int k = 1; k < 4; k++) {
        if (result[k] > mymax) {
            mymax = result[k];
        }
    }
    return mymax;
}

int testAVX(const int32_t * ghs, const int32_t * lhs, size_t n) {
    int result[8] __attribute__((aligned(32))) = {0};
    __m256i vresult = _mm256_set1_epi32(0);
    __m256i v1, v2, vmax;

    for (int k = 0; k < n; k += 8) {
        v1 = _mm256_load_si256((__m256i *) & ghs[k]);
        v2 = _mm256_load_si256((__m256i *) & lhs[k]);
        vmax = _mm256_add_epi32(v1, v2);
        vresult = _mm256_max_epi32(vresult, vmax);
    }
    _mm256_store_si256((__m256i *) result, vresult);
    int mymax = result[0];
    for (int k = 1; k < 8; k++) {
        if (result[k] > mymax) {
            mymax = result[k];
        }
    }
    return mymax;
}

int testNormal(const int32_t * ghs, const int32_t * lhs, size_t n) {
    int max = 0;
    int tempMax;
    for (int k = 0; k < n; k++) {
        tempMax = lhs[k] + ghs[k];
        if (max < tempMax) {
            max = tempMax;
        }
    }
    return max;
}

void alignTestSSE() {

    int n = 4096;
    int normalResult, sseResult, avxResult;
    int nofTestCases = 1000;
    double time, normalTime, sseTime, avxTime;

    int lhs[n] __attribute__ ((aligned(32)));
    int ghs[n] __attribute__ ((aligned(32)));

    for (int k = 0; k < n; k++) {
        lhs[k] = arc4random();
        ghs[k] = arc4random();
    }

    /* Warming UP */
    for (int k = 0; k < nofTestCases; k++) {
        normalResult = testNormal(lhs, ghs, n);
    }

    for (int k = 0; k < nofTestCases; k++) {
        sseResult = testSSE(lhs, ghs, n);
    }

    for (int k = 0; k < nofTestCases; k++) {
        avxResult = testAVX(lhs, ghs, n);
    }

    time = timestamp();
    for (int k = 0; k < nofTestCases; k++) {
        normalResult = testNormal(lhs, ghs, n);
    }
    normalTime = timestamp() - time;

    time = timestamp();
    for (int k = 0; k < nofTestCases; k++) {
        sseResult = testSSE(lhs, ghs, n);
    }
    sseTime = timestamp() - time;

    time = timestamp();
    for (int k = 0; k < nofTestCases; k++) {
        avxResult = testAVX(lhs, ghs, n);
    }
    avxTime = timestamp() - time;

    cout << "===========================" << endl;
    cout << "Normal took " << normalTime << " s" << endl;
    cout << "Normal Result: " << normalResult << endl;
    cout << "SSE took " << sseTime << " s" << endl;
    cout << "SSE Result: " << sseResult << endl;
    cout << "AVX took " << avxTime << " s" << endl;
    cout << "AVX Result: " << avxResult << endl;
    cout << "SpeedUP SSE= " << normalTime / sseTime << endl;
    cout << "SpeedUP AVX= " << normalTime / avxTime << endl;
    cout << "===========================" << endl;

}

int main()
{
    alignTestSSE();
    return 0;
}

Тестовое задание:

$ clang++ -Wall -mavx2 -O3 -fno-vectorize SO_avx.cpp && ./a.out
===========================
Normal took 0.00324106 s
Normal Result: 2143749391
SSE took 0.000527859 s
SSE Result: 2143749391
AVX took 0.000221968 s
AVX Result: 2143749391
SpeedUP SSE= 6.14002
SpeedUP AVX= 14.6015
===========================

Я предлагаю вам попробовать приведенный выше код с -fno-vectorize (или -fno-tree-vectorize при использовании g++) и посмотреть, получите ли вы аналогичные результаты. Если вы это сделаете, вы можете вернуться к исходному коду, чтобы увидеть, откуда может исходить несоответствие.

person Paul R    schedule 06.05.2014
comment
-fno-tree-vectorize — это опция только для gcc, чтобы отключить векторизацию в clang, вам нужно -fno-vectorize - person ismail; 06.05.2014
comment
@ismail: хорошо, это работает с моей версией clang++ - это прямая копия и вставка с моего терминала. Я получаю идентичное поведение с -fno-vectorize или -fno-tree-vectorize. YMMV, конечно. - person Paul R; 06.05.2014
comment
Ну, это, вероятно, игнорирует флаг, но все равно, просто хотел это отметить :) - person ismail; 06.05.2014
comment
@ismail: нет, как я уже сказал, я получаю идентичное поведение с любым переключателем - если я полностью его оставлю, скалярный код будет векторизован, и я получу совершенно другой результат. Вероятно, это зависит от версии, но я все равно убрал -tree, чтобы избежать путаницы. Спасибо за указание на это. - person Paul R; 06.05.2014
comment
Благодаря @PaulR я понял. SSE и Normal работают одинаково для выровненных векторов и массивов, но AVX в два раза медленнее для выровненных векторов. На моем ПК код AVX с массивами в 1,5 раза быстрее, чем SSE. Итак, мне, вероятно, придется переписать код, чтобы использовать массивы для AVX. +1 и принял ваш ответ. Спасибо - person Alexandros; 06.05.2014
comment
Возможно, вы сможете придерживаться векторов - попробуйте создать локальные константные указатели на начало векторов в начале функции, чтобы вы не ссылались на вектор непосредственно внутри цикла. - person Paul R; 06.05.2014
comment
@PaulR ... создание локальных константных указателей на начало векторов. А это как делается? Я не очень хорошо разбираюсь в указателях (черт возьми мой фон Java) - person Alexandros; 06.05.2014
comment
Например. const int32_t * const lp = &lhs[0]; для создания локального указателя, а затем используйте lp[k] вместо lhs[k] внутри цикла. - person Paul R; 06.05.2014
comment
@PaulR, можешь объяснить, зачем нужна разминка? Почему код будет работать медленнее при первом вызове, чем при последующих вызовах? - person Z boson; 07.05.2014
comment
Обычно при бенчмаркинге любого кода рекомендуется выполнить хотя бы одну итерацию перед определением времени, чтобы кэши были прогреты, а любые ленивые распределения виртуальных машин были подключены. Вы также хотите быть уверены, что весь код также выгружается. Многое из этого, вероятно, неприменимо к текущему коду, но это хорошая привычка. - person Paul R; 07.05.2014
comment
@PaulR, я согласен, что сначала нужно запустить хотя бы одну итерацию, я просто не уверен, почему это помогает (я знаю, почему это помогает для OpenMP, но не для этого кода). Под теплым кешем, я думаю, вы имеете в виду чтение значений в кеш. В этом есть смысл. Что означает ленивое выделение виртуальной машины? - person Z boson; 07.05.2014
comment
В большинстве реализаций виртуальной памяти используется ленивый подход к выделению памяти, так что при выделении большого куска памяти сразу выделяются и подключаются только первые несколько страниц, а затем остальные обрабатываются с помощью ошибок страниц. Обычно это общий выигрыш в производительности (поскольку программы часто выделяют больше памяти, чем им на самом деле нужно), но для бенчмаркинга вам обычно нужно подключить всю эту память до того, как вы начнете отсчет времени. - person Paul R; 07.05.2014
comment
Во время прогрева также разогревается кэш предсказания ветвлений ЦП, поскольку код настолько плотный, что все ветвления, вероятно, легко помещаются в любой буфер истории ветвлений, который есть у ЦП. - person Peter Cordes; 06.12.2014

На моей машине (core i7-4900M) на основе обновленного кода Paul R с g++ 4.8.2 и 100 000 итераций вместо 1000, у меня есть следующие результаты:

g++ -Wall -mavx2 -O3 -std=c++11 test_avx.cpp && ./a.exe 
SSE took             508,029 us
AVX took           1,308,075 us
Normal took          297,017 us


g++ -Wall -mavx2 -O3 -std=c++11 -fno-tree-vectorize test_avx.cpp && ./a.exe 
SSE took             509,029 us
AVX took           1,307,075 us
Normal took        3,436,197 us

GCC проделывает потрясающую работу по оптимизации «нормального» кода. Тем не менее, низкая производительность кода «AVX» может быть объяснена приведенными ниже строками, для которых требуется полное 256-битное хранилище (ой!), За которым следует максимальный поиск по 8 целым числам.

_mm256_store_si256((__m256i *) result, vresult);
int mymax = result[0];
for (int k = 1; k < 8; k++) {
  if (result[k] > mymax) {
     mymax = result[k];
  }
}
return mymax;

Лучше продолжать использовать встроенные функции AVX максимум для 8. Я могу предложить следующие изменения.

v1      = _mm256_permute2x128_si256(vresult,vresult,1);  // from ABCD-EFGH to ????-ABCD
vresult = _mm256_max_epi32(vresult, v1);
v1      = _mm256_permute4x64_epi64(vresult,1);  // from ????-ABCD to ????-??AB
vresult = _mm256_max_epi32(vresult, v1);
v1      = _mm256_shuffle_epi32(vresult,1); // from ????-???AB to ????-???A
vresult = _mm256_max_epi32(vresult, v1);

// no _mm256_extract_epi32 => need extra step
__m128i vres128 = _mm256_extracti128_si256(vresult,0);
return _mm_extract_epi32(vres128,0);

Для честного сравнения я также обновил код SSE, после чего:

SSE took             483,028 us
AVX took             258,015 us
Normal took          307,017 us

Время AVX уменьшилось в 5 раз!

person user3636086    schedule 14.05.2014
comment
+1 Я попробую ваш код. Для массивов я также видел, что GCC значительно оптимизировал код для Normal. - person Alexandros; 14.05.2014
comment
Вы обновили код SSE. Можете ли вы также предоставить соответствующий код для этого? - person Alexandros; 14.05.2014
comment
v1 = _mm_shuffle_epi32 (vresult, 0xE); // 00_00_11_10 vresult = _mm_max_epi32(vresult, v1); v1 = _mm_shuffle_epi32 (vresult, 1); // 00_00_00_01 vresult = _mm_max_epi32(vresult, v1); вернуть _mm_extract_epi32 (vresult, 0); - person user3636086; 14.05.2014
comment
Спасибо, я начну с этого, чтобы увидеть, если я получу какие-либо улучшения. - person Alexandros; 14.05.2014

Выполнение развертывания цикла вручную может ускорить код SSE/AVX.

Оригинальная версия на моем i5-5300U:

Normal took 0.347 s
Normal Result: 2146591543
AVX took 0.409 s
AVX Result: 2146591543
SpeedUP AVX= 0.848411

После ручного развертывания цикла:

Normal took 0.375 s
Normal Result: 2146591543
AVX took 0.297 s
AVX Result: 2146591543
SpeedUP AVX= 1.26263
person meteorx    schedule 03.05.2017