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次,也就是说一个时钟周期即是11010s\frac{1}{10^{10}}s,即是0.1ns

  • 机器周期

    由多个时钟周期组成,用来表示流水线阶段的基本单位,所包含时钟周期个数叫做机器周期的时间宽度.如果每个机器周期时间宽度相等则为定长机器周期,反之为变长机器周期

  • 指令周期

    由多个机器周期组成

流水线级数增加对指令执行时间的影响在于增加了很多小的流水段,这些流水段把任务更加细分,实际上是追求机器周期尽可能等于时钟周期*(即是期望CPI->1),从单个指令的完整执行上看,时间是被拉长了的,因为中间的结果你可以要用一些锁存器一类的进行存储,但是指令的吞吐量增大了

比如一个操作4级流水线,每个流水段假设80ns,变成11级流水线,每个流水段30us,整体的执行时间是多了10us,但在无流水线的情况下该指令执行完成需要280us.通过流水线可以看出来明显的增加了吞吐量:280/80≈3.5可以看出来四级流水线相较于无流水线的情况增加了3.5倍的吞吐,若是11级流水线则是280/30≈9.39.3倍的吞吐量的增加!

因此流水线处理的好处在于指令的吞吐增大了非常多,因为我们指令不需要完整等待上一个指令完成才进入.但是单一流水线无论如何优化,它的界限就是IPC=1


指令级并行意味着一个时间周期内可以同时并行执行超过1条指令的能力

为了支持指令级并行,有多发射技术,如下图示:

Multi Issue

多发射并不意味着会有多套重复的流水线部件,但它具备同时并行执行多条指令,也即是执行部件会增多,执行部件被称作FU(functional unit),比如ALU,AGU(address generation unit),BRU(branch unit),FPU等,都是FU中的一种(SIMD的部件也是)

而取指则通过增强取指部件,一次取多条指令,译码器则通过增加部件进行译码,如此来实现多流水线,从而达到IPC>1的效果

从这张图来说,多发射包括静态多发射和动态多发射:

静态多发射即是指的超长指令字(VLIW)一类的方式,一次性将多个指令给它弄成一个指令包,然后在取指译译码的时候都是针对这一个大大指令包,它是静态的在于它是通过编译器来实现多发射,是软件层面的实现而非硬件层面的实现;

动态多发射是指超标量,它通过一次读取多条指令,通过多个译码器进行译码,后续根据执行的时候是乱序的还是顺序再进行细分

以下给出一张A76的微架构图,它的多发射采用的是超标量乱序执行的方式:

A76-micro-architecture

乱序执行

想要同时执行指令,则前后的指令不应有相关性,相关性类型有以下三种:

  • WAR 读后写
  • WAW 写后写
  • RAW 写后读

前面两种可以通过Rename,即重命名寄存器的方式解决,而后面的一种则是真正的依赖,因为不按这个顺序处理你会脏读,它是通过旁路来处理的

还有一种相关性是控制相关性,即分支指令引起的,这是分支预测负责的任务

主要参见参考文件2,没整理好

在理解的时候忽然想到了超线程,即一个核心内,同时有两套寄存器和缓存以保留两份工作现场,这样可以丝滑切换线程.而如果是一个核心内,普通情况下(即只有一套寄存器,缓存)进行多线程,则需要涉及到工作现场的切换,这种多线程(单核心)是不丝滑的

指令重排

一般来说指令重排是发生在编译时期(静态)运行时期(动态)

  • 编译时期的指令重排:当前后指令存在相关性时,顺序执行则会发生阻塞,可以把后续一些无相关性的指令提前执行,以减少阻塞等待的时间.如下:

    1
    2
    3
    a++;
    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 协议 ,转载请注明出处!