【发布时间】:2020-08-11 04:03:54
【问题描述】:
我是 SIMD 优化的新手,试图为一维浮点数组计算每个元素的 sqrt 值。
系统:Windows 10 编译器:Visual Studio 2017 CPU:英特尔酷睿 i5-8500
以下代码在发布模式下编译和运行,但是正常(幼稚)的实现速度几乎与 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
更新1
只是忘了提及我正在使用的优化级别。它是 cmake 生成的 Visual Studio 项目,使用 O2 优化。
还测试过:切换到O1级优化,正常实现decreate到10ms,很明显慢了。感谢@Peter Cordes 的评论。
更新2 在我的电脑上,在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's doc,我们可以看到:
/O1 (Minimize Size) /Og /Os /Oy /Ob2 /GF /Gy
/O2 (Maximize Speed) /Og /Oi /Ot /Oy /Ob2 /GF /Gy
【问题讨论】:
-
您的编译器是否自动矢量化了标量版本?
a[i] = sqrt(a[i]);应该使用现代编译器轻松矢量化。尝试使用gcc -O3 -fno-tree-vectorize,或任何与 MSVC 等效的东西。可能优化大小而不是速度? (MSVC 是否称其为-O1?) -
@PeterCordes 我默认使用的是 /O2,它由 cmake 提供。切换到 /O1 会使
normal变慢。 -
是的,通常你应该在构建时进行全面优化,让编译器知道如何快速编写代码。这表明手动矢量化这个循环没有任何好处。
-
在 MSVC 2019 中 /O1 和 /O2 的性能相同。第一个版本自动矢量化为
sqrtps,并与 SSE 版本相同。顺便说一句,你见过_aligned_malloc和_aligned_free吗? -
@rustyx 我在 MSVC 2019 中测试,/O1 和 /O2 导致不同的性能。
标签: c++ optimization sse simd