jibinghu

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

向量化编程

基本介绍
X86: Intel x86是英特尔公司于1978年推出的16位微处理器;而x86泛指一系列基于Intel 8086且向后兼容的中央处理器指令集架构

Intel ICC和开源的GCC编译器支持SSE/AVX指令的C语言接口(intrinsic,内置函数),在intrin.h头文件中(头文件可能有所不同)
immintrin.h头文件介绍:https://blog.csdn.net/fuxiaoxiaoyue/article/details/83153667

  • 函数命名:
    第一部分:mm/mm256。mm表示SSE指令集,操作长度为64位或128位。mm256表示使用AVX指令集、操作位位256位。
    第二部分:操作函数名称——如 add、load、mul....
    第三部分:操作对象及数据类型— ps表示操作向量数据为单精度、pd表示操作的向量数据为双精度等。

  • 函数命名举例(AVX2):
    _mm256_add_ps:使用AVX 256位寄存器,进行加法操作,操作的向量数据位单精度。
    _mm256_mul_pd:使用AVX 256位寄存器,进行乘法操作,操作的向量数据类型为双精度。

gcc -march=native -Q --help=target|grep march
或
cat /proc/cpuinfo

AVX编程初识->https://blog.csdn.net/nbu_dahe/article/details/122157205

AVX向量化编程
简单向量加:

#include<bits/stdc++.h>
#include<immintrin.h>
#include<omp.h>
using namespace std;

typedef chrono::high_resolution_clock Clock;

void out(int D[])
{
  for(int i=0;i<8;i++)
  {
    cout<<D[i]<<" ";
  }
  cout<<endl;
}
​
int main()​
{
  __m256i x; // 声明m256变量
  __m256i y;
  __m256i z;
​
  int A[8]={1,2,3,4,5,6,7,8};
  int B[8]={1,2,3,4,5,6,7,8};
  int ans[8];
​
  x = _mm256_loadu_si256((__m256i*)&A[0]); // 载入
  y = _mm256_loadu_si256((__m256i*)&B[0]);
  z = _mm256_add_epi64(x,y);  // 并行加法实现
​
_mm256_storeu_si256((__m256i*)&ans[0],z); // 回存
​
  out(A);
  out(B);
  out(ans);
​
  return 0;
}

以矩阵乘为例介绍AVX编程

#include <bits/stdc++.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>
#include <immintrin.h>
struct matrix {
	double* data;
	int rows;
	int cols;
};
struct matrix* matrix_create(int rows, int cols)
{
	struct matrix* m = (struct matrix*)malloc(sizeof(struct matrix));
	m->rows = rows;
	m->cols = cols;
	m->data = (double*)malloc(sizeof(double) * rows * cols);
	return m;
}
void matrix_destroy(struct matrix* m)
{
	free(m->data);
	free(m);
}
void matrix_random_init(struct matrix* m)
{
	for (int i = 0; i < m->rows * m->cols; i++)
	{
		m->data[i]=(double)rand();
		//m->data[i] = (double)rand() / RAND_MAX;
	}
}
void random_init()
{
	srand(time(NULL));
	//time(NULL)用来获取当前的系统时间,本质上得到的是一个大整数
	//srand()是一个设置随机数种子的函数,一般在调用rand()之前使用 
}
int check(struct matrix* a, struct matrix* b)
{
	if (a->rows != b->rows || a->cols != b->cols)
	{
		printf("A错误");
		return 0;
	}
	for (int i = 0; i < a->rows * a->cols; i++) 
	{
		if (a->data[i] != b->data[i]) 
		{
			printf("B错误:%d\na[i]=%d,b[i]=%d\n",i,a->data[i] , b->data[i]);
			return 0;
		}
	}
	return 1;
}
struct matrix* matrix_multiple(struct matrix* a, struct matrix* b)
{
	struct matrix* c = matrix_create(a->rows, b->cols);
	struct timeval start, end;
	gettimeofday(&start, NULL);
	for (int i = 0; i < a->rows; i++)
	{
		for (int j = 0; j < b->cols; j++)
		{
			double sum = 0;
			for (int k = 0; k < a->cols; k++) 
			{
				sum += a->data[i * a->cols + k] * b->data[k * b->cols + j];
			}
			c->data[i * c->cols + j] = sum;
		}
	}
	gettimeofday(&end, NULL);
	//gettimeofday是一个系统调用,用于获取当前时间戳。在C语言中,可以使用gettimeofday(&end, NULL)来获取当前时间戳并存储在变量end中。这个函数返回的时间戳是一个Timeval结构体,其中tv_sec表示时间戳的秒数部分,tv_usec表示时间戳的微秒部分。通过将时间戳转换为Timeval结构体,可以方便地比较两个时间戳之间的差异。
	//需要注意的是,gettimeofday()函数返回的时间戳是相对于系统启动时间的,因此可能不是真正的时间戳
	//总之,使用gettimeofday()函数可以方便地获取当前时间戳,但需要注意时间戳的精度和准确性
	printf("before optimize: %ld\n", (end.tv_sec - start.tv_sec) * 1000000 +end.tv_usec - start.tv_usec);
	return c;
}
struct matrix* matrix_multiple_optimize(struct matrix* a, struct matrix* b) 
{
    struct matrix* c = matrix_create(a->rows, b->cols);
    struct timeval start, end;
    
