学习路线指南:https://zhuanlan.zhihu.com/p/...
原文地址:https://developer.arm.com/arc...

Compiling for Neon with auto-vectorization 使用自动向量化进行Neon编译

本章节涉及编译器细节和汇编比较多,可能会比较晦涩。如果不选择用编译器进行自动向量化,可以略过,实际上复杂代码编译器很难良好的进行自动向量化。

Why rely on the compiler for auto-vectorization? 为什么要依靠编译器进行自动矢量化?

快呀,方便呀,还用说什么好处,这还不够?

  • 自动向量化编译器包括Arm编译器6,Arm C/C++编译器,LLVM-clang和GCC。

Compiling for Neon with Arm Compiler 6 使用Arm编译器6进行Neon编译

  • 如果只想在一个特定的处理器上运行代码,则可以针对该特定的处理器。性能针对该处理器的微体系结构进行了优化。但是,只能保证代码可以在该处理器上运行。
  • 如果希望代码在各种处理器上运行,则可以针对体系结构。生成的代码可以在该目标体系结构的任何处理器实现上运行,但是性能可能会受到影响。
//To target Armv8‑A AArch64 state
armclang --target=aarch64-arm-none-eabi
//To target the Cortex‑A53 in AArch32 state
armclang --target=arm-arm-none-eabi -mcpu=cortex-a53
  • Arm Compiler 6提供了广泛的优化级别,可以通过以下-O选项进行选择
    image.png

1.默认情况下,自动矢量化处于优化级别-O2或更高级别。-fno-vectorize选项可以禁用自动矢量化。
2.在优化级别-O1,默认情况下禁用自动矢量化。-fvectorize选项可以启用自动矢量化。
3.在优化级别-O0,始终禁用自动矢量化。如果指定该-fvectorize选项,则编译器将忽略它。

Example: vector addition

void vec_add(float *vec_A, float *vec_B, float *vec_C, int len_vec) {
        int i;
        for (i=0; i<len_vec; i++) {
                vec_C[i] = vec_A[i] + vec_B[i];
        }
}
  • 不矢量化
armclang --target=aarch64-arm-none-eabi -g -c -O1 vec_add.c
fromelf --disassemble vec_add.o -o disassembly_vec_off.txt
vec_add                  ; Alternate entry point
        CMP      w3,#1
        B.LT     |L3.36|
        MOV      w8,w3
|L3.12|
        LDR      s0,[x0],#4
        LDR      s1,[x1],#4
        SUBS     x8,x8,#1
        FADD     s0,s0,s1
        STR      s0,[x2],#4
        B.NE     |L3.12|
|L3.36|
        RET
  • 矢量化
armclang --target=aarch64-arm-none-eabi -g -c -O1 vec_add.c -fvectorize
fromelf --disassemble vec_add.o -o disassembly_vec_on.txt
vec_add                  ; Alternate entry point
        CMP      w3,#1
        B.LT     |L3.184|
        CMP      w3,#4
        MOV      w8,w3
        MOV      x9,xzr
        B.CC     |L3.140|
        LSL      x10,x8,#2
        ADD      x12,x0,x10
        ADD      x11,x2,x10
        CMP      x12,x2
        ADD      x10,x1,x10
        CSET     w12,HI
        CMP      x11,x0
        CSET     w13,HI
        CMP      x10,x2
        CSET     w10,HI
        CMP      x11,x1
        AND      w12,w12,w13
        CSET     w11,HI
        TBNZ     w12,#0,|L3.140|
        AND      w10,w10,w11
        TBNZ     w10,#0,|L3.140|
        AND      x9,x8,#0xfffffffc
        MOV      x10,x9
        MOV      x11,x2
        MOV      x12,x1
        MOV      x13,x0
|L3.108|
        LDR      q0,[x13],#0x10
        LDR      q1,[x12],#0x10
        SUBS     x10,x10,#4
        FADD     v0.4S,v0.4S,v1.4S
        STR      q0,[x11],#0x10
        B.NE     |L3.108|
        CMP      x9,x8
        B.EQ     |L3.184|
|L3.140|
        LSL      x12,x9,#2
        ADD      x10,x2,x12
        ADD      x11,x1,x12
        ADD      x12,x0,x12
        SUB      x8,x8,x9
|L3.160|
        LDR      s0,[x12],#4
        LDR      s1,[x11],#4
        SUBS     x8,x8,#1
        FADD     s0,s0,s1
        STR      s0,[x10],#4
        B.NE     |L3.160|
|L3.184|
        RET

从指令中可以看到,自动矢量化已成功完成,指令FADD v0.4S,v0.4S,v1.4S对打包到SIMD寄存器中的四个32位浮点数执行加法运算。但是,这给代码大小带来了巨大的代价,因为它必须检测SIMD宽度不是数组长度的因数的情况。

Example: function in a loop

"如果您想使用编译器的特定优化功能,有时不可避免地需要对源代码进行更改。当代码太复杂而编译器无法自动矢量化时,或者您要覆盖编译器有关如何优化特定代码的决定时,可能会发生这种情况",这段话拗口的很,简单来说就是编译器自动矢量化有可能失效,这时候需要调整一下代码。

double cubed(double x) {
        return x*x*x;
}
 
void vec_cubed(double *x_vec, double *y_vec, int len_vec) {
        int i;
        for (i=0; i<len_vec; i++) {
                y_vec[i] = cubed(x_vec[i]);
        }
}
cubed                  ; Alternate entry point
        FMUL     d1,d0,d0
        FMUL     d0,d1,d0
        RET

        AREA ||.text.vec_cubed||, CODE, READONLY, ALIGN=2

