OperatorDev常见优化方案
本文最后更新于:8 个月前
内联函数
减少函数调用带来的栈帧开销,适用于那些简单的函数
循环展开
对于GPU来说,没有分支预测功能,循环展开可以帮助它少做判断,少走分支,规避掉warp divergence.
比如for(int i=0;i<tid;i++){...} // 其中tid代表block内的线程号
可以看出对于[0,31]这一个warp内的线程来说,有很严重的线程束分化.通过循环展开的话,可以减少条件判断的次数;
分块打包
分块的意义在于充分的利用其多层缓存机制,数据放在正确的位置,可以在存取的时候减少延迟,同时可以使得数据被复用;
打包又叫数据重排,把数据放在连续的内存地址,既有利于预取,还有利于少占用TLB条目的数量
指令级并行(ILP)
指令执行是在不同CPU部件上的,单一的串行执行不能很好发挥各部件的性能,为了使得各部件一直处于繁忙状态,由此出现了流水线技术.经典的五级流水线结构是:取指(Instruction Fetch),译码(Instruction Decode),执行(EXE),访存(MEM),写回(Write Back).各部分的详细功能如下:
- 取指: 从L1-ICache中取出指令;
- 译码: 将取出的指令按照相应架构的指定读取方式进行翻译,读出操作数,操作类型等;
- 执行: 交由具体的功能单元如ALU来执行相应操作;
- 访存: 从内存中取数据到寄存器(load)或将数据写入内存中(store),这里是L1-DCache;
- 写回: 将指令执行结果写回RegisterFile中去
CPI(Cycle Per Instruction):每个指令的时钟周期数
IPC(Instruction Per Cycle):每个时钟周期能流出的指令数
-
时钟周期
最小的时间单位,也叫振荡周期,也叫节拍,以主频10GHZ为例,表示1s内能振荡1010次,也就是说一个时钟周期即是,即是0.1ns
-
机器周期
由多个时钟周期组成,用来表示流水线阶段的基本单位,所包含时钟周期个数叫做机器周期的时间宽度.如果每个机器周期时间宽度相等则为定长机器周期,反之为变长机器周期
-
指令周期
由多个机器周期组成
流水线级数增加对指令执行时间的影响在于增加了很多小的流水段,这些流水段把任务更加细分,实际上是追求机器周期尽可能等于时钟周期*(即是期望CPI->1),从单个指令的完整执行上看,时间是被拉长了的,因为中间的结果你可以要用一些锁存器一类的进行存储,但是指令的吞吐量增大了
比如一个操作4级流水线,每个流水段假设80ns,变成11级流水线,每个流水段30us,整体的执行时间是多了10us,但在无流水线的情况下该指令执行完成需要280us.通过流水线可以看出来明显的增加了吞吐量:280/80≈3.5
可以看出来四级流水线相较于无流水线的情况增加了3.5倍的吞吐,若是11级流水线则是280/30≈9.3
9.3倍的吞吐量的增加!
因此流水线处理的好处在于指令的吞吐增大了非常多,因为我们指令不需要完整等待上一个指令完成才进入.但是单一流水线无论如何优化,它的界限就是IPC=1
指令级并行意味着一个时间周期内可以同时并行执行超过1条指令的能力
为了支持指令级并行,有多发射技术,如下图示:

多发射并不意味着会有多套重复的流水线部件,但它具备同时并行执行多条指令,也即是执行部件会增多,执行部件被称作FU(functional unit),比如ALU,AGU(address generation unit),BRU(branch unit),FPU等,都是FU中的一种(SIMD的部件也是)
而取指则通过增强取指部件,一次取多条指令,译码器则通过增加部件进行译码,如此来实现多流水线,从而达到IPC>1
的效果
从这张图来说,多发射包括静态多发射和动态多发射:
静态多发射即是指的超长指令字(VLIW)一类的方式,一次性将多个指令给它弄成一个指令包,然后在取指译译码的时候都是针对这一个大大指令包,它是静态的在于它是通过编译器来实现多发射,是软件层面的实现而非硬件层面的实现;
动态多发射是指超标量,它通过一次读取多条指令,通过多个译码器进行译码,后续根据执行的时候是乱序的还是顺序再进行细分
以下给出一张A76的微架构图,它的多发射采用的是超标量乱序执行的方式:
乱序执行
想要同时执行指令,则前后的指令不应有相关性,相关性类型有以下三种:
- WAR 读后写
- WAW 写后写
- RAW 写后读
前面两种可以通过Rename,即重命名寄存器的方式解决,而后面的一种则是真正的依赖,因为不按这个顺序处理你会脏读,它是通过旁路来处理的
还有一种相关性是控制相关性,即分支指令引起的,这是分支预测负责的任务
主要参见参考文件2,没整理好
在理解的时候忽然想到了超线程,即一个核心内,同时有两套寄存器和缓存以保留两份工作现场,这样可以丝滑切换线程.而如果是一个核心内,普通情况下(即只有一套寄存器,缓存)进行多线程,则需要涉及到工作现场的切换,这种多线程(单核心)是不丝滑的
指令重排
一般来说指令重排是发生在编译时期(静态)和运行时期(动态)
-
编译时期的指令重排:当前后指令存在相关性时,顺序执行则会发生阻塞,可以把后续一些无相关性的指令提前执行,以减少阻塞等待的时间.如下:
1
2
3a++;
b = a + 2;
c--;则b对于a是有数据依赖(RAW)的,必须等
a++
执行完才才能执行b的语句,若是顺序执行,则在五级流水线中,当第二条语句处于exe的流水段时,第一条语句是处于mem流水段,并没有到wb到相应的寄存器文件中去,所以此时会发生流水线停顿.而通过指令重排,则可以规避掉这个停顿注:实际上,上面的这个例子并不太好,因为处理器设计的时候考虑到了这个问题,有一个叫做forwarding的前递设计,会把执行后的结果传递给有依赖的指令
但并不是说指令重排就不重要了,事实上比如
a=b+c
,以汇编的形式写出就是1
2
3
4
5
6
7
8
9
10__asm__ volatile(
ldr w0,[%b] // IF ID EXE MEM WB
ldr w1,[%c] // IF ID EXE MEM WB
add w2,w0,w1 // IF ID EXE(×) EXE WB
str w2,[%a]
: [a] "+r"(a)
: [b] "+r"(b),
[c] "+r"(c)
: "cc","memory","w0","w1","w2"
);从上面的内联汇编中可以看出,这样会导致流水线停顿,因此可以把后面不相关的指令提前(则重排了指令)以规避停顿的问题
-
运行时期的指令重排即是上面所说的乱序执行,是动态的,由硬件控制的
从单线程的角度出发,指令重排并不会对最终的结果产生什么影响,它的结果和顺序执行是一样的,目的只是为了优化流水线并行,提升程序的执行效率
参考文件
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!