Я новичок в оптимизации 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