Intel指令集及SIMD数据加速
查看CPU相关信息
执行结果举例:
查看电脑CPU支持的指令集:
cat /proc/cpuinfo | grep "processor" | wc -l
支持的指令集:
向量指令集
Flynn分类法根据指令和数据进入CPU的方式,将计算机 架构分为四种不同的类型。
- 1.单指令流单数据流(SISD,Single Instruction stream Single Data stream)
- 2.单指令流多数据流(SIMD,Single Instruction stream Multiple Data stream)
- 3.多指令流单数据流(MISD,Multiple Instruction stream Single Data stream)
- 4.多指令流多数据流(MIMD,Multiple Instruction stream Multiple Data stream)
举例:
SISD指令:需要多条指令完成相关操作
SIMD指令:只需要单条指令就可完成相关操作
API 介绍
官方参考文档见: Intel® Intrinsics Guide
需要什么功能,可对应该文档查看对应的API
因API太多,整个左侧区域是用来筛选,左侧上方的选择区域使用来按指令集来筛选,左测下方的选择区域是用来按功能筛选。
数据类型
可简单理解为,MMX对应64位,SSE对应128位,AVX对应256位
intel 指令集的变量命名规则位__mm<bit_width><data_type>
,其中bit_width
即数据长度(64|128|256),data_type
即存储的数据是什么类型(i|d),i对应int,d对应double,要是空,则对应float
数据类型 | 描述 | 大小 |
---|---|---|
__m64 | 包含2个单精度(float)浮点数的64位向量 | 2 x 32 bit |
__m128 | 包含4个单精度浮点数的128位向量 | 4 x 32 bit |
__m128i | 包含数个整型数值的128位向量 | 128 bit |
__m128d | 包含2个双精度浮点数的128位向量 | 2 x 64 bit |
__m256 | 包含8个单精度浮点数的256位向量 | 8 x 32 bit |
__m256i | 包含数个整型数值的256位向量 | 256 bit |
__m256d | 包含4个双精度浮点数的256位向量 | 4 x 256 bit |
SSE Data Type(16 XMM Registers)
AVX Data Types(16 YMM Registers)
注:
AVX的256位寄存器和SSE的128位寄存器存在着相互重叠的关系(XMM寄存器位YMM寄存器的低位),所以最好不要混用AVX与SSE指令集,否则会导致transition penalty(过度处罚),慢50~80时钟周期
函数类型:
SSE/AVX intrinsic functions的命名格式位_mm<bit_width>_<name>_<data_type>
<bit_width>
:表明向量的位长度,对于128位的向量,这个参数为空;256位的向量,这个参数位256<name>
:描述内联函数的算术操作<data_type>
:标识函数主参数的数据类型。
- ps 包含float 类型的向量,把32 bits当作一个数
- pd包含double类型的向量,把64bits当作一个数
- epi8/epi16/epi32/epi64 :向量中每个数都是整型,包含8位/16位/32位/64位的有符号整数
- epu8/epu16/epu32/epu64: 向量中每个数都是无符号整型(unsigned),包含8位/16位/32位/64位的无符号整数
- m128/m128i/m128d/m256/m256i/m256d:输入值与返回类型不同时,标识输入向量类型
例如:__m256i _mm256_setr_m128i(__m128i lo,__m128i hi)
输入两个__m128i
向量,把他们拼在一起,变成一个__m256i
返回,另外这种结尾只见于load- si128/si256:未指定的128或者256位向量,不关心向量里到底是什么类型,反正是128bit/256bit
例如:__m256i _mm_broadcastsi128_si256 (__m128i a)
函数 | 描述 |
---|---|
load | 用于从内存加载到寄存器 |
set | 用于从内存加载到寄存器 |
add、sub、mul、div | 加减乘除 |
store | 用于从寄存器写入到内存 |
cast | 数据类型转换 |
compare | 数据比较 |
logical(and|or|test) | 逻辑运算 |
mask | 条件判断 |
一个小demo
#include <iostream>
#include <immintrin.h>
int main(int argc, char* agv[])
{
__m256 a = _mm256_set_ps(8.0, 7.0, 6.0,5.0,
4.0, 3.0, 2.0, 1.0);
__m256 b = _mm256_set_ps(18.0, 17.0, 16.0, 15.0,
14.0, 13.0, 12.0, 11.0);
__m256 c = _mm256_add_ps(a, b);
float d[8];
_mm256_storeu_ps(d, c);
std::cout << "result equals:" << d[0] <<","<<d[1]<<","<<d[2]<<","
<<d[4]<<","<<d[5]<<","<<d[6]<<","<<d[7]<<std::endl;
return 0;
}
编译时,需要增加编译参数-msse -mavx -mavx2
g++ simple_demo.cc -mavx2 -mavx -msse
运行结果:
result equals:12,14,16,20,22,24,26
PS:
编译参数 -mavx 是用于启用 AVX(Advanced Vector Extensions)指令集的选项。
AVX 是 Intel 推出的一种 SIMD(Single Instruction, Multiple Data)扩展指令集,它可以提高向量化计算的性能。
在使用 g++ 编译器时,可以使用 -mavx 参数来启用 AVX 指令集的支持
需要注意的是,使用 -mavx 参数编译的程序只能在支持 AVX 指令集的 CPU 上运行,否则可能会导致程序崩溃或产生错误。因此,在使用 -mavx 参数之前,建议先检查目标机器是否支持 AVX 指令集。
支持的函数操作:
存取操作(load/store/set)
set操作类型 | 描述 |
---|---|
_mm256_setzero_ps/pd | 返回一个全为0的float类型的向量 |
_mm256_setzero_si256 | 返回一个全为0的整型向量 |
_mm256_set1_ps/pd | 返回一个float类型的数填充向量 |
_mm256_set1_epi8/epi16/epi32/epi64x | 用整型数填充向量 |
_mm256_set_ps/pd | 用8个float或4个double类型数字初始化向量 |
_mm256_set_epi8/epi16/epi32/epi64x | 用一个整型数初始化向量 |
_mm256_set_m128/m128d/m128i | 用2个128位的向量初始化一个256位向量 |
_mm256_setr_ps/pd | 用8个float或4个double的转置顺序初始化向量 |
_mm256_setr_epi8/epi16/epi32/epi64x | 用若干个整型的转置顺序初始化向量 |
load操作类型 | 描述 |
---|---|
_mm256_load_ps/pd | 从对齐的内存地址加载浮点向量 |
_mm256_load_si256 | 从对齐的内存地址加载整型向量 |
_mm256_loadu_ps/pd | 从未对齐的内存地址加载浮点向量 |
_mm256_loadu_si256 | 从未对齐的内存地址加载整型向量 |
_mm_maskload_ps/pd | 根据掩码加载128位浮点向量的部分 |
_mm256_mask_ps/pd | 根据掩码加载256位浮点向量的部分 |
(2)_mm_maskload_epi32/64 | 根据掩码加载128位整型向量的部分 |
(2)_mm256_maskload_epi32/64 | 根据掩码加载256位整型向量的部分 |
PS:
最后两个函数前面有个(2),代表只在AVX2中支持
store函数:
算术运算
- 加减法
数据类型 | 描述 |
---|---|
_mm256_add_ps/pd | 对两个浮点向量做加法 |
_mm256_sub_ps/pd | 对两个浮点向量做减法 |
(2)_mm256_add_epi8/16/32/64 | 对两个整形向量做加法 |
(2)_mm256_sub_epi8/16/32/64 | 对两个整形向量做减法 |
(2)_mm256_adds_epi8/16 (2)_mm256_adds_epu8/16 | 两个整数向量相加且考虑内存饱和问题 |
(2)_mm256_subs_epi8/16 (2)_mm256_subs_epu8/16 | 两个整数向量相减且考虑内存饱和问题 |
_mm256_hadd_ps/pd | 水平方向上对两个float类型向量做加法 |
_mm256_hsub_ps/pd | 垂直方向上最两个float类型向量做减法 |
(2)_mm256_hadd_epi16/32 | 水平方向上对两个整形向量做加法 |
(2)_mm256_hsub_epi16/32 | 水平方向上最两个整形向量做减法 |
(2)_mm256_hadds_epi16 | 对两个包含short类型的向量做加法且考虑内存饱和的问题 |
(2)_mm256_hsubs_epi16 | 对两个包含short类型的向量做减法且考虑内存饱和的问题 |
_mm256_addsub_ps/pd | 加上和减去两个float类型的向量 |
将饱和度考虑在内的函数将结果钳制到可以存储的最小/最大值。没有饱和的函数在饱和发生时忽略内存问题。
而在水平方向上做减法的意思如下图:
_mm256_hadd_pd
接口描述
Operation
dst[63:0] := a[127:64] + a[63:0]
dst[127:64] := b[127:64] + b[63:0]
dst[191:128] := a[255:192] + a[191:128]
dst[255:192] := b[255:192] + b[191:128]
dst[MAX:256] := 0
最后一个指令:_mm256_addsub_ps/pd
在偶数位置减去,奇数位置加上,获最后的目标向量
- 乘除法
数据类型 | 描述 |
---|---|
_mm256_mul_ps/pd | 对两个float类型的向量进行相乘 |
(2)_mm256_mul_epi32 (2)_mm256_mul_epu32 | 将包含32位整数的向量的最低四个元素相乘 |
(2)_mm256_mullo_epi16/32 | Multiply integers and store low halves |
(2)_mm256_mulhi_epi16 (2)_mm256_mulhi_epu16 | Multiply integers and store high halves |
(2)_mm256_mulhrs_epi16 | Multiply 16-bit elements to form 32-bit elements |
_mm256_div_ps/pd | 对两个float类型的向量进行想除 |
- 融合乘法和加法
数据类型 | 描述 |
---|---|
(2)_mm_fmadd_ps/pd/ (2)_mm256_fmadd_ps/pd | 将两个向量相乘,再将积加上第三个。(res=a*b+c) |
(2)_mm_fmsub_ps/pd/ (2)_mm256_fmsub_ps/pd | 将两个向量相乘,然后从乘积中减去一个向量。(res=a*b-c) |
(2)_mm_fmadd_ss/sd | 将向量中最低的元素相乘并相加(res[0]=a[0]*b[0]+c[0]) |
(2)_mm_fmsub_ss/sd | 将向量中最低的元素相乘并相减(res[0]=a[0]*b[0]-c[0]) |
(2)_mm_fnmadd_ps/pd (2)_mm256_fnmadd_ps/pd | 将两个向量相乘,并将负积加到第三个。(res = -(a * b) + c) |
(2)_mm_fnmsub_ps/pd/ (2)_mm256_fnmsub_ps/pd | 将两个向量相乘,并将负积加到第三个 (res = -(a * b) - c) |
(2)_mm_fnmadd_ss/sd | 将两个向量的低位相乘,并将负积加到第三个向量的低位。(res[0] = -(a[0] * b[0]) + c[0]) |
(2)_mm_fnmsub_ss/sd | 将最低的元素相乘,并从求反的积中减去第三个向量的最低元素。(res[0] = -(a[0] * b[0]) - c[0]) |
(2)_mm_fmaddsub_ps/pd/ (2)_mm256_fmaddsub_ps/pd | 将两个矢量相乘,然后从乘积中交替加上和减去(res=a*b+/-c) |
(2)_mm_fmsubadd_ps/pd/ (2)_mmf256_fmsubadd_ps/pd | 将两个向量相乘,然后从乘积中交替地进行减法和加法(res=a*b-/+c)(奇数次方,偶数次方) |
- 排列和洗牌(Permuting and Shuffling)
排列 Permuting
数据类型 | 描述 |
---|---|
_mm_permute_ps/pd _mm256_permute_ps/pd | 根据8位控制值从输入向量中选择元素 |
(2)_mm256_permute4x64_pd/ (2)_mm256_permute4x64_epi64 | 根据8位控制值从输入向量中选择64位元素 |
_mm256_permute2f128_ps/pd | 基于8位控制值从两个输入向量中选择128位块 |
_mm256_permute2f128_si256 | 基于8位控制值从两个输入向量中选择128位块 |
_mm_permutevar_ps/pd _mm256_permutevar_ps/pd | 根据整数向量中的位从输入向量中选择元素 |
(2)_mm256_permutevar8x32_ps (2)_mm256_permutevar8x32_epi32 | 使用整数向量中的索引选择32位元素(浮点和整数) |
洗牌Shuffling
数据类型 | 描述 |
---|---|
_mm256_shuffle_ps/pd | 根据8位值选择浮点元素 |
_mm256_shuffle_epi8/ _mm256_shuffle_epi32 | 根据8位值选择整数元素 |
(2)_mm256_shufflelo_epi16/ (2)_mm256_shufflehi_epi16 | 基于8位控制值从两个输入向量中选择128位块 |
对于_mm256_shuffle_pd,只使用控制值的高4位。如果输入向量包含int或float,则使用所有控制位。对于_mm256_shuffle_ps,前两对位从第一个矢量中选择元素,第二对位从第二个矢量中选择元素。
举例:
#include <iostream>
#include <immintrin.h>
void print_value(__m128 value)
{
float *v = (float *)&value;
std::cout<< v[0] << "," << v[1] << ","<< v[2] << ","<<v[3] << std::endl;
}
// load,用于从内存加载到寄存器中
__m128 function_load(float const*mem_addr)
{
return _mm_load_ps(mem_addr);
}
// set,用于从内存加载到寄存器中
__m128 function_set(float const *mem_addr)
{
return _mm_set_ps(mem_addr[3], mem_addr[2], mem_addr[1], mem_addr[0]);
}
// 直接初始化
__m128 direct_init(float const *mem_addr)
{
__m128 value = {mem_addr[0], mem_addr[1], mem_addr[2], mem_addr[3] };
return value;
}
int main()
{
float *mem_addr = (float*)malloc(sizeof(float)*4);
mem_addr[0] = 1.0f;
mem_addr[1] = 2.0f;
mem_addr[2] = 3.0f;
mem_addr[3] = 4.0f;
print_value(function_load(mem_addr));
print_value(function_set(mem_addr));
print_value(direct_init(mem_addr));
return 0;
}
运行结果:
一个简单的加法验证SIMD效率上的提升
// 写一个简单的程序来验证SIMD在效率方面的提升
#include <iostream>
#include <nmmintrin.h>
#include <immintrin.h>
#include <string>
#include <time.h>
#include <stdlib.h>
void print_time_result(std::string func_name, clock_t start, clock_t finish, int res)
{
double time = (double(finish - start) / double(CLOCKS_PER_SEC)) * 1000; // ms
std::cout << func_name << ": " << time << "ms, result = " << res << std::endl;
}
// 普通的加法
void normal_add(int *nums, size_t n)
{
clock_t start = clock();
int sum = 0;
for (size_t i = 0; i < n; i++)
{
sum += nums[i];
}
clock_t finish = clock();
print_time_result("normal_add", start, finish, sum);
}
// 使用128bit的SIMD
void SSE_add(int *nums, size_t n)
{
clock_t start = clock();
__m128i simd_sum = _mm_setzero_si128();// 累加和
int normal_sum = 0;
size_t loop = n / 4;
size_t reserve = n % 4;
__m128i *p = (__m128i *)nums;
for (size_t i = 0; i < loop; i++)
{
simd_sum = _mm_add_epi32(simd_sum, *p);
p++;
}
int *q = (int *)p;
for (size_t i = 0; i < reserve; i++)
{
normal_sum += q[i];
}
normal_sum += (((int *)&simd_sum)[0] + ((int *)&simd_sum)[1] + ((int *)&simd_sum)[2] + ((int *)&simd_sum)[3]);
clock_t finish = clock();
print_time_result("SSE_add", start, finish, normal_sum);
}
// 使用256bit的SIMD
void AVX2_add(int *nums, size_t n)
{
clock_t start = clock();
__m256i simd_sum = _mm256_setzero_si256();
__m256i simd_load;
int normal_sum = 0;
size_t loop = n / 8;
size_t reserve = n % 8;
__m256i *p = (__m256i *)nums;
for (size_t i = 0; i < loop; i++)
{
simd_load = _mm256_load_si256((const __m256i *)p);
simd_sum = _mm256_add_epi32(simd_sum, simd_load);
p++;
}
int *q = (int *)p;
for (size_t i = 0; i < reserve; i++)
{
normal_sum += q[i];
}
normal_sum += (((int *)&simd_sum)[0] + ((int *)&simd_sum)[1] +
((int *)&simd_sum)[2] + ((int *)&simd_sum)[3] +
((int *)&simd_sum)[4] + ((int *)&simd_sum)[5] +
((int *)&simd_sum)[6] + ((int *)&simd_sum)[7]);
clock_t finish = clock();
print_time_result("AVX2_add", start, finish, normal_sum);
}
int main()
{
size_t n = 10000000;
int *nums = NULL;
// nums = (int *)malloc(sizeof(int) * n);
posix_memalign((void **)&nums, 32, sizeof(int) * n); // 注意要32字节对齐,否则会段错误
for (size_t i = 0; i < n; i++)
{
nums[i] = 5;
}
normal_add(nums, n);
SSE_add(nums, n);
AVX2_add(nums, n);
return 0;
}
上述代码其实就是在做10000000个5相加,normal_add()是按照一般思路,循环1千万次加法,SSE_add()是4个一组(128bit)相加,AVX2_add()是8个一组(256bit)相加。
注意:对于SSE,我们申请的空间要16字节对齐,对于AVX要32字节对齐,否则会出现段错误。
// 方法一:
__attribute__((aligned(16))) float A[10000000];// Linux下
// 方法二:
posix_memalign((void **)&B, 32, sizeof(int) * 10000000); // #include <stdlib.h>
运行结果:
也许和硬件不一样,在自己电脑上还是有提升的。
参考博客
https://blog.triplez.cn/avx-avx2-learning-notes/
https://blog.csdn.net/just_sort/article/details/94393506
https://zhuanlan.zhihu.com/p/457505686