我试图测试添加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;
}
这里是内存对齐问题吗?
答案 0 :(得分:3)
您的代码中有错误,非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)。始终检查汇编代码,以确保要测量什么。
答案 1 :(得分:1)
这里是基准测试的某种改进版本,其中包括错误修复,时序改进以及标量代码(至少对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;
}
在Haswell CPU上,SSE可以提高约1.5倍至1.6倍。这显然比可能的4倍理论上的改进要小,但是由于您每次迭代仅执行1次算术运算,但是执行2次加载和1次存储,因此测试很可能受到带宽限制。
t_scalar = 0.529723 ns / point
t_SIMD = 0.329758 ns / point
speed-up = 1.6064x
答案 2 :(得分:1)
有时尝试通过添加循环来测试时序来“优化” 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条内存指令。如果您的ram是1600Mhz,而CPU是3200Mhz,那么您的测试只是向您证明内存带宽有限。像这样的性能分析循环没有用,您最好在分析器中测试真实情况。...
无论如何,回到有问题的循环中。让我们将代码放入编译器资源管理器中,并使用一些选项...
F0 :只是一个基本的,无聊的C循环。
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
展开,每次迭代处理32xfloat(在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(按要求),所以现在是原始循环的一半性能(不是很正确-内存带宽将成为限制因素)< / em>。
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
但是该代码仍是第一版性能的一半。...