Page 102 - 《软件学报》2024年第6期
P. 102

2678                                                       软件学报  2024  年第  35  卷第  6  期


                 访存和特殊指令的权重系数, 为优化分析模型提供量化数据来选择合适的优化算法, 进而完成代码生成. 为了聚焦
                 于     AutoConfig  的设计思想, 本文只使用单指令多数据流      SIMD  作为示例进行论述.
                 5.1   硬件信息的静态提取
                    静态信息提取根据计算负载的优化分析模型的需求进行采集, 结合这些硬件信息以及低层编译器的代码生成
                 策略可以分析出加速指令的使用和额外的开销. 面向 CPU 的优化需要利用                       SIMD  的加速能力并且使用 Cache 降
                 低访存开销, 因此本文展示的静态信息提取主要包括收集                    SIMD  寄存器堆的尺寸和 Cache     的信息. 配合生成的
                 LLVM  代码和汇编代码进行静态分析.

                 5.1.1    SIMD  寄存器关键信息提取与分析
                    SIMD  计算单元的关键信息包括         SIMD  寄存器长度和    SIMD  寄存器堆的容量. SIMD     寄存器的长度标志着一
                 条  SIMD  指令能够操作的元素个数. SIMD       寄存器堆的容量标志着一个          SIMD  中间表示语句能够使用的最大非溢
                 出尺寸. 使用这两个硬件信息能够结合编译器使用的                 SIMD  抽象尺寸分析出计算的内存溢出次数.
                    编译策略选择的主要的挑战在于向量中间表示参数和实际机器关键信息之间存在差距. MLIR                                  和  LLVM
                 zmm
                 IR  的向量抽象提供了灵活的语义, 并且根据后端硬件平台生成                   SIMD  代码, 这意味着中间表示层面有更强的向
                 量语义的表达能力来承载计算负载的编译优化, 同时能够避免在多个后端重复实现优化算法. 然而, 物理机器
                 上的  SIMD  寄存器的长度和数量是固定的, 需要生成多条               SIMD  指令来弥合向量中间表示到           SIMD  器件的鸿
                 沟. 一旦中间表示层面使用的向量抽象尺寸超过了                  SIMD  寄存器堆的容量, SIMD     寄存器数量不足以完成所有
                 的计算, 此时汇编代码生成器就会将生成访存指令, 将寄存器中的元素溢出到内存, 腾挪出空闲寄存器完成计
                 算, 当溢出的元素被后续计算使用时, 再将这部分元素从内存中取回到                        SIMD  寄存器, 这就造成了访存操作的
                 额外开销.
                    在向量中间表示的转换过程中, 为了兼容多种后端硬件平台, 向量抽象的长度和类型不会发生改变. 图                               3  展示
                 了使用融合乘加操作        (FMA) 给出的具体分析示例. 示例函数由             MLIR  编写, 接受   3  个内存抽象的中间表示
                 (memref) 作为参数, 从这  3  个内存抽象中加在向量中间表示, 对          3  个向量进行融合乘加运算, 并将输出的向量存储
                 到目标内存中间表示中. 示例使用两个不同长度的向量表示, 长度分别为                      128  和  1 024, 向量中的每个元素为   32  位
                 浮点数. 在从   MLIR  编译至  LLVM IR  之前, 在  MLIR  层级还需要将不同方言的操作改写为低层级方言中的等价操
                 作这个过程向量表示的尺寸不会改变. 最终, 所有低层级方言中的操作都被翻译为                          LLVM IR. 图  3(a2) 和图  3(b2)
                 描述了融合乘加函数的         LLVM IR  代码. 图  3  高亮了  MLIR  和  LLVM IR  中的向量类型. 在这个编译过程中, 中间
                 表示的转换和翻译会重写各层级的方言和操作, 但这些向量抽象的长度和类型从未改变.
                    但是, 与  MLIR  和  LLVM IR  所抽象的向量中间表示参数不同, 汇编代码级别的参数与实际机器关键信息强相
                 关. 此处选择   AVX512  扩展来展示编译结果. AVX512       的寄存器堆包括      32  个 zmm SIMD  寄存器, 每个寄存器的宽
                 度为  512  位. 图  3(a3) 展示了向量抽象长度为    128  的融合乘加函数的汇编代码. 汇编代码清楚地表现出了                SIMD  指
                 令的加载、计算和存储的过程. 加载和存储操作使用 vmovups 指令, 融合乘加操作使用                      vfmadd213ps 指令. 在汇编
                 代码中, 每个   vfmadd213ps 指令需要两个    zmm  寄存器. 所以向量抽象类型为        vector<128xf32> 的融合乘加操作需
                 要  16  个  zmm  寄存器  (4096/512×2) 就可以完成计算. 当向量类型为    vector<1024xf32> 时, 理论上需要  128  个  zmm
                 寄存器, SIMD  寄存器堆但只有      32  个可用. 图  3(b3) 的汇编代码展示了寄存器首先用于进行加载操作, 直至                32  个
                     寄存器用尽. 此后指令       vfmadd213ps 进行融合乘加的计算. 由于没有空闲寄存器可用, 需要将计算结果溢出
                 到内存中, 将寄存器腾挪出来负责后续计算. 最后进行存储操作时, 溢出的计算结果被重新加载回寄存器然后存储
                 到目标缓冲区. 因此, 由于汇编代码和硬件平台指令集绑定的, 不合理的向量化配置会降低执行性能.
                    综上所述, 选择向量配置实际是在权衡迭代开销和溢出开销. 如果使用保守的向量配置策略, 寄存器堆可以承
                 载所有的计算负载需求, 但是需要多次迭代才能完成所有的计算, 即增加了迭代的开销. 如果使用激进的向量配置
                 策略, 寄存器堆需要溢出到内存才可以承载所有的计算负载, 这增加了访存开销, 但是可以用更少次数的迭代完成
                 全部计算, 节省了循环迭代的开销. 这种权衡关系可以指导参数配置范围的选择.
   97   98   99   100   101   102   103   104   105   106   107