本文主要是介绍使用自动矢量化编译Neon,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
概述
作为程序员,你有多种方式使用 Neon 技术:
- 支持 Neon 的开源库,例如 Arm Compute Library提供一种最简单的方式使用 Neon
- 编译器的自动矢量优化特性可以利用 Neon 技术自动优化你的代码
- Neon intrinsics内建函数,编译器用相应的 Neon 指令进行了封装,你可以在 C/C++ 代码中直接使用 Neon 指令
- 对于经验丰富的程序员来说,为了获得极佳的性能,手动编写 Neon 汇编也是一种方法
本文介绍了如何使用Arm Compiler 6中的自动矢量化功能自动生成包含Armv8 Advanced SIMD指令的代码。它包含许多 Neon 代码生成的示例,并强调了如何让编译器生成最佳的性能。
本文对 Arm 开发人员非常有用,对于那些想要使用 Neon 技术而不想编写汇编的人来说尤其有用。在本文结束时,您将获得:
- 哪些 Arm Compiler 命令行选项可以启用生成 Advanced SIMD 代码
- 使用 Arm Compiler 6 的各种优化功能编写C/C++代码
- 在哪里可以找到不同编译器的文档
如果还不熟悉 Neon,在开始之前,你应该先阅读Neon 简介。
本文中的示例使用 Arm Compiler 6,专为在裸机设备上运行的嵌入式应用程序开发而设计。如果你无法获取 Arm
Compiler 6,可以使用 Arm Development Studio Gold Edition 中提供的30天免费试用版。
尽管本指南使用 Arm Compiler 6,你也可以改为其他编译器。你需要查阅编译器文档,找出示例中使用的编译器选项。可以生成 Neon 代码的自动矢量化编译器包括:
- Arm Compiler6,专为在裸机设备上运行的嵌入式应用程序开发而设计,也是本文示例中使用的编译器
- Arm C/C++ Compiler,专为Linux用户空间应用程序开发而设计,最初是为了高性能计算
- LLVM‑clang,基于 LLVM 的开源工具链
- GCC,开源 GNU 工具链
为什么要依赖编译器进行自动矢量优化?
尽管手动编写汇编或者使用 Neon intrinsics 函数可以对 Neon 进行深入控制,但是这些方法可能会导致移植复杂,成本增加。
在许多情况下,高质量的编译器可以生成设计时间少但是质量同样好的代码。允许编译器自动识别代码中可以使用 Advanced SIMD 指令的过程称为自动矢量优化。
从具体的编译技术来看,自动矢量优化包括:
- 循环矢量优化:展开循环以减少迭代次数,同时在每次迭代中执行更多的操作
- Superword-Level (SLP) 矢量优化:将标量运算捆绑在一起,使用全位宽的 Advanced SIMD 指令
自动矢量化编译器包括Arm Compiler6,Arm C/C++ Compiler,LLVM‑clang,GCC。
依赖编译器进行自动矢量优化的好处:
- 只要不存在特定架构的代码,比如内联汇编或者intrinsics,用高级语言实现的程序就是可移植的
- 现代编译器能够自动执行高级优化
- 针对给定的微架构就像设置编译器选项一样简单,而优化汇编程序则需要对目标硬件有深入的了解
然而,自动矢量化可能并非都是正确的选择:
- 虽然源代码可以与体系结构无关,但它可能必须依赖特定的编译器才能生成最佳的代码
- 高级语言或编译器选项的微小变动可能会导致生成的代码发生重大且不可预测的变化
使用编译器生成 Neon 代码适用于大多数项目。仅当生成的代码无法提供必要的性能,或者高级语言不支持特定的硬件功能时,才需要使用 Neon 的其他方法。例如配置系统寄存器控制浮点功能必须在汇编代码中执行。
使用 Arm Compiler 6 编译 Neon
要启用自动矢量优化,你必须指定适当的编译器选项:
- 选择一个具有 Neon 功能的处理器
- 指定自动矢量优化的等级
此外,指定‑Rpass=loop
编译选项会输出编译器优化循环的诊断信息,其中包括矢量化宽度和交错计数。
选择一个具有 Neon 功能的处理器
所有标准 Armv8‑A 都需要实现 Neon,因此任何 Armv8‑A 架构或处理器都允许生成 Neon 代码。
如果您只想在一个特定处理器上运行代码,则可以只针对该处理器。对于这个处理器的微架构性能已经进行了优化,因此仅需保证代码在该处理器上运行即可。
如果您希望代码在各种处理器上运行,您可以针对一种架构。生成的代码需要在实施该架构的任何处理器上运行,因此性能可能会受到影响。
选择 Armv8‑A 的AArch64 状态:
armclang --target=aarch64-arm-none-eabi
选择 Cortex-A53 的 AArch32 状态:
armclang --target=arm-arm-none-eabi -mcpu=cortex-a53
对于较旧的 Armv7 架构,Neon 是可选的,你可以使用‑mcpu
、 ‑march
和‑mfpu
选项来指定启用 Neon。
指定自动矢量优化等级
Arm Compiler 6 提供了各种优化等级,可通过‑O
选项进行选择:
Option | Meaning | Auto-vectorization |
---|---|---|
-O0 | Minimum optimization | Never |
-O1 | Restricted optimization | Disabled by default. |
-O2 | High optimization | Enabled by default. |
-O3 | Very high optimization | Enabled by default. |
-Os | Reduce code size, balancing code size against code speed. | Enabled by default. |
-Oz | Smallest possible code size | Enabled by default. |
-Ofast | Optimize for high performance beyond -O3 | Enabled by default. |
-Omax | Optimize for high performance beyond -Ofast | Enabled by default. |
更多关于编译选项信息请查看Selecting optimization options, in the Arm Compiler User Guide和 -O, in the Arm Compiler armclang Reference Guide。
默认情况下,自动矢量优化在O2
或者更高等级下启用。-fno-vectorize
选项允许你禁用自动矢量优化。
在优化等级‑O1
下,默认禁用自动矢量化。-fvectorize
选项允许你启用自动矢量优化。
在优化级等级‑O0
下,始终禁用自动矢量优化。如果指定‑fvectorize
选项,编译器会忽略。
向量加法示例
让我们看看如何使用编译器选项来自动优化简单的 C 程序。
-
创建一个
vec_add.c
文件,函数将两个 32 位浮点数组相加。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 pointCMP w3,#1B.LT |L3.36|MOV w8,w3 |L3.12|LDR s0,[x0],#4LDR s1,[x1],#4SUBS x8,x8,#1FADD s0,s0,s1STR s0,[x2],#4B.NE |L3.12| |L3.36|RET
在这里我们可以看到该函数的标签为
vec_add
,后面是该函数的汇编指令。FADD
指令是执行操作的核心部分,但代码未使用 Neon ,因为一次仅执行一个加法操作。我们可以看到这一点,因为FADD
指令在标量寄存器S0
和S1
上运行。 -
重新编译代码,这次使用自动矢量优化
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 pointCMP w3,#1B.LT |L3.184|CMP w3,#4MOV w8,w3MOV x9,xzrB.CC |L3.140|LSL x10,x8,#2ADD x12,x0,x10ADD x11,x2,x10CMP x12,x2ADD x10,x1,x10CSET w12,HICMP x11,x0CSET w13,HICMP x10,x2CSET w10,HICMP x11,x1AND w12,w12,w13CSET w11,HITBNZ w12,#0,|L3.140|AND w10,w10,w11TBNZ w10,#0,|L3.140|AND x9,x8,#0xfffffffcMOV x10,x9MOV x11,x2MOV x12,x1MOV x13,x0 |L3.108|LDR q0,[x13],#0x10LDR q1,[x12],#0x10SUBS x10,x10,#4FADD v0.4S,v0.4S,v1.4SSTR q0,[x11],#0x10B.NE |L3.108|CMP x9,x8B.EQ |L3.184| |L3.140|LSL x12,x9,#2ADD x10,x2,x12ADD x11,x1,x12ADD x12,x0,x12SUB x8,x8,x9 |L3.160|LDR s0,[x12],#4LDR s1,[x11],#4SUBS x8,x8,#1FADD s0,s0,s1STR s0,[x10],#4B.NE |L3.160| |L3.184|RET
从指令
FADD v0.4S、v0.4S、v1.4S
可以看出,SLP自动矢量优化已经成功。这条指令将四个32位浮点数打包到一个 SIMD 寄存器中,进行加法运算。然而,这会造成代码体积增大,因为它必须检测以下情况:数组长度是否是 SIMD 宽度的整数倍。根据项目和目标硬件的不同,可以决定代码大小增加是否可以接受。这对于手机应用程序来说可以接受,因为与可用内存相比,代码大小变化微不足道,但对于具有少量 RAM 的嵌入式应用程序来说可能是无法接受。
循环中的函数示例
如果你想使用编译器的优化特性,有时对源代码的更改是不可避免的。当代码太复杂而编译器无法自动矢量优化时,或者当你想要覆盖编译器优化特定代码时,可能会发生这种情况。
-
创建一个
cubed.c
新文件,函数计算数组的立方: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]);} }
-
使用自动矢量优化,编译代码:
armclang --target=aarch64-arm-none-eabi -g -c -O1 -fvectorize cubed.c
-
反汇编生成的目标文件,查看生成的指令:
fromelf --disassemble cubed.o -o disassembly.txt
反汇编代码类似如下:
cubed ; Alternate entry pointFMUL d1,d0,d0FMUL d0,d1,d0RETAREA ||.text.vec_cubed||, CODE, READONLY, ALIGN=2 vec_cubed ; Alternate entry pointSTP x21,x20,[sp,#-0x20]!STP x19,x30,[sp,#0x10]CMP w2,#1B.LT |L4.48|MOV x19,x1MOV x20,x0MOV w21,w2 |L4.28|LDR d0,[x20],#8BL cubedSUBS x21,x21,#1STR d0,[x19],#8B.NE |L4.28| |L4.48|LDP x19,x30,[sp,#0x10]LDP x21,x20,[sp],#0x20RET
这段代码有很多问题:
- 编译器未执行循环或SLP矢量优化,也未内联化立方函数
- 代码需要对输入指针进行检查,保证两个数组不存在重叠
这些问题可以通过多种方式解决,例如以更高的优化等级进行编译,但让我们重点讨论在不更改编译器选项的情况下可以更改哪些代码。
-
在代码中添加以下宏和限定符,可以覆盖编译器的一些编译优化
__attribute__((always_inline))
是 Arm 编译器扩展,指示编译器始终尝试内联该函数。在这个例子中,不仅函数被内联,而且编译器还可以执行 SLP 矢量优化。
内联之前,立方函数仅适用于标量双精度,因此不需要、也没有方法对该函数本身执行 SLP 矢量优化。
当立方函数被内联后,编译器可以检测到其操作是在数组上执行的,就会使用 Advanced SIMD 指令对代码进行优化。
restrict
是一个标准 C/C++ 关键字,它告知编译器为数组分别一片独立的内存区域。这就保证不需要进行数组地址重叠检测。#pragma clang loop interleave_count(X)
是一个 Clang 语言的扩展,可以让你指定自动矢量化的向量宽度和交错。这个指令也是Arm Compiler的一个"社区“功能
更多矢量化的宏请参考 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 pointCMP w2,#1B.LT |L4.132|CMP w2,#4MOV w8,w2B.CS |L4.28|MOV x9,xzrB |L4.92| |L4.28|AND x9,x8,#0xfffffffcADD x10,x0,#0x10ADD x11,x1,#0x10MOV x12,x9 |L4.44|LDP q0,q1,[x10,#-0x10]ADD x10,x10,#0x20SUBS x12,x12,#4FMUL v2.2D,v0.2D,v0.2DFMUL v3.2D,v1.2D,v1.2DFMUL v0.2D,v0.2D,v2.2DFMUL v1.2D,v1.2D,v3.2DSTP q0,q1,[x11,#-0x10]ADD x11,x11,#0x20B.NE |L4.44|CMP x9,x8B.EQ |L4.132| |L4.92|LSL x11,x9,#3ADD x10,x1,x11ADD x11,x0,x11SUB x8,x8,x9 |L4.108|LDR d0,[x11],#8SUBS x8,x8,#1FMUL d1,d0,d0FMUL d0,d0,d1STR d0,[x10],#8B.NE |L4.108| |L4.132|RET
此反汇编表明内联、SLP 矢量化和循环矢量化已实现,使用限制指针不需要再进行重叠检查。
由于需要处理总循环计数不是四倍的情况,代码大小略有增加。循环展开深度为2,SLP 宽度为2,因此有效展开深度为4。如果循环计数始终是4的倍数,我们可以进一步优化。
-
假设循环计数始终是四的倍数,我们可以通过mask循环的低位,告知编译器:
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 pointAND w8,w2,#0xfffffffcCMP w8,#1B.LT |L13.40|MOV w8,w8 |L13.16|LDR q0,[x0],#0x10SUBS x8,x8,#2FMUL v1.2D,v0.2D,v0.2DFMUL v0.2D,v0.2D,v1.2DSTR q0,[x1],#0x10B.NE |L13.16| |L13.40|RET
代码大小减小了,因为编译器知道不再需要测试和处理任何不是四的倍数的剩余迭代。我们向编译器保证提供的数据始终是向量长度的倍数,从而可以生成更优的代码。
如果使用-O2
编译优化,就无需按上面那样更改代码了,但是复杂的代码可能还是需要像上面那样调整,从而获得最佳性能。
下面是完整的代码清单,你可以尝试各种优化等级和展开深度,进行编译和反汇编,观察编译器的自动矢量优化行为。
/*
* Copyright (C) Arm Limited, 2019 All rights reserved.
*
* The example code is provided to you as an aid to learning when working
* with Arm-based technology, including but not limited to programming tutorials.
* Arm hereby grants to you, subject to the terms and conditions of this Licence,
* a non-exclusive, non-transferable, non-sub-licensable, free-of-charge licence,
* to use and copy the Software solely for the purpose of demonstration and
* evaluation.
*
* You accept that the Software has not been tested by Arm therefore the Software
* is provided "as is", without warranty of any kind, express or implied. In no
* event shall the authors or copyright holders be liable for any claim, damages
* or other liability, whether in action or contract, tort or otherwise, arising
* from, out of or in connection with the Software or the use of Software.
*/
#include <stdio.h>
void vec_init(double *vec, int len_vec, double init_val)
{int i;for (i=0; i<len_vec; i++) {vec[i] = init_val*i - len_vec/2;}
}void vec_print(double *vec, int len_vec)
{int i;for (i=0; i<len_vec; i++) {printf("%f, ", vec[i]);}printf("\n");
}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]);}
}__attribute__((always_inline)) double cubed_i(double x)
{return x*x*x;
}void vec_cubed_opt(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; i++) {y_vec[i] = cubed_i(x_vec[i]);}
}int main()
{int N = 10;double X[N];double Y[N];vec_init(X, N, 1);vec_print(X, N);vec_cubed(X, Y, 10);vec_print(Y, N);vec_cubed_opt(X, Y, 10);vec_print(Y, N);return 0;
}
自动矢量化的编码最佳实践
如果代码实施比较复杂,编译器进行自动矢量优化的可能性会降低。例如,具有以下特征的循环体难以(或不可能)进行矢量优化:
- 不同循环迭代之间相互依赖
- 具有
break
表达式的循环 - 具有复杂条件的循环
Arm 建议修改源代码实现以避免这些情况。
例如,自动矢量优化的一个必要条件是,在循环开始时必须知道迭代次数。break
语句意味着在循环开始时可能无法知道循环次数,这将阻止自动矢量优化。如果无法完全避免break
语句,可以将循环分解为多个可矢量优化和不可矢量化的部分。
关于控制循环矢量化的编译器指令,请参见LLVM-Clang 文档,其中最重要的两个是:
#pragma clang loop vectorize(enable)
#pragma clang loop interleave(enable)
这些pragma
是提示编译器分别执行 SLP 和 Loop 矢量优化,他们是 Arm Compiler 的 COMMUNITY 特性。
Arm C/C++ Linux 用户空间编译器提供了更多关于自动矢量优化的说明,其中许多要点将适用于 LLVM-Clang 变体:
- Arm C/C++ 编译器:自动矢量化的编码最佳实践
- Arm C/C++ 编译器:使用 pragma 控制自动矢量化
检查你的知识
以下问题可以测试你学习到的知识:
-
什么是 Neon?
Neon 是 Arm 架构 Advanced SIMD 扩展的实现。所有符合 Armv8-A 架构的处理器(例如,Cortex-A76或Cortex-A57)均实现 Neon。在程序员看来,Neon 提供额外32个128位寄存器,指令可以在这些寄存器的8位、16位、32位或64位通道上运行。
-
Arm Compiler 如何启用 Neon 代码生成?
对于 AArch64,使用
--target=aarch64-arm-none-eabi
,并指定合适的优化等级,例如-O1 -fvectorize
、-O2
或者更高。 -
假设 Arm 编译器自动展开一个深度为2的循环,如何强制编译器展开到4的深度?
#pragma clang loop interleave_count (4)
将实现这一点,但仅适用于该循环。 -
如何编写源代码来帮助编译器优化?
考虑如下函数,使用
-O1
编译:float vec_dot(float *vec_A, float *vec_B, int len_vec) {float ret = 0;int i;for (i=0; i<len_vec; i++) {ret += vec_A[i]*vec_B[i];}return ret; }
您可以进行以下更改以帮助编译器优化:
- 以
-O2
或更高等级编译,或使用-fvectorize
编译 - 在循环之前声明
#pragma clang loop vectorize(enable)
提示编译器 - 请注意,在此过程中,我们不会修改向量,因此添加
restrict
关键字不会执行任何操作,输入数组是否重叠并不重要 - SLP 矢量优化会导致代码增加,这可能是可以接受的,具体取决于硬件限制和期望的输入数组长度
以下是优化后的源代码:
float vec_dot(float *vec_A, float *vec_B, int len_vec) {float ret = 0;int i;#pragma clang loop vectorize(enable)for (i=0; i<len_vec; i++) {ret += vec_A[i]*vec_B[i];}return ret; }
相关信息
以下是与本文相关的一些资源:
- Arm Compiler 6 documentation提供有关裸机编译器的信息
- Arm C/C++ Compiler documentation提供有关 Linux 用户空间编译器的信息
- LLVM-clang documentation提供有关基于 LLVM 开源工具链的信息
- GCC documentation提供有关开源 GNU 工具链的信息
- The Architecture Exporation Tools可以了解有关 Advanced SIMD 指令集的更多信息
- The Arm Architecture Reference Manual Armv8, for Armv8-A architecture profile提供 Advanced SIMD 指令集的完整规范
- Optimizing C Code with Neon Intrinsics guide介绍了如何在 C 或 C++ 代码中使用 Neon instrinsics,以利用 Armv8 架构 Advanced SIMD 技术
- 以
欢迎关注“安全有理”微信公众号。
这篇关于使用自动矢量化编译Neon的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!