C++ SSE加法比+;操作人员

C++ SSE加法比+;操作人员,c++,x86,sse,simd,C++,X86,Sse,Simd,我试图测试SSE加法有多快,但有些地方不对劲。我在堆栈中创建了两个用于输入的数组和一个用于输出的数组,并以两种方式对它们执行加法。它比常规+运算符慢。我在这里做错了什么: #include <iostream> #include <nmmintrin.h> #include <chrono> using namespace std; #define USE_SSE typedef chrono::steady_clock::time_point Time

我试图测试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;
}
#包括
#包括
#包括
使用名称空间std;
#定义和使用
typedef chrono::稳定时钟::时间点时间戳;
typedef chrono::稳定的时钟;
int main()
{
常数int MAX=100000*4;
浮动1[MAX];
浮动in2[MAX];
浮出[最大值];
memset(偏差,0,尺寸(浮动)*最大值);
对于(int i=0;i如果代码中有bug,则非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//COD>易失性)。。始终检查装配代码,以确保测量结果。

如果代码中有错误,则非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//COD>易失性)。。请始终查看汇编代码,以确定您测量的是什么。

这里是您的基准测试的改进版本,对标量代码进行了错误修复、计时改进和编译器矢量化禁用(至少对于gcc和clang):


下面是您的基准测试的一个改进版本,其中对标量代码(至少对gcc和clang)进行了错误修复、计时改进和编译器矢量化禁用:


有时候,试图通过添加循环到测试时间来优化C++代码是非常愚蠢的,这是其中的一个例子:(< /P>) 您的代码从字面上看就是这样的:

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;
}
展开,每次迭代处理32xfloat(在AVX2中)[+在迭代结束时处理多达31个元素的额外代码]

F1:上面的SSE“优化”循环。(显然,此代码最多不能处理循环末尾的3个元素)

因此编译器已经展开了循环,但是它回到了SSE(根据请求),所以现在是原始循环性能的一半(不完全正确-内存带宽将是这里的限制因素)

<强> f2< /强>:手动展开的C++循环(索引已被纠正,但仍然无法处理最后3个元素)

嗯,这完全无法矢量化!它只是一次处理一个加法。嗯,这通常是指针别名,因此我将从以下内容更改函数原型:

void func(float* out, const float* in1, const float* in2, int MAX);
为此:(F4

现在编译器将输出向量化的内容:

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

<>强> > 这个代码仍然是第一个版本的一半……

< P>有时试图通过添加循环到测试时序来优化C++代码是非常愚蠢的,这是其中的一个例子:(< /P>) 您的代码从字面上看就是这样的:

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;
}
展开,每次迭代处理32xfloat(在AVX2中)[+在迭代结束时处理多达31个元素的额外代码]

F1:上面的SSE“优化”循环。(显然,此代码最多不能处理循环末尾的3个元素)

因此编译器已经展开了循环,但是它回到了SSE(根据请求),所以现在是原始循环性能的一半(不完全正确-内存带宽将是这里的限制因素)

<强> f2< /强>:手动展开的C++循环(索引已被纠正,但仍然无法处理最后3个元素)

嗯,这完全无法矢量化!它只是一次处理一个加法。嗯,这通常是指针别名,因此我将从以下内容更改函数原型:

void func(float* out, const float* in1, const float* in2, int MAX);
为此:(F4

现在编译器将输出向量化的内容:

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

然而这段代码的性能仍然是第一个版本的一半….

让SSE版本遍历数组而不是标量版本看起来不公平..任何合适的优化编译器都会将非SSE路径转换为实际使用SSE。事实上,它不仅会使循环矢量化,而且会也很可能展开它。在这种情况下,朴素循环很可能会更快。编译器确实非常聪明,很难(但并非不可能)击败它们。真正确定发生了什么的唯一方法是检查生成的代码(asm)。还要记住,每当计时代码时,都要打开优化。计时非优化代码通常是毫无意义的。让SSE版本遍历数组而不是标量版本看起来不公平。任何像样的优化编译器都会将非SSE路径转换为实际使用SSE。事实上,这不仅仅是矢量化你的循环很可能也会展开它。在这种情况下,朴素的循环很可能会更快。编译器真的非常聪明,很难(但并非不可能)击败他们。真正确定发生了什么的唯一方法是检查泛型
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
void func(float* out, const float* in1, const float* in2, int MAX);
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