SSE加法比+运算符慢

时间:2018-11-15 05:01:45

标签: c++ x86 sse simd

我试图测试添加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 :(得分: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,那么您的测试只是向您证明内存带宽有限。像这样的性能分析循环没有用,您最好在分析器中测试真实情况。...

无论如何,回到有问题的循环中。让我们将代码放入编译器资源管理器中,并使用一些选项...

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

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

但是该代码仍是第一版性能的一半。...