向量intrinsic编程

本文最后更新于:1 年前

SIMD指令集的发展

什么是SIMD

我们接触的传统的程序,一般是SISD,即Single Instruction Single Data,单指令单数据流,它是传统的串行计算机架构,即相当于在该计算机上,任何时刻都是从指令流里取一条指令来处理数据流中的一个数据,交由一个核心来处理。

SIMD则是利用到了计算机的多个核心的较新的并行架构,采用控制器来控制多个处理器,对多个核心的不同数据执行同一条指令,从而实现并行。

SIMD有对应SIMD指令集,而支持这些SIMD指令集的CPU在设计的时候增加了专用的向量寄存器

就好像之前一条指令只能处理一个单精度浮点数,而现在采用一条SIMD指令能处理如4个,8个甚至更多个的单精度浮点数,这些数据就存储于向量寄存器中,显然,向量寄存器较通用寄存器其能容纳更多的位数。当然,放入向量寄存器的多个数据的数据类型是一致的

SIMD指令集的历史进程

下图是SIMD指令集随CPU的发展:

SIMD_Instruction_development

  • MMX(Multi Media eXtension),伴生64位的MM寄存器
  • SSE(Streaming Simd Extension),伴生128位的XMM寄存器,XMM0-XMM15,一共十六个寄存器
  • AVX/AVX2(Advanced Vector eXtension),伴生256位的YMM寄存器,YMM0-YMM15,一共十六个寄存器。AVX2增加了乘加融合指令(FMA,这同时也是一个硬件)。低128位对应XMM
  • AVX512,伴生512位的ZMM寄存器,ZMM0-ZMM31,一共32个寄存器,还有8个操作掩码寄存器,K0-K7,低128位和低256位对应XMM和YMM

SIMD指令集的数据类型

虽说有好像很多种指令集,但毕竟都是一家公司发行的,有一定规律可遵循:

__m+位宽+数据类型

其中位宽有128位(SSE),256位(AVX/AVX2),512位(AVX512)

数据类型有三种:

  • 什么都不写,就代表单精度浮点数
  • d,代表double,双精度浮点数
  • i,整型,对应char,short,int,long这一堆定点数

因此数据类型可以如下表示:

__m256: 向量长度为256位的单精度浮点数,有8个32位的float

__m128i: 向量长度为128位的整型,有多个整型数值

__m5121d: 向量长度为512位的双精度浮点数,有8个64位的double

SIMD指令集的方法

在说到SIMD指令集,如AVX指令集时,我们知道,可以采用指令集中的汇编指令去操纵YMM寄存器,但是,汇编难写难读难管理,因此有了Intrinsic Function

Intrinsic Function就跟C一样,是较于汇编的一种高级语言(但没到C那种高级程度,只是用类C的方式编写汇编),它又与C不同(毕竟面对的寄存器不是通用寄存器了,而是向量寄存器),但可以和C/C++无缝融合

需要注意:Intrinsic Function并非完全与指令集一一对应

SSE/AVX的intrinsic function命名习惯如下:

__<return_type> _<vector_size>_<intrin_op>_<suffix>

  • 返回类型return_type就是上面对应的SIMD数据类型,如__m256d这些
  • 向量长度vector_size就是代表函数操作的数据向量的位长度,如mm表示128位的数据向量(SSE),mm256表示256位的数据向量(AVX/AVX2),mm512表示512位的数据向量(AVX512)
  • 代表函数具体功能的intrin_op
  • suffix是后缀,表示函数参数的数据类型,p=packed,s=single(fp32),d=double(fp64),ep=整型(具体英文指代词没找到)
    • ps
    • pd
    • epi8/epi16/epi32/epi64
    • epu8/epu16/epu32/epu64
    • si128/si256: 未指定的128位/256位向量,si=unSpecIfied

常规方法类型

SET系列,LOAD系列,STORE系列,MATH系列,COMPARE系列,CONVERT系列,SHUFFLES系列,详情参见[1]

注:load是往向量寄存器YMM里灌数据,而store给从向量寄存器写到内存去

其中shuffles中有个_mm256_blendv_pd(__m256d a,__m256d b,__m256d mask)中的v表示的是variable,否则是constant,且它可以这么理解:mask中的false由a填充,true的部分则由b填充

第一个intrinsic的demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <immintrin.h>
using namespace std;
int main(int argc, char const *argv[])
{
srand(time(0));
__m256 a = _mm256_set_ps(1.0, 2.1, 3.2, 4.3,
5.4, 6.5, 7.6, 8.7);

__m256 b = _mm256_set_ps(3.8, 4.9, 6.0, 7.1,
8.2, 9.3, 10.4, 11.5);

__m256 c = _mm256_add_ps(a, b);

float d[8];

_mm256_storeu_ps(d, c); // 这里的u是unaligned的意思

for (int i = 0; i < 8; i++)
cout << d[i] << ends;
cout << endl;

return 0;
}

immintrin.h是AVX的头文件,然后在编译的时候要加上-mavx以启用AVX指令集

AVX2算子编写

这部分的内容大量的参考了学习资料[1],其中内容编写部分是我看了之后自己重新编写的

GELU

参照学习资料[1]自行观看,过于直白,其中tanh是一种近似实现,当然也可以用别的轻量数学库来替换,好像是Intel C++ Compiler没有这个的实现(但是好像MKL是有的),用别的编译器可能存在不支持AVX2指令的情况.查了下百度,原因如下(要做优化,就别用cmath这些,当然icc针对这些数学公式也有优化的库:mkl):

icc-tanh

softmax

矩阵加

矩阵转置

参考文章


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!