面向 ARM 处理器的 GNU 编译器集合 (GCC) 命令行选项最初是在多年前设计的,当时可用处理器和变体的列表与现今相比要短很多。随着 ARM 架构的演进,从 GCC 中获取最佳代码所需的选项也已改变,但也做了各种尝试来确保选项的现有集合不会改变其本意。编译器的设计意味着充分利用 ARM CortexTM-A 处理器 所需的选项现在已非常复杂。本文阐述 GCC 命令行选项的三大方面:CPU、浮点和 SIMD(单指令多数据)加速。
应当针对我的 CPU 使用什么选项?
首先,我们来看看用于告知编译器您所使用的 CPU 的主要选项;然后,我们来阐述一些可在特殊情形中使用的更为高级的选项。
在您编译文件时,编译器都需要知道您要用于运行结果代码的 CPU 类型。实现这一目的的主要选项是 -mcpu=<cpu-name> 选项。正如您可能预想到的,cpu-name 替换为您拥有的 CPU 类型的具体名称,但使用小写。例如,对于 Cortex-A9,该选项为 -mcpu=cortex-a9。GCC 目前支持到 Cortex-A15(含)为止的所有 Cortex-A,即:
Cortex-A5 -mcpu=cortex-a5
Cortex-A7 -mcpu=cortex-a7
Cortex-A8 -mcpu=cortex-a8
Cortex-A9 -mcpu=cortex-a9
Cortex-A15 -mcpu=cortex-a15
如果您的 GCC 版本无法识别上述之一,或许其版本太旧,您应当考虑升级。如果不指定要使用的 CPU,GCC 将使用其内置的默认值。这会因编译器原先构建的方式而不同,可能意味着生成的代码在您所拥有的 CPU 上执行时速度非常缓慢(或者无法执行)。
添加浮点和 SIMD
目前市面上的所有 ARM Cortex-A 处理器都配有浮点单元,大多数也拥有实施 ARM 高级 SIMD 处理器扩展(通常称为 NEONTM)的 SIMD 单元。然而,准确的可用指令集根据您所拥有的 CPU 而不同,GCC 需要单独的选项来进行控制;它不会尝试从 -mcpu 选项进行处理。对浮点和 SIMD 指令的选择是通过 –mfpu 选项控制的,下表中列出了各种 CPU 的建议选择:
处理器
仅浮点
浮点 + SIMD
Cortex-A5
Cortex-A7
Cortex-A8
Cortex-A9
Cortex-A15
VFPv3 和 VFPv4 实施从 32 个双精度寄存器开始;但是,在没有 NEON 时,上部 16 个寄存器变为可选;这是通过选项名称的 d16 部分控制的。该名称的 fp16 部分指定是否存在半精度(16 位)浮点加载、存储和转换指令;这是对 VFPv3 的扩展,但在所有 VFPv4 实施中都可用。
由于历史原因,如果被明确告知这么做是安全的,GCC 将仅使用浮点和 NEON 指令。对此进行控制的选项有些令人混淆,选项的一部分也可更改编译器所遵守的 ABI。选项 -mfloat-abi 有三个可能的选项:
-mfloat-abi=soft -- 忽略所有 FPU 和 NEON 指令,仅使用核心寄存器集合,并通过库调用模拟所有浮点运算。
-mfloat-abi=softfp -- 使用与 -float-abi=soft 相同的调用约定,但会根据需要使用浮点和 NEON 指令。此选项二进制兼容 -mfloat-abi=soft,可用于改善必须遵守软浮点环境的代码的性能,但需要是已知相关硬件指令也可用的环境。
-mfloat-abi=hard -- 根据需要使用浮点和 NEON 指令,也会更改 ABI 调用约定来生成效率更高的函数调用;现在可以在扩展寄存器中于函数之间传递浮点和矢量类型,不仅节约大量复制操作,也意味着需要在堆栈上传递参数的调用变少。
应当使用上述选项中的哪一个在很大程度上取决于您的目标系统,也可能默认选项就是正确的选项。例如,Ubuntu 12.04 (Precise) 现在默认使用 -mfloat-abi=hard。
矢量化浮点运算
NEON 架构包含同时对整数和浮点数据类型进行运算的指令,GCC 现在拥有强大的自动矢量化优化,可发现何时适合使用可改善性能的矢量引擎。不过,让许多用户讶异的是,即使他们可能希望要这么做,编译器也无法矢量化其代码。
首先要记住的是,自动矢量化器只有在 -O3 时默认启用。有可以在其他时候将它打开的选项,您可以在 GCC 手册中找到相关内容。
然而,即使启用了矢量化器,浮点代码也常常不能矢量化。其原因为,尽管 NEON 中的浮点运算使用 IEEE 单精度格式来容纳值,为了最小化 NEON 单元中所需的功率并且最大化吞吐量,只有在输入和结果在正常运算范围内(即值不是非正常值或 NaN)时,矢量引擎才会完全按照标准进行编译。GCC 默认配置为生成严格遵守 IEEE 浮点算法规范的代码,前文所述的限制意味着默认使用 SIMD 指令是不适当的。
幸运的是,GCC 确实提供了多个命令行选项,可用于准确控制需要的 IEEE 标准遵守级别。虽然细节已超过本文的讨论范畴,在大多数情形中,使用 -ffast-math 选项来放宽规则并启用矢量化是完全安全的。
此外,也可在 GCC 4.6 或更高版本中使用 -Ofast 选项来实现基本相同的效果。它会打开 -O3 以及多种其他优化,通常可以安全地用于从您的代码中获得最佳的性能。
要记住的另一点是,NEON 仅支持单精度数据的矢量运算。除非您编写的代码用于处理这一格式,否则可能会发现矢量化并不可行。您也应当了解,浮点常量(字面值)最终会强制编译器以双精度执行计算。在 C 和 C++ 中,请编写 '1.0F' 而非 '1.0' 确保编译器了解您的意思。
最后,如果对于找出矢量化器为何不按您预期运作的原因依然存有疑问,而且您也准备好亲自动手探究,GCC 对于其所做的行为提供了非常丰富的信息。-fdump-tree-vect 和 -ftree-vectorizer-verbose=<level> 选项控制所生成的信息量,级别是 1 到 9 范围内的数字。虽然生成的大多数信息只有编译器开发人员才会感兴趣,但您不时也会在输出结果中找到提示,了解您的代码不能按预期矢量化的原因。
总而言之
看来,选项有许多,我的日常运算中应当使用哪个呢?幸运的是,一旦确定了目标环境,大多数的选项不会时常更改。以下是一些示例:
具有 NEON 的 Cortex-A15 处理器,以及操作使用“浮点”数据类型的数据阵列的一些浮点代码。该运行环境可以支持在浮点寄存器中传递参数:
arm-gcc -O3 -mcpu=cortex-a15 -mfpu=neon-vfpv4 -mfloat-abi=hard \
-ffast-math -o myprog.exe myprog.c
不带 NEON 的 Cortex-A7 处理器,处理浮点代码。该运行环境仅支持在整数寄存器中传递参数,但可以使用浮点硬件
arm-gcc -O3 -mcpu=cortex-a7 -mfpu=vfpv4-d16 -mfloat-abi=softfp \
-o myprog2.exe myprog2.c
最后,Cortex-A9 处理器在浮点/NEON 寄存器集合全都无法使用的环境中运作(例如,因为它在中断处理程序的中间,并且浮点上下文保留用于用户状态)。
arm-gcc -O3 -mcpu=cortex-a9 -mfloat-abi=soft -c -o myfile.o myfile.c
相关 ARM 博客:
Richard Earnshaw 是 ARM 首席工程师,也是 ARM GNU 编译器工具团队的技术总管和软件架构师。除了其他工作外,他在编译器工作方面拥有将近 20 年的经验,也是 GCC 全球评论员的一员。