Умножение с плавающей запятой: ПОТЕРЯ скорости с AVX против SSE?

У меня есть код, который делает то же самое, но версия AVX значительно МЕДЛЕННЕЕ, чем версия SSE. Кто-нибудь может это объяснить?

Что я уже сделал, так это попытался профилировать код с помощью VerySleepy, но это не дало мне никаких полезных результатов, оно просто подтвердило, что работает медленнее...

Я уже просмотрел команды в руководстве по SSE/AVX, и на моем процессоре (Haswell) им нужна та же задержка/пропускная способность, просто для горизонтального добавления нужны дополнительные команды для AVX...

** задержки и пропускная способность **

_mm_mul_ps            -> L 5, T 0.5
_mm256_mul_ps         -> L 5, T 0.5
_mm_hadd_ps           -> L 5, T 2
_mm256_hadd_ps        -> L 5, T ?
_mm256_extractf128_ps -> L 1, T 1

Кратко о том, что делает код: Final1 = SUM( m_Array1 * m_Array1 * m_Array3 * m_Array3 )

Final2 = SUM( m_Array2 * m_Array2 * m_Array3 * m_Array3 )

Final3 = СУММ( m_Array1 * m_Array2 * m_Array3 * m_Array3 )

инициализировать

float Final1 = 0.0f;
float Final2 = 0.0f;
float Final3 = 0.0f;

float* m_Array1 = (float*)_mm_malloc( 32 * sizeof( float ), 32 );
float* m_Array2 = (float*)_mm_malloc( 32 * sizeof( float ), 32 );
float* m_Array3 = (float*)_mm_malloc( 32 * sizeof( float ), 32 );

ССЭ:

for ( int k = 0; k < 32; k += 4 )
{

    __m128 g1 = _mm_load_ps( m_Array1 + k );
    __m128 g2 = _mm_load_ps( m_Array2 + k );
    __m128 g3 = _mm_load_ps( m_Array3 + k );

    __m128 g1g3 = _mm_mul_ps( g1, g3 );
    __m128 g2g3 = _mm_mul_ps( g2, g3 );

    __m128 a1 = _mm_mul_ps( g1g3, g1g3 );
    __m128 a2 = _mm_mul_ps( g2g3, g2g3 );
    __m128 a3 = _mm_mul_ps( g1g3, g2g3 );

    // horizontal add
    {
        a1 = _mm_hadd_ps( a1, a1 );
        a1 = _mm_hadd_ps( a1, a1 );
        Final1 += _mm_cvtss_f32( a1 );

        a2 = _mm_hadd_ps( a2, a2 );
        a2 = _mm_hadd_ps( a2, a2 );
        Final2 += _mm_cvtss_f32( a2 );

        a3 = _mm_hadd_ps( a3, a3 );
        a3 = _mm_hadd_ps( a3, a3 );
        Final3 += _mm_cvtss_f32( a3 );

    }

}

AVX:

for ( int k = 0; k < 32; k += 8 )
{
    __m256 g1 = _mm256_load_ps( m_Array1 + k );
    __m256 g2 = _mm256_load_ps( m_Array2 + k );
    __m256 g3 = _mm256_load_ps( m_Array3 + k );

    __m256 g1g3 = _mm256_mul_ps( g1, g3 );
    __m256 g2g3 = _mm256_mul_ps( g2, g3 );

    __m256 a1 = _mm256_mul_ps( g1g3, g1g3 );
    __m256 a2 = _mm256_mul_ps( g2g3, g2g3 );
    __m256 a3 = _mm256_mul_ps( g1g3, g2g3 );

    // horizontal add1
    {
        __m256 t1 = _mm256_hadd_ps( a1, a1 );
        __m256 t2 = _mm256_hadd_ps( t1, t1 );
        __m128 t3 = _mm256_extractf128_ps( t2, 1 );
        __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
        Final1 += _mm_cvtss_f32( t4 );
    }
    // horizontal add2
    {
        __m256 t1 = _mm256_hadd_ps( a2, a2 );
        __m256 t2 = _mm256_hadd_ps( t1, t1 );
        __m128 t3 = _mm256_extractf128_ps( t2, 1 );
        __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
        Final2 += _mm_cvtss_f32( t4 );
    }
    // horizontal add3
    {
        __m256 t1 = _mm256_hadd_ps( a3, a3 );
        __m256 t2 = _mm256_hadd_ps( t1, t1 );
        __m128 t3 = _mm256_extractf128_ps( t2, 1 );
        __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
        Final3 += _mm_cvtss_f32( t4 );
    }

}

