SIMD
初始SIMD前言AVX基础介绍矩阵乘加速总结前言完成了计算机系统原理之后来做一个总结,本次实验时继多线程之后继续研究矩阵乘优化的内容,主要使用SIMD指令集并与上次实验的多线程进行对比。SIMD即单指令集多数据(Single Instruction Multiple Data)。说的简单一点就是一种向量运算,想象一下在单指令单数据上你要做一个op需要几步?你得先取指译码,然后访问内存获得操作数,然
前言
完成了计算机系统原理之后来做一个总结,本次实验时继多线程之后继续研究矩阵乘优化的内容,主要使用SIMD指令集并与上次实验的多线程进行对比。
SIMD即单指令集多数据(Single Instruction Multiple Data)。说的简单一点就是一种向量运算,想象一下在单指令单数据上你要做一个op需要几步?你得先取指译码,然后访问内存获得操作数,然后才能进行计算吧,Intel告诉我们可以用SIMD一下子完成128bit,或者256bit,甚至是512bit数据的运算,怎么理解这样的一个数据结构呢,可以把它想象成一个可以同时按位计算的数组,既然是个数组说明也可以通过索引访问数据,这里有个误区是这个数据结构本身表现出来的数没有太多意义,如一个__m128 a存放了4个float类型的数,那么a就是一个类似数组的东西。
AVX基础介绍
MMX和SSE用的不多这里就以AVX为例了如果想看完整的更深层的建议看Intel官网https://software.intel.com/content/www/us/en/develop/home.html
数据类型
数据类型 | 描述 |
---|---|
__m128 | 包含四个float类型数字的向量 |
__m128d | 包含两个double类型数字的向量 |
__m128i | 包含若干个int类型数字的向量 |
__m256 | 包含八个float类型数字的向量 |
__m256d | 包含四个double类型数字的向量 |
__m256i | 包含若干个int类型数字的向量 |
访存操作
数据类型 | 描述 |
---|---|
_mm256_load_ps/pd | 从对齐的内存地址加载浮点向量 |
_mm256_load_si256 | 从对齐的内存地址加载整形向量 |
_mm256_loadu_ps/pd | 从未对齐的内存地址加载浮点向量 |
_mm256_loadu_si256 | 从未对齐的内存地址加载整形向量 |
写入的操作把load换成store即可,这里需要知道SIMD是要求对齐的,可以用align对齐,当然你也可以用上述的不对齐时的操作写代码,但是据我测试似乎时间花费更多。
加减乘
数据类型 | 描述 |
---|---|
_mm256_add_ps/pd | 对两个浮点向量做加法 |
_mm256_sub_ps/pd | 对两个浮点向量做减法 |
_mm256_mul_ps/pd | 对两个浮点向量做乘法 |
_mm256_hadd_ps/pd | 水平方向对两个浮点向量做加法 |
_mm256_hsud_ps/pd | 垂直方向对两个浮点向量做加法 |
这里解释一下水平方向时怎么操作的,如下图。
矩阵乘加速
没错又是我们的老朋友矩阵乘,有了上述的基础知识已经完全可以写出一个入门级的SIMD优化代码了。
int main() {
double* A, * B, * C;
A = new double[M * N];
B = new double[N * P];
C = new double[M * P];
for (int i = 0; i < M; i++) {
for (int j = 0; j < N; j++) {
A[i * N + j] = i * N + j;
}
}
for (int i = 0; i < N; i++) {
for (int j = 0; j < P; j++) {
B[i * P + j] = i * P + j;
}
}
avxmul(A, B, C); //avx+转置
return 0;
}
//avx+转置
void avxmul(double* A, double* B, double* C) {
for (int i = 0; i < M; i++) {
int c = i * P;
for (int j = 0; j < P; j++) {
C[c + j] = 0;
}
}
clock_t start, end;
int i, j, k;
double temp;
start = clock();
double* B1 = T(B);
for (i = 0; i < M; i++) {
for (j = 0; j < P; j++) {
temp = 0; double t[4] = { 0 };
int a = i * N; int b = j * N;
for (k = 0; k < N && k + 4 <= N; k += 4) {
_mm256_store_pd(&t[0],
_mm256_mul_pd(_mm256_load_pd(&A[a + k]), _mm256_load_pd(&B1[b + k])));
temp += t[0] + t[1] + t[2] + t[3];
}
C[i * P + j] = temp;
}
}
end = clock();
cout << "avx+转置时间: " << (double)(end - start) / CLOCKS_PER_SEC
* 1000 << "ms" << endl;
}
总结
本次实验我们以 SIMD(单指令多数据)为主要研究对象,实现了矢量加和矩阵乘的优化。在实验过程中我们不断在探讨 SIMD 与 SIMT(单指令多线程)计算机系统-SIMD 程序设计与性能分析之间的区别与联系。
从单个线程看,假设我们用 SSE 正在处理一个四维向量(指令向量化处理是 SIMD 的核心),这条指令最快在一个 cycle 就能完成,但是在SIMT 架构中则需要把这个四维向量分解开,一个 cycle 处理一个数据,最快也需要 4 个 cycle,但这也需要比起 SIMT 四倍的逻辑单元。换而言之在同等算力进行对比下,其实 SIMD 可以拿出更少的线程个数来实现 SIMT 的算力,这也是为什么在我们的实验中 SIMD 往往优化程度更好的原因。
但是当我们增加线程数的时候,SIMD 的优势将会逐渐减少,而倘若 SIMD 单纯把向量维度扩大时,浪费也会增加,所以现在一些架构会对 16 个线程分成四组的 SIMD 解决这个问题,当然不管是 SIMD 还是 SIMT 都存在不少的局限,SIMD 需要严格按照维数的整数倍去实现,否则会造成逻辑单元的浪费,而 SIMT 则需要在计算机的可以使用的逻辑单元数内实现线程数的创建,否则线程数的增加将不能导致速度加快,反而需要大量的分支跳转影响了效率,这里也体现了算法设计上面的折衷思想,这种折衷既与算法本身有关,又与对应的硬件有关。
经过上面的陈述仿佛 SIMD 和SIMT 存在不同程度的对立,但是这并不意味着 SIMD 就不能与 SIMT 进行“合作优化”,我们设计了同时使用 SIMD 与 SIMT 的算法,算法思路也很简单,只需要思考在多线程下每个线程是否支持 SIMD 即可。
更多推荐
所有评论(0)