学习路线指南: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
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
- 不矢量化
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
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
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)