person S.H    schedule 13.03.2015    source источник
comment
Вам нужен намного больший цикл для надежного сравнения этих двух фрагментов кода. Кроме того, горизонтальные добавления и извлечение конечного скалярного значения должны быть вне цикла в обоих случаях.   -  person Paul R    schedule 13.03.2015
comment
Это просто минимальный пример. Замените 32 на несколько целых чисел, умноженных на восемь, проблема останется прежней.   -  person S.H    schedule 13.03.2015
comment
extractf128ps, как и все операции с перекрестными срезами, довольно медленные. На самом деле хадд тоже довольно медленный, но это как в версии AVX, так и в версии SSE.   -  person harold    schedule 13.03.2015
comment
AVX имеет гораздо более высокую пропускную способность, чем SSE, но имеет более высокую задержку. Постоянные рабочие нагрузки скрывают задержку, а короткие нагрузки выявляют ее.   -  person dtech    schedule 13.03.2015
comment
@PaulR Скалярное извлечение выполняется в обоих случаях. даже если я уберу скалярное извлечение, это не будет иметь значения для AVX и SSE.   -  person S.H    schedule 13.03.2015
comment
@ddriver: я напишу пропускную способность/задержки в тексте выше, они в основном идентичны   -  person S.H    schedule 13.03.2015
comment
@SH: конечно, но ваш код очень неэффективен в обоих случаях - если вы переместите горизонтальные добавления и скалярное извлечение из цикла и просто используете обычные (вертикальные) добавления внутри цикла, вы получите более эффективный код в обоих случаях. и относительная производительность также может измениться из-за различного набора инструкций.   -  person Paul R    schedule 13.03.2015
comment
Почему вы вообще делаете это именно так? Вы можете легко сделать это почти полностью по вертикали, только суммируя по горизонтали один раз после цикла   -  person harold    schedule 13.03.2015
comment
@PaulR: проблема состоит в том, чтобы поэлементно умножить массивы (как описано выше) и суммировать все продукты. Я не понимаю, как можно вывести суммирование из цикла без сохранения умножений в каком-либо другом массиве... Вы уверены, что ваши предложения соответствуют моему разделу, что делает код?   -  person S.H    schedule 13.03.2015
comment
Пытаюсь вернуться к теме: Операции практически идентичны, откуда берется задержка??   -  person S.H    schedule 13.03.2015
comment
VEXTRACTF128 на самом деле имеет задержку 3, а не 1   -  person harold    schedule 13.03.2015
comment
@SH: поскольку вы просто суммируете все продукты, вы можете просто выполнить вертикальное сложение внутри цикла, получив четыре частичные суммы, а затем выполнить одно горизонтальное сложение после цикла, чтобы объединить эти четыре частичные суммы. Это будет намного эффективнее и позволит избежать вышеупомянутых инструкций с высокой задержкой.   -  person Paul R    schedule 13.03.2015
comment
FWIW, запускающий ваш код здесь на Haswell, компиляция с clang -O3 -mavx2 обеспечивает более высокую производительность с AVX по сравнению с SSE, как и ожидалось. Я подозреваю, что виноваты либо ваш компилятор, либо методы бенчмаркинга.   -  person Paul R    schedule 13.03.2015
comment
@PaulR, какое ускорение вы заметили?   -  person S.H    schedule 13.03.2015
comment
@SH: 1,4x для исходных версий, 1,8x для оптимизированных версий с горизонтальными добавлениями, удаленными из цикла.   -  person Paul R    schedule 13.03.2015
comment
Делаете ли вы какие-либо стандартные библиотечные вызовы перед кодом AVX? Например из math.h? Попробуйте добавить _mm256_zeroupper() перед кодом AVX.   -  person Z boson    schedule 16.03.2015