vec_cubed                  ; Alternate entry point
        STP      x21,x20,[sp,#-0x20]!
        STP      x19,x30,[sp,#0x10]
        CMP      w2,#1
        B.LT     |L4.48|
        MOV      x19,x1
        MOV      x20,x0
        MOV      w21,w2
|L4.28|
        LDR      d0,[x20],#8
        BL       cubed
        SUBS     x21,x21,#1
        STR      d0,[x19],#8
        B.NE     |L4.28|
|L4.48|
        LDP      x19,x30,[sp,#0x10]
        LDP      x21,x20,[sp],#0x20
        RET

此代码中存在许多问题:
1.编译器尚未执行Loop或SLP矢量化,也没内联cubed函数。
2.该代码需要对输入指针执行检查,以验证数组不重叠。
可以通过多种方式解决这些问题,例如以更高的优化级别进行编译,但是让我们集中讨论在不更改编译器选项的情况下可以进行哪些代码更改。

  • 将以下宏和限定符添加到代码中,以覆盖某些编译器的决策。

    __attribute__((always_inline)) - 是Arm编译器扩展,它表示编译器始终尝试内联函数。在此示例中,不仅内联了函数,而且编译器还可以执行SLP矢量化
    restrict - 是标准的C/C++关键字,它向编译器指示给定的数组对应于内存的唯一区域。这消除了对重叠数组进行运行时检查的需要
    #pragma clang loop interleave_count(X) - 是Clang语言扩展,可让您通过指定矢量宽度和交织计数来控制自动矢量化
    
__always_inline double cubed(double x){ 
        return x * x * x; 
} 
 
void vec_cubed(double * restrict x_vec,double * restrict y_vec,int len_vec){ 
        int i; 
        #pragma clang loop interleave_count(2)
        for(i = 0; i <len_vec; i ++){ 
                y_vec [i] = cubed(x_vec [i]); 
        } 
}
vec_cubed                  ; Alternate entry point
        CMP      w2,#1
        B.LT     |L4.132|
        CMP      w2,#4
        MOV      w8,w2
        B.CS     |L4.28|
        MOV      x9,xzr
        B        |L4.92|
|L4.28|
        AND      x9,x8,#0xfffffffc
        ADD      x10,x0,#0x10
        ADD      x11,x1,#0x10
        MOV      x12,x9
|L4.44|
        LDP      q0,q1,[x10,#-0x10]
        ADD      x10,x10,#0x20
        SUBS     x12,x12,#4
        FMUL     v2.2D,v0.2D,v0.2D
        FMUL     v3.2D,v1.2D,v1.2D
        FMUL     v0.2D,v0.2D,v2.2D
        FMUL     v1.2D,v1.2D,v3.2D
        STP      q0,q1,[x11,#-0x10]
        ADD      x11,x11,#0x20
        B.NE     |L4.44|
        CMP      x9,x8
        B.EQ     |L4.132|
|L4.92|
        LSL      x11,x9,#3
        ADD      x10,x1,x11
        ADD      x11,x0,x11
        SUB      x8,x8,x9
|L4.108|
        LDR      d0,[x11],#8
        SUBS     x8,x8,#1
        FMUL     d1,d0,d0
        FMUL     d0,d0,d1
        STR      d0,[x10],#8
        B.NE     |L4.108|
|L4.132|
        RET

反汇编表明内联,SLP矢量化和循环矢量化已成功。使用限制指针消除了运行时重叠检查。
由于总循环计数不是四的倍数(有效展开深度)时,循环尾可以处理任何剩余的迭代,因此代码大小略有增加。循环展开深度为2,SLP宽度为2,因此有效展开深度为4。在下一步中,如果我们知道循环计数始终是4的倍数,我们将研究可以进行的优化。

  • 让我们假设循环计数将始终是四的倍数。我们可以通过屏蔽循环计数器的低位与编译器进行通信
void vec_cubed(double * restrict x_vec,double * restrict y_vec,int len_vec){ 
        int i; 
        #pragma clang loop interleave_count(1)
        for(i = 0; i <(len_vec&〜3); i ++){ 
                y_vec [i] = cubed_i(x_vec [i]); 
        } 
}
vec_cubed                  ; Alternate entry point
        AND      w8,w2,#0xfffffffc
        CMP      w8,#1
        B.LT     |L13.40|
        MOV      w8,w8
|L13.16|
        LDR      q0,[x0],#0x10
        SUBS     x8,x8,#2
        FMUL     v1.2D,v0.2D,v0.2D
        FMUL     v0.2D,v0.2D,v1.2D
        STR      q0,[x1],#0x10
        B.NE     |L13.16|
|L13.40|
        RET

Coding best practices for auto-vectorization 编码自动向量化的最佳做法

随着实现变得更加复杂,编译器可以自动矢量化代码的可能性降低了。例如,具有以下特征的循环特别难以(或不可能)进行矢量化:

在不同循环迭代之间具有相互依赖性的循环。
带有break子句的循环。
具有复杂条件的循环。

Arm建议修改您的源代码实现以消除这些情况。

例如,自动向量化的必要条件是必须在循环开始时就知道循环大小中的迭代次数。中断条件意味着循环的大小在循环开始时可能是未知的,这将阻止自动向量化。如果不可能完全避免出现中断条件,则值得将循环分解为多个可矢量化和不可矢量化的部分。

  • 控制循环矢量化的编译器指令
#pragma clang loop vectorize(enable)
#pragma clang loop interleave(enable)

ysysys
10 声望1 粉丝

引用和评论

0 条评论