开发者
资源
[object Object]

指令双发指的是处理器在同一个时钟周期内,能够同时发射两条指令到执行单元进行处理。这种能力需要满足以下两个条件:

  • 两条指令之间没有数据上的依赖关系(依赖关系指后一条指令需要使用前一条指令产生的结果)
  • 硬件中拥有足够的执行资源

这种机制可以在不改变程序逻辑的前提下,提升处理器在单位时间内的指令处理效率,是实现指令级并行的重要基础之一。

如下示例中,VLoop-1循环16次,由于每个循环内的4条指令有数据依赖,所以执行队列的深度是64。64条指令并发执行顺序如下图所示,LoadAlign_0和LoadAlign_1没有依赖关系,可以并发执行。黑框选中的位置仅代表该4条指令有同时执行的资格,真正执行时是乱序选取其中的2条执行。

[object Object]

图 1 执行队列和指令的执行顺序[object Object][object Object]

在编写算子时,开发者通常习惯于按“加载数据 → 处理计算 → 存储结果”的顺序来组织代码流程。这种写法在寄存器资源充足时运行良好,但一旦资源紧张,问题就会被放大。当多个计算指令之间会产生依赖关系,这些等待会堆积在执行队列中,导致后续指令无法及时发射。

开发者在编程时应尽可能保证队列里存在数目充足且无依赖的并发指令,从而高效的利用硬件双发特性。可以通过合理拆分VF循环以及手动控制循环展开等方案来提高性能。

[object Object]

VF并不是写的越长,把所有运算都放在一个for循环内就好,需要适当的搬出中间结果到UB,减少数据依赖。

优化前:

[object Object]

优化后:

[object Object]
[object Object]

如果循环内存在依赖关系过多的指令,队列没有办法同时加载进for(i = n)和for(i = n+1)的所有指令,那么即使循环之间没有依赖关系,也无法使能双发特性,指令无法并发执行。可以通过手动展开循环,这样做有两个好处:贴近硬件乱序执行的特性,为下发的指令创造更多执行的机会;减少指令因为寄存器资源未到位而产生的等待。

[object Object]

展开后

[object Object]
[object Object]

在同一个VF中,硬件可以同时处理的最大RegTensor寄存器个数为32,如果超出,编译器会进行数据的换入换出并加入同步指令,严重拖慢算子的执行效率。同理,在同一个VF中,MaskReg不超过8个,读/写UnalignRegForLoad和UnalignRegForStore各不超过4个,否则会发生寄存器溢出,导致性能劣化。

优化方案:

  • 使用布尔代数运算。比如!(a && b) 可简化为 !a || !b,!(a || b) 可以化简为 !a && !b。
  • 适当等价调整指令顺序,节省寄存器。

本示例用于判断两个double类型数据是否相等,需要处理两个特殊场景,是否为NAN或者+0和-0的场景。

[object Object]

优化后:

[object Object]