    gettimeofday(&start, NULL);
    for (int i = 0; i < a->rows; i++)
	{
        for (int j = 0; j < b->cols; j++)
		{
            __m256d sum = _mm256_setzero_pd();
            int k=0;
            for (; k < a->cols-a->cols%4; k += 4) {
                __m256d a_vec = _mm256_loadu_pd(&a->data[i * a->cols + k]);
                __m256d b_vec = _mm256_set_pd(b->data[(k+3)*b->cols+j],b->data[(k+2)*b->cols+j],b->data[(k+1)*b->cols+j],b->data[k*b->cols+j]);//注意这里的数组元素地址不连续,且顺序是相反的
                __m256d prod = _mm256_mul_pd(a_vec, b_vec);
                sum = _mm256_add_pd(sum, prod);
            }
            double result[4]={0.0};
            _mm256_storeu_pd(result, sum);
            for(;k<a->cols;k++)
            {
            	result[0]+=a->data[i * a->cols + k] * b->data[k * b->cols + j];
			}
			c->data[i * c->cols + j]=result[0] + result[1] + result[2] + result[3];
        }
    }
    gettimeofday(&end, NULL);
    printf("after optimize: %ld\n", (end.tv_sec - start.tv_sec) * 1000000 + end.tv_usec - start.tv_usec);
    return c;
}
int main() {
	random_init();
	struct matrix* m = matrix_create(100, 2000);
	matrix_random_init(m);
	struct matrix* n = matrix_create(2000, 100);
	matrix_random_init(n);
	struct matrix* r = matrix_multiple(m, n);
	struct matrix* r_optimize = matrix_multiple_optimize(m, n);
	if (check(r, r_optimize)) 
	{
		printf("check ok\n");
	} else 
	{
		printf("check failed\n");
	}
	matrix_destroy(m);
	matrix_destroy(n);
	
	matrix_destroy(r);
	matrix_destroy(r_optimize);
	return 0;
}

编译指令:g++ avx-add.cpp -o avx-add -mavx -mavx2 && ./avx-add
__mm256_loadu_si256:加载数据到寄存器上,起始坐标为数组的开头,即A[0];
__mm256_add_epi64:相加操作,即寄存器对应位置数据进行相加。
__mm256_storeu_si256:寄存器数据写入到结果数组Z内。
ARM Neon技术处理SIMD编程: https://blog.csdn.net/Selenitic_G/article/details/106565566

  • 在移动平台上进行一些复杂算法的开发,一般需要用到指令集来进行加速。NEON 技术是 ARM Cortex™-A 系列处理器的 128 位 SIMD(单指令,多数据)架构扩展,专门针对大规模并行运算设计的,是一个实现ARMv7-A或ARMv7-R架构的系列处理器。ARM NEON技术建立在SIMD的概念上,支持128位向量操作,也称为单指令多数据向量模式。 其本质上使用的是128位NEON SIMD寄存器,这意味着如果操作32位浮点数,可同时操作4个(变量可定义:float32x4_t);如果操作 16 位整数(short),可同时操作 8 个(变量可定义:int16x8_t);而如果操作 8 位整数,则可同时操作 16 个(变量可定义:int8x16_t)。
    ARMv7 NEON 指令集架构具有 16 个 128 位的向量寄存器,命名为 q0~q15。这 16 个寄存器又可以拆分成 32 个 64 位寄存器,命名为 d0~d31。其中qn和d2n,d2n+1是一样的,故使用汇编编写代码时要注意避免产生寄存器覆盖。

float32x4_t vdupq_n_f32 (float32_t value)
将value复制4分存到返回的寄存器中
float32x4_t vld1q_f32 (float32_t const * ptr)
从数组中依次Load4个元素存到寄存器中
float32x4x2_t vld2q_f32 (float32_t const * ptr)
从数组中依次Load8个元素存到两个寄存器中
相应的 有void vst1q_f32 (float32_t * ptr, float32x4_t val)
将寄存器中的值写入数组中
float32x4_t vaddq_f32 (float32x4_t a, float32x4_t b)
返回两个寄存器对应元素之和 r = a+b
float32_t vaddvq_f32 (float32x4_t a)
返回a向量寄存器中元素之和给一个新的标量
相应的 有float32x4_t vsubq_f32 (float32x4_t a, float32x4_t b)
返回两个寄存器对应元素之差 r = a-b
float32x4_t vmulq_f32 (float32x4_t a, float32x4_t b)
返回两个寄存器对应元素之积 r = ab
float32x4_t vmlaq_f32 (float32x4_t a, float32x4_t b, float32x4_t c)
返回乘加值r = a +b
c
float32x2_t vmaxq_f32 (float32x4_t a, float32x4_t b)
返回向量a、b对应通道的最大值给存入到一个新的向量中
float32_t vmaxvq_f32 (float32x4_t a)
求出向量a中的最大值返回给一个新的标量

  • 通过一个示例来解释如何利用NEON内置函数来加速实现统计一个数组内的元素之和。
