本章节主要介绍了自动向量化的相关内容。
🎬个人简介:一个全栈工程师的升级之路!
📋个人专栏:高性能(HPC)开发基础教程
🎀CSDN主页?发狂的小花
🌄人生秘诀:学习的本质就是极致重复!
目录
SIMD 作为一种重要的并行化技术,在提升性能的同时也会增加开发的难度。目前大多数编译器都具有自动向量化的功能,将 C/C++ 代码自动替换为 SIMD 指令。
从编译技术上来说,自动向量化一般包含两部分:循环向量化(Loop vectorization)和超字并行向量化(SLP,Superword-Level Parallelism vectorization,又称Basic block vectorization)。
演示代码:
void add(int *a, int *b, int n, int * restrict sum)
{
// it is assumed that the input n is an integer multiple of 4
for (int i = 0; i < (n & ~3); ++i)
{
sum[i] = a[i] + b[i];
}
}
for (int i = 0; i < (n & ~3); i += 4)
{
sum[i] = a[i ] + b[i];
sum[i + 1] = a[i + 1] + b[i + 1];
sum[i + 2] = a[i + 2] + b[i + 2];
sum[i + 3] = a[i + 3] + b[i + 3];
}
SLP 自动向量化
接下来介绍如何通过编译器实现自动向量化。
目前支持自动向量化的编译器有 Arm Compiler 6、Arm C/C++ Compiler、LLVM-clang 以及 GCC,这几种编译器间的相互关系如下表所示。
Arm Compiler 6 | 需arm授权 | 基于LLVM-clang,针对裸机端的嵌入式开发 |
Arm C/C++ Compiler | 需arm授权 | 基于LLVM-clang,最初为高性能计算设计,针对linux下的用户应用程序开发 |
LLVM-clang | 开源 | 基于LLVM架构的C/C++/Objective-C编译器前端 |
GCC | 开源 | GNU编译器套件 |
自动向量化默认不会被启用,编程人员需要向编译器提供允许自动向量化的“许可证”来对自动向量化功能进行使能
。
下文中 Arm Compiler 6 与 Arm C/C++ Compiler 使用 armclang 统称
,armclang 使能自动向量化配置信息如下表所示:
配置项 | 说明 | 常用参数 |
---|---|---|
--target | 给定支持 neon 的目标平台 | arm-none-eabi:32 位 arm 嵌入式平台 aarch64-arm-none-eabi:64 位 arm 嵌入式平台 |
-mcpu | 给定包含 neon 单元的 cpu(32位) | cortex-a53、cortex-a8、cortex-a15 等 |
-fvectorize | 启用自动向量化(-O2 及以上优化等级默认启用) | 无 |
-O | 自动向量化仅在 -O1 及以上优化等级生效 | -O1、-O2 等 |
--fpmode | 可选项,针对浮点向量化,定义浮点运算类型 | ieee_full:完全遵循IEEE标准std:默认配置,非正规数flush到0、舍入到最接近的IEEE标准,不带异常fast:可能会有一点精度损失 |
armclang 实现自动向量化示例:
# AArch32
armclang --target=arm-none-eabi -mcpu=cortex-a53 -O1 -fvectorize main.c
# AArch64
armclang --target=aarch64-arm-none-eabi -O2 main.c
Android NDK 从 r13 开始以 clang 为默认编译器,本节通过 cmake 调用Android NDK r19c 工具链展示 clang 的自动向量化方法。
-fvectorize | 启用自动向量化(-O2 及以上优化等级默认启用) | 无 |
---|---|---|
-O | 自动向量化仅在 -O1 及以上优化等级生效 | -O1、-O2 等 |
# method 1
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O1 -fvectorize")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O1 -fvectorize")
# method 2
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O2")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2")
在 gcc 中使能自动向量化配置参数如下:
配置项 | 说明 | 常用参数 |
---|---|---|
-mcpu | 给定支持 neon 的目标平台 | cortex-a53、cortex-a8、cortex-a15 |
-mfpu | 明确目标平台支持 neon(64 位弃用,默认支持) | vfpv3、neon、vfpv4 |
-ftree-vectorize | 启用自动向量化(-O3 默认启用) | 无 |
-O | 自动向量化仅在 -O1 及以上优化等级生效 | -O1、-O2 |
--ffast-math | 可选项,针对浮点向量化,弱化 IEEE-754 标准 | 无 |
-mfloat-abi | 可选项,针对浮点向量化,明确浮点协处理器 | soft:不使用 fpusoftfp:使用 fpu 计算,通过普通寄存器传参数hard:使用 fpu 计算,通过 fpu的寄存器传参数 |
-mcpu | -mfpu | -mfpu |
---|---|---|
- | FP only | FP + SIMD |
cortex-a5 | -mfpu=vfpv3-fp16 -mfpu=vfpv3-d16-fp16 | -mfpu=neon-fp16 |
cortex-a7 | -mfpu=vfpv4 -mfpu=vfpv4-d16 | -mfpu=neon-vfpv4 |
cortex-a8 | -mfpu=vfpv3 | -mfpu=neon |
cortex-a9 | -mfpu=vfpv3-fp16 -mfpu=vfpv3-d16-fp16 | -mfpu=neon-fp16 |
cortex-a15 | -mfpu=vfpv4 | -mfpu=neon-vfpv4 |
gcc 中实现自动向量化的编译配置如下:
# AArch32
arm-none-linux-gnueabihf-gcc -mcpu=cortex-a53 -mfpu=neon -ftree-vectorize -O2 main.c
# AArch64
aarch64-none-linux-gnu-gcc -mcpu=cortex-a53 -ftree-vectorize -O2 main.c
此外,gcc 中可以通过 -fopt-info-vec 命令查看自动向量化的详细信息,比如哪些代码实现了向量化,哪些代码没有实现向量化及没有进行向量化的原因。
我们以上节的求和示例代码,来对编译器自动向量化的功能进行演示。编译器以 32 位 arm-gcc 为例:
# automatic vectorization is not enabled
arm-none-linux-gnueabihf-gcc -O2 main.c -o avtest
# automatic vectorization is enabled
arm-none-linux-gnueabihf-gcc -mfpu=neon -ftree-vectorize -O2 main.c -o avtest
arm-none-linux-gnueabihf-objdump -d avtest > assemble.txt
反汇编代码
启用自动向量化之后,编译器通过矢量化加载?(ldr?->?vld1)、求和?(add?->?vadd)以及保存?(str?->?vst1)等指令,将每次循环中处理的数据变为 4 个,循环次数精简为之前的 1/4。
基于一定的编程优化准则,可以更好的协助编译器完成自动向量化的工作,获得理想的性能状态。
当循环中存在数据依赖时,编译器无法进行向量化。
下述代码中计算 a[i] 时依赖上一次循环的输出,无法被向量化。
// the output of a[i] depends on its last result
for (int i = 1; i < n; ++i)
{
a[i] = a[i - 1] + 1;
}
编译器无法对间接寻址,多级索引、多级解引用等行为进行向量化,尽量避免使用多级指针。
下述代码通过?idx
?进行了多级索引,无法被向量化。
// idx is unpredictable, so this code cannot be vectorized
for (int i = 0; i < n; ++i)
{
sum[idx[i]] = a[idx[i]] + b[idx[i]];
}
当循环中存在条件语句或跳转语句时,代码很难被向量化。因此应尽量避免在循环中的使用if、break等语句。当循环中需要调用函数时,尽量使用内联函数进行替换。
下述代码通过调用内联函数 add_single2 避免发生函数跳转。
__attribute__((noinline)) int add_single1(int a, int b);
__inline__ __attribute__((always_inline)) int add_single2(int a, int b);
void add(const int *a, const int *b, int n, int * restrict sum)
{
for (int i = 0; i < (n & ~3); ++i)
{
// replace normal functions with inline functions
// sum[i] = add_single1(a[i], b[i]);
sum[i] = add_single2(a[i], b[i]);
}
}
neon 对 64 位长数据类型的支持有限,且较小的数据位宽有更高的并行度,应尽量选用较小的数据类型。当程序中存在浮点数据时,指明其数据类型。
下述代码指明1.0是浮点数据,否则编译器会优先将其理解为double。
// assume that array sum and a are floating-point arrays
for (int i = 0; i < (n & ~3); ++i)
{
// replace 1.0 with 1.f
// sum[i] = a[i] + 1.0;
sum[i] = a[i] + 1.f;
}
指针操纵同一片数据区的情况被称为地址交叠。地址交叠会阻止自动向量化操作。
当程序不会发生地址交叠时,用?restrict
?限定符(C99 引入)在代码中声明指针所指区域是独立的。
下述代码通过?restrict
?限定 sum 与 a、b 间没有地址交叠的情况。
// add restrict before the output parameter sum
void add(const int *a, const int *b, int n, int * restrict sum)
明确数组尺寸,使其达到向量化处理长度的整数倍。但应注意处理不足向量化部分的剩余数据。
下述代码通过掩码操作表明处理循环次数是 4 的整数倍。
// make number of cycles is an integer multiple of 4,
for (int i = 0; i < (n & ~3); ++i)
// don't forget to process the remaining data
在一些编译器中可以通过在?for
?循环之前增加预处理语句告知编译器循环展开级数。
下述代码告知?armclang
?编译器希望将循环展开 4 次。
// #pragma unroll (4) // armcc
#pragma clang loop interleave_count(4) //armclang
for (int i = 0; i < n; ++i)
{
// ...
}
编译器仅会对每一成员都有操作的结构体加载操作进行自动向量化,可以结合实际需求考虑去除用于结构体对齐的填充数据。
下述代码中删除用于填充结构体的变量 padding 以避免无法向量化。
struct st_align
{
char r;
char g;
char b;
// delete the data used to populate the structure
// char padding;
};
下述代码中结构体由于?short
?类型与?char
?类型不一致而不会被执行自动向量化。
struct st_align
{
short r; // change short to char to get auto-vectoration
char g;
char b;
};
尽量通过?<
?构造循环,而不是?<=
?和?!=
?。
下述代码通过调整i
的范围实现?<
?替换?<=
?。
// use '<' to construct a loop instead of '<='
// for(int i = 1; i <= n; ++i)
for (int i = 1; i < n + 1; ++i)
{
// ...
}
当对数组进行操作时,使用数组索引替代指针索引。
下述代码通过?sum[i]
?进行索引,而不是*(sum + i)
。
// replace arrary with pointer
// *(sum + i) = *(a + i) + *(b + i);
sum[i] = a[i] + b[i];
当数据连续存储在结构体中时,可以进行循环合并操作,即在一个循环内处理临近的数据,提高缓存命中率。
下述代码将 r、g、b 三个通道的处理合并到一个循环中。
// combine the rgb operation
/*
for (...)
{
pixels[i].r = ....;
}
for (...)
{
pixels[i].g = ....;
}
for (...)
{
pixels[i].b = ....;
}
*/
// cache friendly code
for (...)
{
pixels[i].r = ....;
pixels[i].g = ....;
pixels[i].b = ....;
}
本章节主要介绍了自动向量化的相关内容,其优缺点对比如下:
优点 | 缺点 |
---|---|
源代码采用传统 C/C++实现,便于理解 | 源代码可以跨平台,但是依赖编译器 |
无内联汇编或 intrinsic API 调用,可移植性高 | 源代码修改影响优化效果 |
现代编译器具备高级优化能力 | 编译器优化能力有限,难以完整利用硬件特性 |
总之,虽然通过自动向量化技术我们可以在一定程度上降低向量化编程难度,增强代码的可移植性,但是不能完全依赖于编译器,而且有时为了获得更高性能的代码,还是需要通过intrinsic甚至neon汇编进行编程。
五、参考资料
🌈我的分享也就到此结束啦🌈
如果我的分享也能对你有帮助,那就太好了!
若有不足,还请大家多多指正,我们一起学习交流!
📢未来的富豪们:点赞👍→收藏?→关注🔍,如果能评论下就太惊喜了!
感谢大家的观看和支持!最后,?祝愿大家每天有钱赚!!!欢迎关注、关注!