C++ SIMD入门
阅读原文时间:2021年04月24日阅读:1

Intel SIMD入门

我前些日子优化一个程序,需要使用C++的SIMD。我查阅了很多资料,很多说的都不详细,遇到了很多问题,踩了很多坑,心灵很受伤。为了总结一下前些日子的学习情况,让后面需要学习SIMD的人少走弯路,写这篇博客。

什么是SIMD?

SIMD的全称叫做,单指令集多数据(Single Instruction Multiple Data)。最直观的理解就是,向量计算。比如一个加法指令周期只能算一组数(一维向量相加),使用SIMD的话,一个加法指令周期可以同时算多组数(n维向量相加),二者用时基本相等,极大地提高了运算效率。

Intel在自家的CPU上实现了SIMD技术,SIMD技术本质上是一种指令集上的并行。我们使用的SIMD技术,大概率是在Intel的CPU上跑的,小概率有人使用AMD的,AMD我没用过。所以使用前,大家先查查自己的cpu是不是支持SIMD技术,具体方法自己查。

我的笔记本的CPU是core i5 4210M,已经很落后的CPU了,然后我的台式机cpu是i5 8500,这两个都支持SIMD。所以你可以直接跑个demo测试一下,我直接就跑通了,所以我也没去查询自己支不支持。

如何使用SIMD

先放SIMD一段代码,给大家一个直观的感受,就是两个向量相加的一个程序。

#include "immintrin.h"
#include "stdio.h"

void _mm256_print_epi32(__m256i p){
     int *p1 = (int*)&p;
     printf("%d %d %d %d %d %d %d %d\n",p1[0],p1[1],p1[2],p1[3],p1[4],p1[5],p1[6],p1[7]);
};

int main()
{
    __m256i a = _mm256_set_epi32(7,6,5,4,3,2,1,0);//从由地址高到低的顺序装载
    _mm256_print_epi32(a);
    __attribute__((aligned(32))) int d1[8] = {-1,-2,-3,-4,-5,-6,-7,-8};
    __m256i d = _mm256_load_si256((__m256i*)d1);//装在int可以使用指针类型转换 必须32位对齐
   _mm256_print_epi32(d);
    __m256i d2 = _mm256_add_epi32(d,a);
   _mm256_print_epi32(d2);
    return 0;
}

makefile:

simd:main.cpp
    g++ -mavx2 main.cpp -o simd -g 
clean:
    rm simd

结果是:

ls@ls-ChengMing-3980:~/Documents/vscode/C++ simd guide$ make
g++ -mavx2 main.cpp -o simd -g 
ls@ls-ChengMing-3980:~/Documents/vscode/C++ simd guide$ ./simd 
0 1 2 3 4 5 6 7
-1 -2 -3 -4 -5 -6 -7 -8
-1 -1 -1 -1 -1 -1 -1 -1

文件目录就是一个main.cpp 和一个makefile,很普通,亲测可跑。之前在网上翻了好多例子,以八个数相加求和的例子居多,我跑了发现运行不了。现在懂了,他们的写的代码不规范,很容易跑错。

