Моя оптимизация SSE / AVX для поэлементного sqrt не стимулирует, почему - PullRequest
1 голос
/ 27 апреля 2020

Я новичок в оптимизации SIMD, пытаюсь вычислить значение sqrt каждого элемента для массива с плавающей запятой 1 *.

Система: Windows 10 Компилятор: Visual Studio 2017 Процессор: Intel Core i5-8500

Следующий код компилируется и запускается в режиме Release, однако нормальная (наивная) реализация почти такой же скорости, что и оптимизированная версия SSE и AVX . Не знаю почему. Моя реализация или метод, который я использую, неверны?

#include <iostream>
#include <string>
#include <sstream>
#include <chrono>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#include "nmmintrin.h" // SSE4.2
#include "immintrin.h"  // for AVX

using namespace std;

template<typename T, typename P>
std::string toString(std::chrono::duration<T,P> dt)
{
    std::ostringstream str;
    using namespace std::chrono;
    str << duration_cast<microseconds>(dt).count()*1e-3 << " ms";
    return str.str();
}

template<typename _Tp> static inline _Tp* alignPtr(_Tp* ptr, int n = (int)sizeof(_Tp))
{
    return (_Tp*)(((size_t)ptr + n - 1) & -n);
}

typedef unsigned char uchar;
#define MALLOC_ALIGN 16

void* fast_malloc(size_t size);
void fast_free(void* ptr);

void* fast_malloc(size_t size) {
    uchar* udata = (uchar*)malloc(size + sizeof(void*) + MALLOC_ALIGN);
    if (!udata) {
        fprintf(stderr, "error: failed to allocate memory\n");
        assert(0);
    }
    uchar** adata = alignPtr((uchar**)udata + 1, MALLOC_ALIGN);
    adata[-1] = udata;
    return adata;
}

void fast_free(void* ptr)
{
    if (ptr)
    {
        uchar* udata = ((uchar**)ptr)[-1];
        assert(udata < (uchar*)ptr &&
            ((uchar*)ptr - udata) <= (ptrdiff_t)(sizeof(void*) + MALLOC_ALIGN));
        free(udata);
    }
}


//element-wise squar root of an array, normal impl
void ew_sqrt_normal(float* a, int N) {
    for (int i = 0; i < N; i++) {
        a[i] = sqrt(a[i]);
    }
}


//element-wise squar root of an array, sse impl
void ew_sqrt_sse(float* a, int N) {
    int iters = N / 4;
    __m128* ptr = (__m128*)a;
    for (int i = 0; i < iters; i++, ptr++, a += 4) {
        _mm_store_ps(a, _mm_sqrt_ps(*ptr));
    }
}


//element-wise squar root of an array, sse impl
void ew_sqrt_avx(float* a, int N) {
    int iters = N / 8;
    __m256* ptr = (__m256*)a;
    for (int i = 0; i < iters; i++, ptr++, a += 8) {
        _mm256_store_ps(a, _mm256_sqrt_ps(*ptr));
    }
}


int main(){
    volatile int num_elem = 1024 * 1024 * 3;
    size_t size = num_elem * sizeof(float);
    float* data = (float*)fast_malloc(size);
    for (int i = 0; i < num_elem; i++) {
        data[i] = i + 1;
    }


    for (int i = 0; i < 10; i++) {
        //float* data1 = (float*)fast_malloc(size);
        float* data1 = (float*)fast_malloc(size);
        memcpy(data1, data, size);
        const auto t1_start = chrono::steady_clock::now();
        ew_sqrt_normal(data1, num_elem);
        const auto t1_end = chrono::steady_clock::now();


        float* data2 = (float*)fast_malloc(size);
        memcpy(data2, data, size);
        const auto t2_start = chrono::steady_clock::now();
        ew_sqrt_sse(data2, num_elem);
        const auto t2_end = chrono::steady_clock::now();


        float* data3 = (float*)fast_malloc(size);
        memcpy(data3, data, size);
        const auto t3_start = chrono::steady_clock::now();
        ew_sqrt_avx(data3, num_elem);
        const auto t3_end = chrono::steady_clock::now();


        cout << i + 1 << "-th perf, got: " << endl;
        cout << "normal: " << toString(t1_end - t1_start) << endl;
        cout << "sse:    " << toString(t2_end - t2_start) << endl;
        cout << "avx:    " << toString(t3_end - t3_start) << endl;
        cout << endl;

        fast_free(data1);
        fast_free(data2);
        fast_free(data3);
    }

    fast_free(data);

    return 0;
}