#include <iostream>
#include <arm_neon.h> //需包含的头文件
using namespace std;
/*
本质上是对寄存器进行处理(128位NEON SIMD寄存器)
*/
float sum_array(float *arr, int len)
{
    if(NULL == arr || len < 1)
    {
        cout<<"input error\n";
        return 0;
    }
 
    int dim4 = len >> 2; // 数组长度除4整数
    int left4 = len & 3; // 数组长度除4余数
    float32x4_t sum_vec = vdupq_n_f32(0.0);//定义用于暂存累加结果的寄存器且初始化为0
    for (; dim4>0; dim4--, arr+=4) //每次同时访问4个数组元素
    {
        float32x4_t data_vec = vld1q_f32(arr); //依次取4个元素存入寄存器vec
        sum_vec = vaddq_f32(sum_vec, data_vec);//ri = ai + bi 计算两组寄存器对应元素之和并存放到相应结果
    }
    //将累加结果寄存器中的所有元素相加得到最终累加值
    float sum = vgetq_lane_f32(sum_vec, 0)+vgetq_lane_f32(sum_vec, 1)+vgetq_lane_f32(sum_vec, 2)+vgetq_lane_f32(sum_vec, 3);
    for (; left4>0; left4--, arr++)
        sum += (*arr) ;   //对于剩下的少于4的数字,依次计算累加即可
    return sum;
}

程序中函数解释:

float32x4_t vdupq_n_f32(float32_t val):将val复制四份放入返回的寄存器中。
float32x4_t vld1q_f32(float32_t const * ptr):从地址ptr依次向后加载四个元素放入返回的寄存器中。
float32x4_t vaddq_f32(float32x4_t a, float32x4_t b):返回a+b的值,向量运算,四个值同时相加。
float32_t vgetq_lane_f32(float32x4_t v, const int lane):返回v中某一个lane的值

向量点乘实现:

void neonax(float A[][1000],float *x,int len)
{
	double t2,t1;
	double y[len];
	float32x4_t vec1,vec2,sum_vec = vdupq_n_f32(0);
	for(int i=0;i<len;i++)
	{
		for(int j=0;j<len;j+=4)
		{
			vec1 = vld1q_f32(A[i]+j); // 寄存器vec1和vec2每次可以取4个float值
			vec2 = vld1q_f32(x+j);
			sum_vec = vmlaq_f32(sum_vec,vec1,vec2); // 使用vmlaq_f32函数进行向量乘
		}
		y[i]=vgetq_lane_f32(sum_vec,0)+vgetq_lane_f32(sum_vec,1)+vgetq_lane_f32(sum_vec,2)+vgetq_lane_f32(sum_vec,3);
		sum_vec=vdupq_n_f32(0);
	}
}

向量加实现:

void add_neon1(float *c, float *a, float *b,int count)
{
	int i;
	float32x4_t in1,in2,out;
	for(i=0;i<count;i += 4)
	{
		in1 = vld1q_f32(a);
		a += 4;
		in2 = vld1q_f32(b);
		b += 4;
		out = vaddq_f32(in1, in2);
		vst1q_f32(c,out);
		c +=4;
	}	
}

自动向量化:

  • 在GCC中启用自动向量化使用命令行选项:
    -ftree-vectorize
    -mfpu=neon
    -mcpu 来指定核心或架构。
    在优化级编译-O3意味着-ftree向量化。如果没有指定-mcpu选项,那么GCC将使用其内置的默认内核。

手动向量化:(主要为使用下图中的intrinsics内联函数方式):

另附参考资料:Neon内置函数:https://blog.csdn.net/billbliss/article/details/78488013
Neon常用函数总结:https://blog.csdn.net/may0324/article/details/72847800
Neon并行化技术简介:https://blog.csdn.net/qq_39589936/article/details/109154895?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-1-109154895-blog-129000993.235^v43^pc_blog_bottom_relevance_base9&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-1-109154895-blog-129000993.235^v43^pc_blog_bottom_relevance_base9&utm_relevant_index=2
intel指令集doc官网:https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html
[x86]SIMD指令集发展历程表(MMX、SSE、AVX等):https://www.cnblogs.com/zyl910/archive/2012/02/26/x86_simd_table.html
AVX指令集基本介绍:https://blog.csdn.net/m0_55063425/article/details/128603137

Pytorch初上手:

参考资料:
pytorch上手简要:https://www.elecfans.com/d/883656.html
pytorch中文官方文档:https://pytorch.apachecn.org/2.0/tutorials/beginner/basics/quickstart_tutorial/

posted on 2024-04-07 16:23  zombie_black  阅读(240)  评论(0编辑  收藏  举报