那么从第一行添加头文件开始说起吧。打开这个网址: intel的官方guaid:[Intel intrinsics Guaid](https://software.intel.com/sites/landingpage/IntrinsicsGuide/#expand=914) 里面有MMX、SSE、AVX、AVX512等等,这些讲的都是寄存器,你要同时算多少位。 多少位得看你计算量了,MMX指的是一个64位寄存器,SSE是128bit,AVX是256bit,AVX512就是512bit的寄存器啦。如果整型int是32位的,那么一个avx寄存器有256bit就能装8个int型,那么他就可以同时操作8个整数,并只花一个加法指令的时间。那么使用avx的话,就得看那个网站,把有关avx的函数都选出来。 任意点开avx的一个函数,上面都会告诉你添加什么头文件,可以看到添加的是“immintrin.h”

然后呢,下面那个打印函数是我自己写的,先不care它,向后看。

__m256i a = _mm256_set_epi32(7,6,5,4,3,2,1,0);

这是啥意思捏,就是定义一个m256i的向量a,将0~7一共8个整数装载到256位的a向量中。m256i的意思是这个类型里面装的是8个整型数据,m256(没有i)的意思的装了8个float类型数据的意思,m256d装的就是4个double类型的,因为double是64位的。同理你还会遇到m128i就是装了4个整型变量的意思,m128就是装了4个float型的。变量的命名规则大致讲清楚了。然后咱们去看看这个set函数又是个啥,查找guide:

__m256i _mm256_set_epi32 (int e7, int e6, int e5, int e4, int e3, int e2, int e1, int e0)
Synopsis
__m256i _mm256_set_epi32 (int e7, int e6, int e5, int e4, int e3, int e2, int e1, int e0)
#include <immintrin.h>
CPUID Flags: AVX
Description
Set packed 32-bit integers in dst with the supplied values.
Operation
dst[31:0] := e0
dst[63:32] := e1
dst[95:64] := e2
dst[127:96] := e3
dst[159:128] := e4
dst[191:160] := e5
dst[223:192] := e6
dst[255:224] := e7
dst[MAX:256] := 0

查guide你会发现一堆函数,命名大致为_mm256 ×× ss/ps/epi32/si256等等。这个set函数顾名思义,就是个设置函数,将8个整型一一设置好是什么。一般操作整型的,结尾都是epi32或者si256,操作浮点型的,结尾都是s,ss是操作一个(single),ps是操作一组的意思(packed)。
那么现在a设置好了,a的值是0,1,2,3,4,5,6,7 。
上了个厕所,回来继续写。
接着看下一条

    __attribute__((aligned(32))) int d1[8] = {-1,-2,-3,-4,-5,-6,-7,-8};
    __m256i d = _mm256_load_si256((__m256i*)d1);//装在int可以使用指针类型转换 必须32位对齐

这是把自己的数组装载到一个m256的向量里。整型数组就放m256i,浮点型就放m256。这个数组定义为啥加前面的一段attribute呢,是因为使用load函数要保证数组的起始地址32位字节对齐。在linux下就需要使用我前面的这一段,Windows下要用别的, __declspec(align(32))。这个函数我们可以发现他装载的时候数组地址前要做类型转换,我也闹不明白为啥也要这么干,按照guide上来吧。
再往下就是这个add函数,两个m256i的向量相加。这个函数的指令周期是和“+”一样的。这个函数具体看guide吧,没啥好说的。

    __m256i d2 = _mm256_add_epi32(d,a);


__m256i _mm256_add_epi32 (__m256i a, __m256i b)
Synopsis
__m256i _mm256_add_epi32 (__m256i a, __m256i b)
#include <immintrin.h>
Instruction: vpaddd ymm, ymm, ymm
CPUID Flags: AVX2
Description
Add packed 32-bit integers in a and b, and store the results in dst.
Operation
FOR j := 0 to 7
    i := j*32
    dst[i+31:i] := a[i+31:i] + b[i+31:i]
ENDFOR
dst[MAX:256] := 0

这一步算完,d2中存的就是d和a的和。结果是-1,-1,-1,-1,-1,-1,-1,-1

打印出来看一下:

void _mm256_print_epi32(__m256i p){
     int *p1 = (int*)&p;
     printf("%d %d %d %d %d %d %d %d\n",p1[0],p1[1],p1[2],p1[3],p1[4],p1[5],p1[6],p1[7]);
};
他自己本身不带打印函数,但是他这个m256的结构体啊,就是8个int或者8个float,或者4个double的数组,反正你装什么就是什么数组。所以你想知道里面的具体内容,用指针打印出来就行了,就像我函数里写的那样。

关于SIMD的效率问题

理论上,比如我用m256加法跟串行加法相比,肯定优化完时间提升到之前的12.5%,提升了近87.5%.但是实际上由于装载函数等的操作的存在,效率并达不到那么高,优化效率提高了60%到70%,这还是有可能的。我调试的体会就是,尽量不要在循环里用load,还有gather函数。效率不能体现出来有一部分是你的写法有问题。

SIMD的使用体会

将数组数据装载到向量中,这个操作大部分人都会遇到,连续读取的话使用load就行了,间隔读取的话使用gather就行。但是读取数组再装载到向量里,这个操作非常耗时间,优化的时候需要注意一下。其次就是结果得出来以后啊,是个向量形式,对于有判断的一些操作啊,还得对数据的组织结构进行调整,这一步也挺麻烦,是比串行操作多出来的一部分操作。如果单单是计算,不考虑装载啊,变换结构等操作,SIMD确实可以达到理论上的优化效率,但是还得掺杂一些其他操作,所以效率会有所下降。
SIMD的编写思想就是想实现什么操作,就去查guide,查表,每天多看看,想出来个好方法就去试一试。觉得效率不行就把每个部分的时间算一算,调着调着结果就差不多了。
太困了,写的很快,有些细节可能也没多考虑,第一次写博客,有啥疑惑下方留言,大家一起探讨一下。

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章