Ответы (1)


Я взял ваш код и поместил его в тестовую обвязку, скомпилировал его clang -O3 и замерил время. Я также реализовал более быстрые версии двух подпрограмм, в которых горизонтальное добавление было удалено из цикла:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <sys/time.h>   // gettimeofday
#include <immintrin.h>

static void sse(const float *m_Array1, const float *m_Array2, const float *m_Array3, size_t n, float *Final1, float *Final2, float *Final3)
{
    *Final1 = *Final2 = *Final3 = 0.0f;
    for (int k = 0; k < n; k += 4)
    {
        __m128 g1 = _mm_load_ps( m_Array1 + k );
        __m128 g2 = _mm_load_ps( m_Array2 + k );
        __m128 g3 = _mm_load_ps( m_Array3 + k );

        __m128 g1g3 = _mm_mul_ps( g1, g3 );
        __m128 g2g3 = _mm_mul_ps( g2, g3 );

        __m128 a1 = _mm_mul_ps( g1g3, g1g3 );
        __m128 a2 = _mm_mul_ps( g2g3, g2g3 );
        __m128 a3 = _mm_mul_ps( g1g3, g2g3 );

        // horizontal add
        {
            a1 = _mm_hadd_ps( a1, a1 );
            a1 = _mm_hadd_ps( a1, a1 );
            *Final1 += _mm_cvtss_f32( a1 );

            a2 = _mm_hadd_ps( a2, a2 );
            a2 = _mm_hadd_ps( a2, a2 );
            *Final2 += _mm_cvtss_f32( a2 );

            a3 = _mm_hadd_ps( a3, a3 );
            a3 = _mm_hadd_ps( a3, a3 );
            *Final3 += _mm_cvtss_f32( a3 );
        }
    }
}

static void sse_fast(const float *m_Array1, const float *m_Array2, const float *m_Array3, size_t n, float *Final1, float *Final2, float *Final3)
{
    *Final1 = *Final2 = *Final3 = 0.0f;
    __m128 a1 = _mm_setzero_ps();
    __m128 a2 = _mm_setzero_ps();
    __m128 a3 = _mm_setzero_ps();
    for (int k = 0; k < n; k += 4)
    {
        __m128 g1 = _mm_load_ps( m_Array1 + k );
        __m128 g2 = _mm_load_ps( m_Array2 + k );
        __m128 g3 = _mm_load_ps( m_Array3 + k );

        __m128 g1g3 = _mm_mul_ps( g1, g3 );
        __m128 g2g3 = _mm_mul_ps( g2, g3 );

        a1 = _mm_add_ps(a1, _mm_mul_ps( g1g3, g1g3 ));
        a2 = _mm_add_ps(a2, _mm_mul_ps( g2g3, g2g3 ));
        a3 = _mm_add_ps(a3, _mm_mul_ps( g1g3, g2g3 ));
    }
    // horizontal add
    a1 = _mm_hadd_ps( a1, a1 );
    a1 = _mm_hadd_ps( a1, a1 );
    *Final1 += _mm_cvtss_f32( a1 );

    a2 = _mm_hadd_ps( a2, a2 );
    a2 = _mm_hadd_ps( a2, a2 );
    *Final2 += _mm_cvtss_f32( a2 );

    a3 = _mm_hadd_ps( a3, a3 );
    a3 = _mm_hadd_ps( a3, a3 );
    *Final3 += _mm_cvtss_f32( a3 );
}

