zbtrs / recruitment-2022-spring

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

七边形战队春招题目解答

设备信息

笔记本: ROG幻14 2020款 CPU: AMD Ryzen7 4800HS with Radeon Graphics 八核 内存: 16G (镁光 DDR4 3200MHz)

Task1 multi-thread

  1. 先学习了一下makefile,通过makefile输出的信息来推测出任务中的每个文件大概有什么用以及依赖关系。
  2. 阅读main.cpp代码,弄清楚了整个代码的逻辑,在mandelbrotThread.cpp中找到了需要完善的函数. 这个函数提示我们可以用一个线程来计算上半部分另一个线程来计算下半部分。
  3. 顺着提示想,如果现在要用8个线程来优化,就可以把整个区域横着切割成8块,每个线程负责计算其中一块,测试下来发现远没有达到6x的加速比。
  4. 按照任务的提示,输出每个线程的运行时间,发现中间线程的运行时间远大于两侧线程的运行时间。分析得知是中间白色的点太多,计算量太大。如果要进一步优化就最好是分配给每个线程的计算量都要平均。
  5. 很自然的思路:给第i个线程分配第8k+i行,也就是这些行分别分配给线程0,1,2,3,4,5,6,7,0,1,2,...... 两个任务加速比均达到7x,完成任务。

坑: 虚拟机分配cpu分配错了,分配了8个处理器和1个核......导致一开始不管怎么优化都是1.8x加速比.

Task2 SIMD

  1. 还是先看makefile文件,这个任务主要的代码都在main.cpp中.
  2. 上网查了一下SIMD的概念,感觉这玩意比较像循环展开,就是一次计算多个值。
  3. 好在这个任务把SIMD指令给封装了起来,看了那个都是声明的头文件,注释还蛮清楚,但是不知道他这些函数类型到底要怎么用...
  4. 在main.cpp中发现了abs函数,通过这个函数知道了这个伪SIMD指令要怎么用,照葫芦画瓢一步步搞出来了...
  5. 比较坑的就是N不是向量长度倍数的情况,多出来的部分直接调用暴力写法吧...

Task3 GEMM

  1. 数据是用vector存的,不开O2的话会比较慢,转成int数组来存.
  2. 想起了csapp第六章最后一节关于矩阵乘法的一个优化,可以改变ijk的顺序,使得访存是连续的,感觉效果不大?加速效果可以忽略不计。
  3. 为了之后的SIMD向量化,对算法的实现步骤做了一些调整,例如,对于矩阵C的每一个元素的计算,朴素实现中每次只是拿矩阵A的一行去点乘矩阵B的一列。而如果我们同时算C中的4个元素,那么我们就能同时取A中的连续4行与B中的连续4列。这样连续取整块的内存是有利于后面的向量化的。具体而言,我们每次同时计算C的4x4的16个元素,这样的话我们同时用到了A的连续4行与B的连续4列。但是类似前一种优化,如果我们只是调整一下形式,不做其他优化的话,性能几乎不会有变化。
  4. 用csapp第五章中提到过的一个方法:尽量用临时变量来存值而不是频繁访存. 可以先将16个C中的元素都先用寄存器暂存,然后累加时使用寄存器,最后再写入内存。具体来说,就是每一次内层循环,A和B都只用了4个元素,那么可以先用4个寄存器把这些元素从内存中加载进来,然后使用寄存器计算. 减少访存后,加速比5.5x,是很大的提升。
  5. 接下来直接SIMD向量化,和上一题高度封装的指令不同,这里不同的SSE指令有一些区别。一开始想着用_mm_load_si128指令来载入值到si128中,但是发现总是段错误,看了intel官方的文档才知道这里要16字节对齐,也就是每次必须取连续的4个数,但是这并不符合我们的要求,后来反复查阅文档才终于找到了一个能用的loadu指令,解决了读入问题.
  6. 总之用SIMD指令的时候出了各种各样的问题,比如想用乘法指令用成了_mm_mul_epi32,这将两个32位的int相乘的结果保存为一个64位的整数,看文档还是要好好看那些名字相近的函数啊。。。改成_mm_mullo_epi32才对. 而且也还出现了无法编译的问题,查阅google才知道要加选项-sse2,因为用到了sse4.1的指令,还要加-sse4.1. csdn真的害死人了,**公司快倒闭吧。
  7. 一个性能瓶颈: 一次load比四次set要慢,但是这里我找不到能替代set指令的其他指令,只能暂时用这个指令了。在网上看别人代码实现,发现别人是把矩阵定义为(N + delta) * N大小的,也就是原来二维矩阵的第i行第j列个元素映射到这个一维数组上就是第i * (N + delta) + j个. 这个优化放到我的程序上瞬间加速了2倍,可能是内存对齐的原因?
  8. 感觉csapp里面有好多知识看完一遍就忘了,周末又把csapp的第五章和第六章回顾了一遍,算是对cache,访存这些知识有了更深的理解吧.
  9. 之前的方法在数据规模变大后性能都会有较大的下降,问题在于L2 Cache大小有限,如果不停地访存很快会把L2 Cache刷满一遍。因此应该让高密度计算中的访存范围集中,使用Blocked分块的方法。采取的分块策略是A每次访问MCxKC的块,B每次访问KC x ldb的块。其中MC和KC的大小进行多次尝试决定。分块后加速比为13x,但是小数据反而变慢了一点点,矩阵大小本来就不算大,对大矩阵优化效果更好一些。
  10. 每次对A进行4x1的访存时,是对4行同一列元素进行的访存,但是每次循环就会跳到下一列,由于是列主序的矩阵,循环间访存不是连续的,最好将所有循环会访问的元素排到一起,可以减少跨区域访存的次数,增加数据在一条Cache Line中的概率。因此对A进行了4行元素的内存重排。每次对B是进行1x4的访存,每次对同一行四列的元素访问,我的代码为了方便SIMD运算,采用了列主序,一次循环内的访存是不连续的,可以将4列的元素重排到一起。加速比为17x
  11. 进一步的想法:感觉SIMD的set指令还是个瓶颈,内层循环有5次访存和4次乘加指令,访存运算比过高了。想着有没有什么指令可以替换,在文档上找了好久都没找到能用的指令,寄!
  12. 最后一步就是用openmp来实现并行优化了。这个对于没有内存重排的写法而言优化起来比较简单,直接加一条语句就好了,因为几个for循环都堆在一起,也不存在什么变量共享引发的冲突之类的问题。但是如果用openmp来优化我最后写的那个内存重排的优化,就出现了各种问题,一是结果不对,就算不考虑结果是否正确,运行时间也慢了好多,这一块暂时还不知道是什么原因,如果我对每一小块计算用openmp优化,还是变慢了。。。寄!

最终优化效果: 不加优化:

Running, dataset: size 256
time spent: 101841us
Passed, dataset: size 256

Running, dataset: size 512
time spent: 904292us
Passed, dataset: size 512

Running, dataset: size 1024
time spent: 1.72648e+07us
Passed, dataset: size 1024

Running, dataset: size 2048
time spent: 2.75832e+08us
Passed, dataset: size 2048

优化:

Running, dataset: size 256
time spent: 5484us
Passed, dataset: size 256

Running, dataset: size 512
time spent: 44395us
Passed, dataset: size 512

Running, dataset: size 1024
time spent: 342135us
Passed, dataset: size 1024

Running, dataset: size 2048
time spent: 2.90118e+06us
Passed, dataset: size 2048

About


Languages

Language:C++ 98.4%Language:Makefile 1.3%Language:Shell 0.3%