SIMD 编译器优化

2023-04-23 language

简介

如下是一个简单的示例,可以通过 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 中的介绍。