static void avx(const float *m_Array1, const float *m_Array2, const float *m_Array3, size_t n, float *Final1, float *Final2, float *Final3)
{
    *Final1 = *Final2 = *Final3 = 0.0f;
    for (int k = 0; k < n; k += 8 )
    {
        __m256 g1 = _mm256_load_ps( m_Array1 + k );
        __m256 g2 = _mm256_load_ps( m_Array2 + k );
        __m256 g3 = _mm256_load_ps( m_Array3 + k );

        __m256 g1g3 = _mm256_mul_ps( g1, g3 );
        __m256 g2g3 = _mm256_mul_ps( g2, g3 );

        __m256 a1 = _mm256_mul_ps( g1g3, g1g3 );
        __m256 a2 = _mm256_mul_ps( g2g3, g2g3 );
        __m256 a3 = _mm256_mul_ps( g1g3, g2g3 );

        // horizontal add1
        {
            __m256 t1 = _mm256_hadd_ps( a1, a1 );
            __m256 t2 = _mm256_hadd_ps( t1, t1 );
            __m128 t3 = _mm256_extractf128_ps( t2, 1 );
            __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
            *Final1 += _mm_cvtss_f32( t4 );
        }
        // horizontal add2
        {
            __m256 t1 = _mm256_hadd_ps( a2, a2 );
            __m256 t2 = _mm256_hadd_ps( t1, t1 );
            __m128 t3 = _mm256_extractf128_ps( t2, 1 );
            __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
            *Final2 += _mm_cvtss_f32( t4 );
        }
        // horizontal add3
        {
            __m256 t1 = _mm256_hadd_ps( a3, a3 );
            __m256 t2 = _mm256_hadd_ps( t1, t1 );
            __m128 t3 = _mm256_extractf128_ps( t2, 1 );
            __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
            *Final3 += _mm_cvtss_f32( t4 );
        }
    }
}

static void avx_fast(const float *m_Array1, const float *m_Array2, const float *m_Array3, size_t n, float *Final1, float *Final2, float *Final3)
{
    *Final1 = *Final2 = *Final3 = 0.0f;
    __m256 a1 = _mm256_setzero_ps();
    __m256 a2 = _mm256_setzero_ps();
    __m256 a3 = _mm256_setzero_ps();
    for (int k = 0; k < n; k += 8 )
    {
        __m256 g1 = _mm256_load_ps( m_Array1 + k );
        __m256 g2 = _mm256_load_ps( m_Array2 + k );
        __m256 g3 = _mm256_load_ps( m_Array3 + k );

        __m256 g1g3 = _mm256_mul_ps( g1, g3 );
        __m256 g2g3 = _mm256_mul_ps( g2, g3 );

        a1 = _mm256_add_ps(a1, _mm256_mul_ps( g1g3, g1g3 ));
        a2 = _mm256_add_ps(a2, _mm256_mul_ps( g2g3, g2g3 ));
        a3 = _mm256_add_ps(a3, _mm256_mul_ps( g1g3, g2g3 ));
    }

    // horizontal add1

    {
        __m256 t1 = _mm256_hadd_ps( a1, a1 );
        __m256 t2 = _mm256_hadd_ps( t1, t1 );
        __m128 t3 = _mm256_extractf128_ps( t2, 1 );
        __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
        *Final1 += _mm_cvtss_f32( t4 );
    }

    // horizontal add2

    {
        __m256 t1 = _mm256_hadd_ps( a2, a2 );
        __m256 t2 = _mm256_hadd_ps( t1, t1 );
        __m128 t3 = _mm256_extractf128_ps( t2, 1 );
        __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
        *Final2 += _mm_cvtss_f32( t4 );
    }

    // horizontal add3

    {
        __m256 t1 = _mm256_hadd_ps( a3, a3 );
        __m256 t2 = _mm256_hadd_ps( t1, t1 );
        __m128 t3 = _mm256_extractf128_ps( t2, 1 );
        __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
        *Final3 += _mm_cvtss_f32( t4 );
    }

}

