SIMD编程
SIMD概念
SPMD单个程序在不同数据流上执行
本讲主要介绍单核向量编程
SIMD编程概述
向量计算机
早期的SIMD超级计算机:银河
当前的SIMD架构
- 多媒体扩展:SSE、AVX
- 图形和游戏处理器:CUDA
- 协处理器:Xeon Phi
没有占压倒优势的SIMD编程模型
- 向量计算机都是科学家用来编程
- 多媒体扩展指令集多是系统程序员在用
- GPU多是游戏开发者、大数据分析人员使用
标量和SIMD(多媒体扩展架构)差别
多媒体扩展架构的核心
SIMD并行
可变大小的数据域
向量长度=寄存器宽度➗类型大小
-
这里有128位寄存器,存储数据的大小由数据类型决定,比如如果存储长整型(32字节)的话,只能支持4个数同时计算
适合应用SIMD的特点
- 规律的数据访问模式
- 数据项在内存中连续存储
- 短数据类型:8、16、32位
- 流式数据处理,一系列处理阶段
- 时间局域性,数据流重用
- 很多情况下可用来提升计算效率
- 很多常量
- 循环迭代短
- 规律的数据访问模式
为什么采用SIMD
- 更大的并发度
- 设计简单、重复功能单元即可
- 更小的芯片设计
- 缺点:代码很底层繁琐
多媒体扩展编程
语言/指令集扩展
程序接口类似函数调用
C/C++:内置函数、 intrinsics
大多数编译器支持多媒体扩展
➢gcc:-march=corei7, -faltivec
SSE2: dst= _mm_add_ps(src1, src2);
AltiVec: dst= vec_add(src1, src2);
Neon: dst = vaddq_f32(src1, src2)
➢无统一标准
很多编译器支持自动编译
SIMD并行(不是很重要,了解即可)
几个例子
-
把重复的算术运算变成向量运算
-
存取数据的次数降低,效率提升
可向量化的循环
可部分向量化的循环(有数据依赖)
SIMD编程的额外开销
打包/解包数据的开销:重排数据使之连续
- 打包源运算对象——拷贝到连续内存区域
- 解包目的运算对象——拷贝回内存
对齐:调整数据访问,使之对齐
对齐的内存访问
- 地址总是向量长度的倍数(例如16字节)
未对齐的内存访问
地址不是16字节的整数倍
静态对齐:对未对齐的读操作,做两次相邻的对齐读操作,然后进行合并
有未对齐相应操作函数,仍会产生多次内存操作
底层硬件的操作:
静态调整循环
动态对齐:
不知道从几开始,虽然有额外的对齐开销,但是结果一定是正确的
小结
最坏情况需要计算地址,动态对齐
编译器/程序员可分析确认对齐
- 一般而言数据是从起始地址处对齐的
- 如果在一个循环中顺序访问数据,起始位置固定,则对齐特性是不变的
可调整算法,先串行处理到对齐边界, 然后进行SIMD计算
有时对齐开销会完全抵消SIMD的并行收 益
控制流可能要求执行所有路径
底层实现
能否改进
- 假定所有控制流路径执行频率都不同
- 应该针对频率最高的路径优化代码
- 其他路经按默认方式执行
控制流开销小结
SIMD编程复杂性
SSE/AVX编程
X86架构
发展历史
X86:Intel开发的一种微处理器体系结构
出现:1978年Intel 8086 CPU中
- 但是一般来说X86指的是X86_32bit,32位系统
- 64位就是指X86_64bit。简写为X64
发展:
❑ 1971-1992年数字编号:80X86系列
❑ 1993-2005年奔腾系列:Pentium
❑ 2005酷睿系列:Core
基本框架
- 基本的执行模式
- 数据类型
- 指令集合(只讲这个)
- ❑ 通用指令(传送,算术,逻辑,控制等)
- ❑ X87 FPU指令(传送,算术,比较,控制等 )
- ❑ MMX指令(传送,转化,打包,比较等)
- ❑ SSE指令(增加寄存器,SIMD浮点数运算)
- ❑ SSE2指令(整数指令,64-bit SIMD浮点运 算)
- 寄存器
x86架构SIMD支持
SSE指令集
是什么
发展历史
SSE是MMX的超集
MMX
- ❑ 1996年Intel在奔腾处理器集成MMX指令, 为应对音频、图片、视频等多媒体应用的密 集的计算需求
- ❑ 64-bit的MMX寄存器(8个,复用了浮点寄存器的尾部,与x87共用寄存器,缺少浮点指令)
- ❑ 支持在打包的字,字节,双字整数上的 SIMD操作
SSE128bit寄存器与MMX寄存器的区别
- AVX 256位
AVX指令集
SSE编程
SSE指令
数据移动指令
将数据移入/出向量寄存器
算术指令
多个数据(2 doubles、4 floats等) 上的算术运算
- ❑ PD:两个双精度,PS:四个单精度,SS:标量
- ❑ ADD、SUB、MUL、DIV、SQRT、MAX、MIN、RCP等
- ➢ADDPS:四个单精度加法;ADDSS:标量加法
逻辑指令
多个数据上的逻辑运算
- ❑ AND、OR、XOR、ANDN等
- ➢ANDPS – 运算对象位与
- ➢ANDNPS – 运算对象位与非
比较指令
多个数据上的比较运算
❑ CMPPS、CMPSS:比较运算对象,每个比较结果影 响SIMD寄存器中32位——全1或全0
洗牌指令
在SIMD寄存器内移动数据
- ❑ SHUFPS:从一个运算对象洗牌数据保存到另一个运 算对象
- ❑ UNPCKHPS:解包高位数据到一个SIMD寄存器
- ❑ UNPCKLPS
其他指令
- ❑ 类型转换:CVTPS2PI mm,xmm/mem64
- ❑ 缓存控制
- ➢MOVNTPS将浮点数据从一个SIMD寄存器保存到内存,绕 过缓存
- ❑ 状态管理:LDMXCSR读取MXCSR状态寄存器
SSE C/C++编程
SSE指令对应C/C++ intrinsic
- ❑ intrinsic:编译器能识别的函数,直接映射 为一个或多个汇编语言指令。Intrinsic函数 本质上比调用函数更高效
- ❑ Intrinsics为处理器专有扩展特性提供了一个 C/C++编程接口
- ❑ 主流编译器都支持,如GCC
使用SSE intrinsics所需的头文件(向前兼容)
- 向前兼容:比如我想要使用SSE3,其中就包含了SSE和SSE2
编译选项: -march=corei7
AMD CPU对MMX/SSE/SSE2支持较好,SSE4支持较差
矩阵乘法
串行版本
void mul(int n,float a[][maxN],float b[][maxN],float c[][maxN]){
for (int i=0;i<n;i++){
for(int j=0;j<n;j++){
c[i][j]=0.0;
for(int k=0;k<n;k++){
c[i][j]=a[i][k]*b[k][j];
}
}
}
}
cache优化
把ab两个矩阵都变成行主序
void trans_mul(int n,float a[][maxN],float b[][maxN],float [][maxN]){
//转置
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
swap(b[i][j],b[j][i])
for (int i=0;i<n;i++){
for(int j=0;j<n;j++){
c[i][j]=0.0;
for(int k=0;k<n;k++){
c[i][j]=a[i][k]*b[j][k];//⚠️是b[j][k],不是b[k][j]
}
}
}
//转置
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
swap(b[i][j],b[j][i])
}
SSE版本🌟
不懂
void sse_mul(int n,float a[][maxN],b[][maxN],float c[][maxN]){
__m128t1,t2,sum;
//转置
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
swap(b[i][j],b[j][i]);
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
c[i][j]=0;
sum=_mm_setzero_ps();
for(int k=n-4;k>=0;k-=4){
t1=_mm_loadu_ps(a[i]+k);
t2=_mm_loadu_ps(b[j]+k);
t1=_mm_mul_ps(t1,t2);
sum=_mm_add_ps(sum,t1);
}
sum=_mm_hadd_ps(sum,sum);
sum=_mm_hadd_ps(sum,sum);
_mm_store_ss(c[i]+j,sum);
//如果还有不能被4整除的部分
for(int k=(n%4)-1;k>=0;k--){
c[i][j]+=a[i][k]*b[j][k];
}
}
}
//转置
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
swap(b[i][j],b[j][i]);
}
分片策略
啥玩意啊
看不懂