Добавление SSE медленнее, чем оператор + - PullRequest
0 голосов
/ 15 ноября 2018

Я пытался проверить, насколько быстро работает SSE, но что-то не так. Я создал два массива для входных данных и один массив для вывода в стеке и выполнил добавления к ним обоими способами. Это медленнее, чем обычный оператор +. Что я тут не так делаю:

#include <iostream>
#include <nmmintrin.h>
#include <chrono>

using namespace std;

#define USE_SSE

typedef chrono::steady_clock::time_point TimeStamp;
typedef chrono::steady_clock Clock;
int main()
{
    const int MAX = 100000 * 4;
    float in1[MAX];
    float in2[MAX];
    float out[MAX];

    memset(out,0,sizeof(float) * MAX);

    for(int i = 0 ; i < MAX ; ++i)
    {
        in1[i] = 1.0f;
        in2[i] = 1.0f;
    }

    TimeStamp start,end;
    start = Clock::now();

    for(int i = 0 ; i < MAX ; i+=4)
    {
#ifdef USE_SSE

        __m128 a = _mm_load_ps(&in1[i]);
        __m128 b = _mm_load_ps(&in2[i]);
        __m128 result = _mm_add_ps(a,b);
        _mm_store_ps(&out[i],result);
#else
        out[0] = in1[0] + in2[0];
        out[1] = in1[1] + in2[1];
        out[2] = in1[2] + in2[2];
        out[3] = in1[3] + in2[3];
#endif
    }


    end = Clock::now();
    double dt = chrono::duration_cast<chrono::nanoseconds>(end-start).count();
    cout<<dt<<endl;

    return 0;
}

здесь проблема выравнивания памяти?

Ответы [ 3 ]

0 голосов
/ 15 ноября 2018

Вот несколько улучшенная версия вашего теста с исправлениями ошибок, улучшениями синхронизации и отключением векторизации компилятора для скалярного кода (по крайней мере для gcc и clang):

#include <iostream>
#include <xmmintrin.h>
#include <chrono>

using namespace std;

typedef chrono::steady_clock::time_point TimeStamp;
typedef chrono::steady_clock Clock;

typedef void (*add_func)(const float *in1, const float *in2, volatile float *out, const size_t n);

#ifndef __clang__
__attribute__((optimize("no-tree-vectorize")))
#endif
static void add_scalar(const float *in1, const float *in2, volatile float *out, const size_t n)
{
#ifdef __clang__
    #pragma clang loop vectorize(disable)
#endif
    for (size_t i = 0 ; i < n ; i += 4)
    {
        out[i + 0] = in1[i + 0] + in2[i + 0];
        out[i + 1] = in1[i + 1] + in2[i + 1];
        out[i + 2] = in1[i + 2] + in2[i + 2];
        out[i + 3] = in1[i + 3] + in2[i + 3];
    }
}

static void add_SIMD(const float *in1, const float *in2, volatile float *out, const size_t n)
{
    for (size_t i = 0 ; i < n ; i += 4)
    {
        __m128 a = _mm_loadu_ps(&in1[i]);
        __m128 b = _mm_loadu_ps(&in2[i]);
        __m128 result = _mm_add_ps(a, b);
        _mm_storeu_ps((float *)&out[i], result);
    }
}

static double time_func(const float *in1, const float *in2, volatile float *out, const size_t n, add_func f)
{
    const size_t kLoops = 10000;

    TimeStamp start,end;
    start = Clock::now();

    for (size_t k = 0; k < kLoops; ++k)
    {
        f(in1, in2, out, n);
    }

    end = Clock::now();

    return chrono::duration_cast<chrono::nanoseconds>(end - start).count() / ((double)kLoops * (double)n);
}

int main()
{
    const size_t n = 100000 * 4;
    float *in1 = new float[n];
    float *in2 = new float[n];
    volatile float *out = new float[n]();

    for (size_t i = 0; i < n; ++i)
    {
        in1[i] = (float)i;
        in2[i] = 1.0f;
    }

    double t_scalar = time_func(in1, in2, out, n, add_scalar);
    double t_SIMD = time_func(in1, in2, out, n, add_SIMD);

    cout << "t_scalar = " << t_scalar << " ns / point" << endl;
    cout << "t_SIMD   = " << t_SIMD << " ns / point" << endl;
    cout << "speed-up = " << t_scalar / t_SIMD << "x" << endl;

    delete [] in1;
    delete [] in2;
    delete [] out;

    return 0;
}