int main(int argc, char *argv[])
{
    size_t n = 4096;

    if (argc > 1) n = atoi(argv[1]);

    float *in_1 = valloc(n * sizeof(in_1[0]));
    float *in_2 = valloc(n * sizeof(in_2[0]));
    float *in_3 = valloc(n * sizeof(in_3[0]));
    float out_1, out_2, out_3;

    struct timeval t0, t1;
    double t_ms;

    for (int i = 0; i < n; ++i)
    {
        in_1[i] = (float)rand() / (float)(RAND_MAX / 2);
        in_2[i] = (float)rand() / (float)(RAND_MAX / 2);
        in_3[i] = (float)rand() / (float)(RAND_MAX / 2);
    }

    sse(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    printf("sse     : %g, %g, %g\n", out_1, out_2, out_3);
    sse_fast(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    printf("sse_fast: %g, %g, %g\n", out_1, out_2, out_3);
    avx(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    printf("avx     : %g, %g, %g\n", out_1, out_2, out_3);
    avx_fast(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    printf("avx_fast: %g, %g, %g\n", out_1, out_2, out_3);

    gettimeofday(&t0, NULL);
    for (int k = 0; k < 100; ++k) sse(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    gettimeofday(&t1, NULL);
    t_ms = ((double)(t1.tv_sec - t0.tv_sec) + (double)(t1.tv_usec - t0.tv_usec) * 1.0e-6) * 1.0e3;
    printf("sse     : %g, %g, %g, %g ms\n", out_1, out_2, out_3, t_ms);

    gettimeofday(&t0, NULL);
    for (int k = 0; k < 100; ++k) sse_fast(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    gettimeofday(&t1, NULL);
    t_ms = ((double)(t1.tv_sec - t0.tv_sec) + (double)(t1.tv_usec - t0.tv_usec) * 1.0e-6) * 1.0e3;
    printf("sse_fast: %g, %g, %g, %g ms\n", out_1, out_2, out_3, t_ms);

    gettimeofday(&t0, NULL);
    for (int k = 0; k < 100; ++k) avx(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    gettimeofday(&t1, NULL);
    t_ms = ((double)(t1.tv_sec - t0.tv_sec) + (double)(t1.tv_usec - t0.tv_usec) * 1.0e-6) * 1.0e3;
    printf("avx     : %g, %g, %g, %g ms\n", out_1, out_2, out_3, t_ms);

    gettimeofday(&t0, NULL);
    for (int k = 0; k < 100; ++k) avx_fast(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    gettimeofday(&t1, NULL);
    t_ms = ((double)(t1.tv_sec - t0.tv_sec) + (double)(t1.tv_usec - t0.tv_usec) * 1.0e-6) * 1.0e3;
    printf("avx_fast: %g, %g, %g, %g ms\n", out_1, out_2, out_3, t_ms);

    return 0;
}

Результаты на моем Haswell 2,6 ГГц (MacBook Pro):

sse     : 0.439 ms
sse_fast: 0.153 ms
avx     : 0.309 ms
avx_fast: 0.085 ms

Таким образом, версия AVX действительно кажется быстрее, чем версия SSE, как для исходных реализаций, так и для оптимизированных реализаций. Оптимизированные реализации значительно быстрее исходных версий, однако с еще большим отрывом.

Я могу только предположить, что либо ваш компилятор не генерирует очень хороший код для AVX (или, может быть, вы забыли включить оптимизацию компилятора?), либо, возможно, есть что-то подозрительное в вашем методе бенчмаркинга.

person Paul R    schedule 13.03.2015
comment
Это действительно странно, я проверю и выложу свои результаты - если они будут - person S.H; 13.03.2015
comment
С вашим кодом я понял, что вы имели в виду, убрав хадд из цикла. Я думал, вы хотите каким-то образом сначала умножить (без последнего сложения) и волшебным образом суммировать их. СПАСИБО! - person S.H; 13.03.2015
comment
Да, как правило, в SIMD-коде следует избегать горизонтальных операций, а когда их нельзя избежать, в идеале они должны находиться вне циклов, критичных к производительности. - person Paul R; 13.03.2015
comment
Я компилирую с помощью VS2012, -O3 и /arch:AVX. Может ли это быть проблемой? - person S.H; 13.03.2015
comment
Я бы подумал, что либо /O2, либо /Ox подойдут для Visual Studio, но я мало работаю с Windows и не уверен, насколько хороша Visual Studio, когда дело доходит до генерации кода AVX. gcc, clang и ICC от Intel генерируют довольно хороший код AVX, поэтому вы можете подумать о том, чтобы попробовать другой компилятор. Вы также можете пересмотреть свои методы бенчмаркинга, так как есть много ловушек для неосторожных. Если вы сможете скомпилировать мой тестовый комплект с помощью Visual Studio, было бы интересно посмотреть, какие цифры вы получите для времени. - person Paul R; 13.03.2015
comment
Вот мои ускорения: sse_fast/sse: 2.8 // avx_fast/avx: 3.8 // avx/sse: 1.3 // avx_fast/sse_fast: 1.7. Это почти то, что @PaulR: сообщил. Горизонтальное добавление кажется очень дорогим - person S.H; 18.03.2015
comment
Хорошо, спасибо, значит, это согласуется с моими цифрами? - person Paul R; 18.03.2015
comment
Да! Спасибо за вашу помощь. Мне все еще нужно выяснить, откуда происходит потеря скорости в моем исходном коде. - person S.H; 18.03.2015
comment
изменение /ach:SSE на /arch:AVX дает огромную разницу (скорость SSE такая же, AVX улучшается более чем в 10 раз) - person S.H; 18.03.2015
comment
Обратите внимание, что в моем коде все кеши предварительно прогреваются, и я использую достаточно большое количество повторов. Также обратите внимание, что я предпринимаю шаги, чтобы компилятор не был слишком умным и оптимизировал вызовы циклов/функций и т. д. - person Paul R; 18.03.2015
comment
все кеши предварительно прогреваются: ведь массивы инициализируются непосредственно перед использованием, верно? - person S.H; 18.03.2015
comment
Я не уверен, что делают переключатели /arch:, но, очевидно, они имеют большое значение для вас! - person Paul R; 18.03.2015
comment
Что я обычно делаю, так это сначала вызываю каждую интересующую функцию один раз (и обычно использую вывод для проверки), прежде чем я начну синхронизацию - таким образом и кэши данных, и кэш инструкций разогреваются, и весь соответствующий код выгружается, поэтому нет ошибки страниц или промахи кеша, которых можно избежать, в циклах синхронизации. - person Paul R; 18.03.2015
comment
Фактор 10 почти наверняка получен из-за смешивания VEX с инструкциями SSE, не закодированными в VEX, без использования VZEROUPPER между ними. Использование /arch:AVX, вероятно, означает, что все инструкции simd во всей программе выдаются в кодировке VEX, что позволяет избежать проблемы. /arch:SSE Я предполагаю результаты в VEX-кодировании только в случае необходимости. (например, _mm256 встроенные функции.) Кроме того, если вы попытаетесь сделать версию FMA, вам может потребоваться развернуться на 2 и запустить 2-й набор аккумуляторов. FMA увеличит петлевую цепочку отложений до 5 циклов (для FMA) с 3 (addps). - person Peter Cordes; 10.07.2015
comment
@PeterCordes: это имеет смысл - я всегда предполагал, что, если вы не пишете ассемблер, вы не можете столкнуться с проблемами с инструкциями VEX v non-VEX - либо ваш компилятор генерирует инструкции AVX + VEX SSE, либо просто генерирует простые ( не VEX) SSE. Я думаю, что это относится к gcc/clang/ICC, по крайней мере, по моему опыту, но похоже, что это не всегда верно для MSVC (в зависимости от настройки /ARCH)? - person Paul R; 10.07.2015