优化计算10^6个整数数的向量之和
1.问题描述
- 在第一次课程中,早起单节点计算系统并行的粒度分为:Bit级并行、指令级并行和线程级并行。现代处理器如Intel、ARM、Power以及国产CPU如华为鲲鹏等均包含了并行指令集
- 请调查这些处理器中的并行指令集,并选择其中一种进行编程练习,计算两个各包含10^6个整数的向量之和
- 现代操作系统为了发挥多核的优势,支持多线程并行编程模型,请将问题1用多线程的方式实现,线程实现的语言不限
- 参考实现:
- click here https://github.com/chen0031/AVX-AVX2-Example-Code
- click here https://software.intel.com/en-us/articles/using-intel-avx-without-writing-avx/
- click here https://www.tutorialspoint.com/java/java_multithreading.htm
2.解决方案
问题一:
- SIMD(Single-Instruction,Multiple-Data)单指令多数据流技术:
运用SIMD技术同时计算第一条和第二条指令。一条指令处理了4个操作数。SIMD指令集可以提供更快的图像、声音、视频数据等运行速度e = a + b f = c + d m = e*f
- Intel AVX指令集,在单指令多数据流计算性能增强的同时也沿用了的MMX/SSE指令集。不过和MMX/SSE的不同点在于增强的AVX指令,从指令的格式上就发生了很大的变化。AVX是在之前的128位扩展到和256位的单指令多数据流.
- 使用AVX指令级,并行执行8个矢量加法,实现优化计算;而往常我们都是用一层
for
循环实现,通过对比可以看出串行与并行计算的区别及差距,下面是指令级并行的部分函数代码,用于实现8个向量同时相加clock_t sum_avx(int *arr1, int *arr2, int *result) { int i = 0; int j = 0; for(i = 0; i < __VEC_LENGTH__; i += 8) { vecarr1[i / 8] = _mm256_setr_ps(arr1[i], arr1[i + 1], arr1[i + 2], arr1[i + 3],arr1[i+4], arr1[i + 5], arr1[i + 6], arr1[i + 7]); vecarr2[i / 8] = _mm256_setr_ps(arr2[i], arr2[i + 1], arr2[i + 2], arr2[i + 3],arr1[i+4], arr1[i + 5], arr1[i + 6], arr1[i + 7]); } struct timeval tv1, tv2; gettimeofday(&tv1, NULL); for(i = 0; i < __VEC_LENGTH__ / 8; ++i) { temp[i] = _mm256_add_ps(vecarr1[i], vecarr2[i]); } gettimeofday(&tv2, NULL); for(i = 0; i < __VEC_LENGTH__ / 8; ++i) { result[i / 8] = temp[i][0]; result[i / 8 + 1] = temp[i][1]; result[i / 8 + 2] = temp[i][2]; result[i / 8 + 3] = temp[i][3]; result[i / 8 + 4] = temp[i][4]; result[i / 8 + 5] = temp[i][5]; result[i / 8 + 6] = temp[i][6]; result[i / 8 + 7] = temp[i][7]; } return tv2.tv_usec - tv1.tv_usec; }
问题二:
- 什么是线程:
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。 一个进程可以有很多线程,每条线程并行执行不同的任务。
- 线程的优点:
线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O等会产生堵塞的情况的表现性能。在很多情况下,完成相关任务的不同代码间需要交换数据。如果采用多进程的方式,那么通信就需要在用户空间和内核空间进行频繁的切换,开销很大。但是如果使用多线程的方式,因为可以使用共享的全局变量,所以线程间的通信(数据交换)变得非常高效
- 采用C语言实现多线程用于优化计算,部分代码如下:
gettimeofday(&tv1, NULL); // 绑定线程,开始分叉 for (thread = 0; thread < thread_count; ++thread) pthread_create(&thread_handles[thread],NULL,sum_vec,(void*)thread); //线程hello,和给hello的参数绑定到线程上 // 结束线程 for (thread = 0; thread < thread_count; ++thread) pthread_join(thread_handles[thread], NULL); gettimeofday(&tv2, NULL); // 输出高维加法执行时间 printf("Spliting the date to 4 pieces takes %ld us\n", (tv2.tv_usec - tv1.tv_usec)); printf("The first three elements of result is %lf %lf %lf\n", result[0], result[1], result[2]); free(thread_handles);
3.实验结果
问题一的结果:
cal_way | run_time1 | run_time2 | run_time3 |
---|---|---|---|
series | 41284us | 41284us | 49970us |
AVX_pal | 19990us | 29982us | 19990us |
经过多次计算发现,加速比大约为2左右
问题二的结果:
cal_way | run_time1 | run_time2 | run_time3 |
---|---|---|---|
series | 59968us | 59965us | 60002us |
Mul_thr_pal | 39979us | 19989us | 29951us |
经过多次计算发现,加速比大约为2.2左右
4.遇到的问题及解决方法
1. 刚开始AVX优化后的运行时间比串行循环的运行时间还要长
-
在进行一些简单运算时,编译器会在你没有主动使用SIMD的情况下通过一些优化技术自动转换为SIMD的代码,所以你自己写的使用SIMD的代码最快也不会超过编译器自动优化的结果。编译器编译时默认会有-O1优化,所以编译时时需要加上-O0
-
以加法指令为例,单指令流单数据流(SISD)型CPU对加法指令译码后,执行部件先访问主存,取得第一个操作数,之后再一次访问主存,取得第二个操作数,随后才能进行求和运算;而在SIMD型CPU中,指令译码后,几个执行部件同时访问主存,一次性获得所有操作数进行运算,而其转存的时间大约占到30%,这额外的开销是比较大的,对于这种简单加法,使用SIMD带来的计算上的效率提升,掩盖不了使用SIMD带来的额外性能损失,可能计算比较复杂时SIMD才有加速的效果,故计算时间转存的时间不加进去
2. 我的电脑是4核8线程,但在多线程优化计算中分成8线程和4线程的效果差不多
-
四核八线程是指使用了超线程技术 , 把一个物理核心模拟成两个逻辑核心, 理论上要像八颗物理核心一样在同一时间执行八个线程,所以设备管理器和任务管理器中会显示出八个核心,但事实上并不是真正的八个核心,四核八线程就是真四核,虚拟八核
-
四核八线程在有些情况下比如任务量不大能让CPU利用率提高很多从而使其性能接近八核CPU的水平,而在另外一些情况比如CPU占用100%满负荷工作的情况下,这时候四核八线程和八核的性能表现差距明显,其实质就是虽然采用超线程技术能同时执行两个线程,但它并不象两个真正的CPU那样,每个CPU都具有独立的资源。当两个线程都同时需要某一个资源时,其中一个要暂时停止,并让出资源,直到这些资源闲置后才能继续。因此超线程的性能并不等于两颗CPU的性能。这也是四核八线程和八核的最大区别
对了这里需要注意的一点是,在使用gcc编译带有immintrin.h头文件函数的代码时,根据你使用的函数类别,需要添加相应的编译选项(老的SSE是默认支持的通常不需要额外添加选项),如AVX2.0就需要使用-mavx2、FMA需要使用-mfma等等,否则编译会报错