Я получаю улучшение от 1.5x до 1.6x для SSE на процессоре Haswell. Это явно меньше, чем 4-кратное теоретическое улучшение, которое могло бы быть возможным, но тест, скорее всего, ограничен пропускной способностью из-за того, что вы выполняете только 1 арифметическую операцию за итерацию, но 2 загрузки и 1 хранение:

t_scalar = 0.529723 ns / point
t_SIMD   = 0.329758 ns / point
speed-up = 1.6064x
0 голосов
/ 23 ноября 2018

Иногда пытаться «оптимизировать» код C ++, добавляя циклы для проверки времени, в общем, довольно глупо, и это один из таких случаев: (

Ваш код В буквальном смысле сводится только к этому:

int main()
{
    TimeStamp start = Clock::now();
    TimeStamp end = Clock::now();

    double dt = chrono::duration_cast<chrono::nanoseconds>(end-start).count();
    cout<<dt<<endl;

    return 0;
}

Компилятор не глуп, и поэтому он решил удалить ваш внутренний цикл (поскольку вывод не используется, и поэтому цикл является избыточным).

Даже если компилятор решил сохранить цикл, вы даете 3 инструкции по памяти для каждого дополнения. Если ваша оперативная память составляет 1600 МГц, а ваш процессор 3200 МГц, то ваши тесты просто доказывают, что у вас ограниченная пропускная способность памяти. Подобные профилирующие циклы бесполезны, вам всегда будет лучше тестировать реальную ситуацию в профилировщике ....

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

https://godbolt.org/z/5SJQHb

F0 : Просто простая, скучная С-петля.

for(int i = 0 ; i < MAX ; i++)
{
    out[i] = in1[i] + in2[i];
}

Компилятор выводит этот внутренний цикл:

vmovups ymm0,YMMWORD PTR [rsi+r8*4]
vmovups ymm1,YMMWORD PTR [rsi+r8*4+0x20]
vmovups ymm2,YMMWORD PTR [rsi+r8*4+0x40]
vmovups ymm3,YMMWORD PTR [rsi+r8*4+0x60]
vaddps ymm0,ymm0,YMMWORD PTR [rdx+r8*4]
vaddps ymm1,ymm1,YMMWORD PTR [rdx+r8*4+0x20]
vaddps ymm2,ymm2,YMMWORD PTR [rdx+r8*4+0x40]
vaddps ymm3,ymm3,YMMWORD PTR [rdx+r8*4+0x60]
vmovups YMMWORD PTR [rdi+r8*4],ymm0
vmovups YMMWORD PTR [rdi+r8*4+0x20],ymm1
vmovups YMMWORD PTR [rdi+r8*4+0x40],ymm2
vmovups YMMWORD PTR [rdi+r8*4+0x60],ymm3

Развернуто, имеет дело с 32xfloats за итерацию (в AVX2) [+ дополнительный код для обработки до 31 элемента в конце итерации]

F1 : ваш «оптимизированный» цикл SSE выше. (Очевидно, этот код не обрабатывает до 3 элементов в конце цикла)

for(int i = 0 ; i < MAX ; i+=4)
{
    __m128 a = _mm_load_ps(&in1[i]);
    __m128 b = _mm_load_ps(&in2[i]);
    __m128 result = _mm_add_ps(a,b);
    _mm_store_ps(&out[i],result);
}

Это выводит:

vmovaps xmm0,XMMWORD PTR [rsi+rcx*4]
vaddps xmm0,xmm0,XMMWORD PTR [rdx+rcx*4]
vmovaps XMMWORD PTR [rdi+rcx*4],xmm0
vmovaps xmm0,XMMWORD PTR [rsi+rcx*4+0x10]
vaddps xmm0,xmm0,XMMWORD PTR [rdx+rcx*4+0x10]
vmovaps XMMWORD PTR [rdi+rcx*4+0x10],xmm0
vmovaps xmm0,XMMWORD PTR [rsi+rcx*4+0x20]
vaddps xmm0,xmm0,XMMWORD PTR [rdx+rcx*4+0x20]
vmovaps XMMWORD PTR [rdi+rcx*4+0x20],xmm0
vmovaps xmm0,XMMWORD PTR [rsi+rcx*4+0x30]
vaddps xmm0,xmm0,XMMWORD PTR [rdx+rcx*4+0x30]
vmovaps XMMWORD PTR [rdi+rcx*4+0x30],xmm0

Таким образом, компилятор развернул цикл, но он вернулся к SSE (как было запрошено), поэтому теперь он вдвое меньше производительности исходного цикла (не совсем верно - пропускная способность памяти будет ограничивающим фактором) .

F2 : ваш развернутый цикл C ++ (с исправленными индексами, но все еще не может обработать последние 3 элемента)

for(int i = 0 ; i < MAX ; i += 4)
{
    out[i + 0] = in1[i + 0] + in2[i + 0];
    out[i + 1] = in1[i + 1] + in2[i + 1];
    out[i + 2] = in1[i + 2] + in2[i + 2];
    out[i + 3] = in1[i + 3] + in2[i + 3];
}

И вывод:

vmovss xmm0,DWORD PTR [rsi+rax*4]
vaddss xmm0,xmm0,DWORD PTR [rdx+rax*4]
vmovss DWORD PTR [rdi+rax*4],xmm0
vmovss xmm0,DWORD PTR [rsi+rax*4+0x4]
vaddss xmm0,xmm0,DWORD PTR [rdx+rax*4+0x4]
vmovss DWORD PTR [rdi+rax*4+0x4],xmm0
vmovss xmm0,DWORD PTR [rsi+rax*4+0x8]
vaddss xmm0,xmm0,DWORD PTR [rdx+rax*4+0x8]
vmovss DWORD PTR [rdi+rax*4+0x8],xmm0
vmovss xmm0,DWORD PTR [rsi+rax*4+0xc]
vaddss xmm0,xmm0,DWORD PTR [rdx+rax*4+0xc]
vmovss DWORD PTR [rdi+rax*4+0xc],xmm0

Ну, это полностью не удалось векторизовать! Это просто обработка 1 сложения за раз. Ну, это обычно сводится к псевдониму указателя, поэтому я изменю прототип функции из этого:

void func(float* out, const float* in1, const float* in2, int MAX);

к этому: ( F4 )

void func(
    float* __restrict out, 
    const float* __restrict in1, 
    const float* __restrict in2, 
    int MAX);

и теперь компилятор выведет что-то векторизованное:

vmovups xmm0,XMMWORD PTR [rsi+rcx*4]
vaddps xmm0,xmm0,XMMWORD PTR [rdx+rcx*4]
vmovups xmm1,XMMWORD PTR [rsi+rcx*4+0x10]
vaddps xmm1,xmm1,XMMWORD PTR [rdx+rcx*4+0x10]
vmovups XMMWORD PTR [rdi+rcx*4],xmm0
vmovups xmm0,XMMWORD PTR [rsi+rcx*4+0x20]
vaddps xmm0,xmm0,XMMWORD PTR [rdx+rcx*4+0x20]
vmovups XMMWORD PTR [rdi+rcx*4+0x10],xmm1
vmovups xmm1,XMMWORD PTR [rsi+rcx*4+0x30]
vaddps xmm1,xmm1,XMMWORD PTR [rdx+rcx*4+0x30]
vmovups XMMWORD PTR [rdi+rcx*4+0x20],xmm0
vmovups XMMWORD PTR [rdi+rcx*4+0x30],xmm1

ОДНАКО этот код по-прежнему вдвое меньше производительности первой версии ....

0 голосов
/ 15 ноября 2018

В вашем коде есть ошибка, часть, не относящаяся к SSE, должна выглядеть следующим образом:

    out[i+0] = in1[i+0] + in2[i+0];
    out[i+1] = in1[i+1] + in2[i+1];
    out[i+2] = in1[i+2] + in2[i+2];
    out[i+3] = in1[i+3] + in2[i+3];

Вам следует подумать о том, чтобы сделать эталонный тест более длинным, поскольку измерение коротких периодов времени ненадежно.И, возможно, вам нужно будет что-то сделать, чтобы компилятор не оптимизировал ваш код (например, пометив out volatile).Всегда проверяйте код сборки, чтобы быть уверенным, что вы измеряете.

...