Результат выполнения:

1-th perf, got:
normal: 1.101 ms
sse:    0.997 ms
avx:    1.034 ms

2-th perf, got:
normal: 1.098 ms
sse:    0.868 ms
avx:    0.823 ms

3-th perf, got:
normal: 1.018 ms
sse:    0.927 ms
avx:    0.878 ms

4-th perf, got:
normal: 0.802 ms
sse:    1.113 ms
avx:    0.759 ms

5-th perf, got:
normal: 0.886 ms
sse:    0.879 ms
avx:    0.757 ms

6-th perf, got:
normal: 0.815 ms
sse:    0.918 ms
avx:    0.922 ms

7-th perf, got:
normal: 0.852 ms
sse:    0.786 ms
avx:    0.796 ms

8-th perf, got:
normal: 0.809 ms
sse:    0.874 ms
avx:    0.763 ms

9-th perf, got:
normal: 0.884 ms
sse:    1.442 ms
avx:    0.877 ms

10-th perf, got:
normal: 0.864 ms
sse:    0.802 ms
avx:    0.999 ms

update1

Просто забыл упомянуть уровень оптимизации, который я использую. Это сгенерированный cmake проект Visual Studio с использованием оптимизации O2.

Также протестировано: переход на оптимизацию уровня O1, нормальная реализация уменьшается до 10 мс, что, очевидно, медленнее. Спасибо за комментарий @Peter Cordes.

update2 В моем P C, в режиме VS2019, x64, полные параметры команды при выборе оптимизации / O1:

/permissive- /GS /GL /W3 /Gy /Zc:wchar_t /Zi /Gm- /O1 /sdl /Fd"x64\Release\vc142.pdb" /Zc:inline /fp:precise /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /Gd /Oi /MD /FC /Fa"x64\Release\" /EHsc /nologo /Fo"x64\Release\" /Fp"x64\Release\Project1.pch" /diagnostics:column 

Переключение на / O2 оптимизацию, это:

/permissive- /GS /GL /W3 /Gy /Zc:wchar_t /Zi /Gm- /O2 /sdl /Fd"x64\Release\vc142.pdb" /Zc:inline /fp:precise /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /Gd /Oi /MD /FC /Fa"x64\Release\" /EHsc /nologo /Fo"x64\Release\" /Fp"x64\Release\Project1.pch" /diagnostics:column 

Единственное отличие состоит в /O1 и /O2.

При просмотре MSDN do c, мы видим:

/O1 (Minimize Size) /Og /Os /Oy /Ob2 /GF /Gy
/O2 (Maximize Speed)    /Og /Oi /Ot /Oy /Ob2 /GF /Gy

Ответы [ 2 ]

0 голосов
/ 27 апреля 2020

Кажется, ваш первый l oop был оптимизирован. Я вынужден был сохранить его, добавив оператор cout со случайным индексом.

Типичный результат:

normal: 9.203 ms
sse:    0.814 ms
avx:    0.678 ms

Интересно, что даже после включения автоматизации c векторизация время normal не улучшить.

0 голосов
/ 27 апреля 2020

Первое, что я вижу, это то, что вы используете несколько копий памяти. вы можете оставить их в стороне, определив объединение:

union f64_m256{
double f[4];
__m256d m; 
} 

теперь каждый объект типа f64_m256 занимает одно и то же место в памяти (типы имеют одинаковые 256 бит). Теперь вы можете сделать, например:

//build x as a combined type - builds first the array
f64_m256 x = {1.,2.,3.,4.}; 

//calculate the sqrt via simd and return it to x 
x.m = _mm256_sqrt_pd(x.m);

//access the calculated sqrts
cout << x.f[2] << endl;

, это сэкономит вам некоторые ненужные копии и может быть применено практически ко всем инструкциям simd.

Хитрость заключается в доступе к памяти в x в способ, который вам удобен / подходит для функции (как _m256d или массив значений типа double).

...