简介
如下是一个简单的示例,可以通过 godbolt.org 中的 clang 17.0.0 进行查看。
void foobar(int size, int *lhs, int *rhs)
{
for (int i = 0; i < size; i++) {
lhs[i] = (rhs[i] + 1) >> 1;
}
}
不同的编译参数会生成不同代码。
-O3 -fno-tree-vectorize -fno-unroll-loops
mov edi, dword ptr [rdx + 4*rcx]
inc edi
sar edi
mov dword ptr [rsi + 4*rcx], edi
inc rcx
-O3 -mavx2 -fno-unroll-loops
vmovdqu ymm1, ymmword ptr [rdx + r8]
vpsubd ymm1, ymm1, ymm0
vpsrad ymm1, ymm1, 1
vmovdqu ymmword ptr [rsi + r8], ymm1
add r8, 32
对于 SIMD 指令来说,一次可以处理 256Bits=32Bytes
,也就是每次处理 4 个 int
类型,从而以简单粗暴的方式进行优化。
其中汇编指令可以从 Intel 64 and IA-32 Architectures Software Developer’s Manual 中查看。
编译参数
编译时需要通过参数指定支持的 SIMD 类型,常见的 SSE4.2 为 -msse4.2
,AVX2 对应 -mavx2
,AVX512 对应 -mavx512f -mavx512bw
等等,也可以通过 -march=native
让编译器根据处理器选择最好的 CPU 架构和 flags
进行编译。
还可以通过 gcc --target-help
命令查看支持的所有参数,以及 gcc -march=native -c -Q --help=target
当前机器支持的架构。
指定了编译参数之后,代码中就可以根据宏来分别进行处理,常见的如 __SSE__
、__AVX2__
、__PCLMUL__
等等,可以在添加参数之后通过如下命令查看。
gcc -msse3 -dM -E - < /dev/null | egrep "SSE|AVX" | sort
例如在 CMake 中可以通过如下方式指定。
SET(CMAKE_C_FLAGS "-msse4.2 -mpclmul ${CMAKE_C_FLAGS}")
SET(CMAKE_CXX_FLAGS "-msse4.2 -mpclmul ${CMAKE_CXX_FLAGS}")
优化查看
有时候需要确定编译器是否做了向量化、循环展开、内联,如果是循环展开,那么对应的系数是多少,对于 clang 编译器来说,就可以通过 -Rpass*
参数进行查看,例如编译时增加 -O3 -Rpass-analysis=loop-vectorize -Rpass=loop-vectorize -Rpass-missed=loop-vectorize
参数。
对于上述的代码有如下的输出。
Output of x86-64 clang 17.0.1 (Compiler #1)
<source>:3:2: remark: vectorized loop (vectorization width: 4, interleaved count: 2) [-Rpass=loop-vectorize]
3 | for (int i = 0; i < size; i++) {
| ^
对于 gcc 来说可以使用 -ftree-vectorize -ftree-vectorizer-verbose=X
参数,详细查看 Auto-vectorization in GCC 中的介绍。
其中 vectorization width: 4
表示使用的是 ymm
寄存器(156bits=32bytes=4ints),每次并行操作 4 个整数;interleaved count: 2
表示循环展开,也就是同时使用两个 ymm
寄存器。这样一个循环内可以同时处理 8 个整形。
对于 clang 可以通过 #pragma clang loop vectorize_width(4)
宏进行指定,更多内容可以查看 Auto-Vectorization in LLVM 中的介绍。