CPU向量化诊断技巧

众所周知,CPU向量化是ClickHouse在查询性能上如此出色的重要原因之一。在编译过程中,编译器会尽量将循环代码自动向量化,以提升运行性能。然而编译器自动向量化需要满足比较苛刻的条件。那么站在程序员的角度,如何判断一段代码有没有被编译器自动向量化呢?本文将按照不同编译器介绍相关诊断技巧。

clang诊断

编译选项

-Rpass=loop-vectorize : 找到被自动向量化的代码

-Rpass-missed=loop-vectorize: 探测自动向量化失败的循环

-Rpass-analysis=loop-vectorize: 分析导致自动向量化失败的原因

示例

代码如下:

#include <iostream>
#include <cstdlib>


void foo(int* A, int len) {
	for (int i = 0; i < len; i++) {
		switch(A[i]) {
			case 0: A[i] = i*2; break;
			case 1: A[i] = i;   break;
			default: A[i] = 0;
		}
	}
}

int main()
{
	const int N = 1024;
	int A[N];
	for (int i=0; i<N; ++i)
		A[i] = std::rand() % 3;

	foo(A, N);
	std::cout << A[N-1] << std::endl;
	return 0;
}

使用上述编译选项编译之:

clang++ -Rpass-missed=loop-vectorize -Rpass-analysis=loop-vectorize -scalable-vectorization=on 1.cpp -g  -O2

输出如下:

1.cpp:7:3: remark: loop not vectorized: loop contains a switch statement [-Rpass-analysis=loop-vectorize]
                switch(A[i]) {
                ^
1.cpp:6:2: remark: loop not vectorized [-Rpass-missed=loop-vectorize]
        for (int i = 0; i < len; i++) {
        ^
1.cpp:20:10: remark: loop not vectorized: call instruction cannot be vectorized [-Rpass-analysis=loop-vectorize]
                A[i] = std::rand() % 3;
                       ^
1.cpp:19:2: remark: loop not vectorized: instruction cannot be vectorized [-Rpass-analysis=loop-vectorize]
        for (int i=0; i<N; ++i)
        ^
1.cpp:19:2: remark: loop not vectorized [-Rpass-missed=loop-vectorize]
1.cpp:7:3: remark: loop not vectorized: loop contains a switch statement [-Rpass-analysis=loop-vectorize]
                switch(A[i]) {
                ^
1.cpp:6:2: remark: loop not vectorized [-Rpass-missed=loop-vectorize]
        for (int i = 0; i < len; i++) {
        ^


从诊断信息中可知,main函数中循环因为使用了 rand函数而无法被自动向量化。foo函数中的循环因为使用了switch case而无法被自动向量化。后续便可根据这些诊断信息进行针对性的优化。

gcc诊断

编译参数

-fopt-info-vec-all: 找到被自动向量化的代码

-fopt-info-vec-missed: 探测自动向量化失败的循环

-fdump-tree-vect-all: 分析导致自动向量化失败的原因

其他诊断工具

介绍两个更直接的诊断工具。可根据其逆向生成的汇编代码判断是否有被自动向量化。作为上述诊断方法的补充手段

  • objdump: 命令行工具, objdump -CS a.out
  • https://godbolt.org/: web工具,可在线生成c/cpp对应的汇编代码

阻碍编译器自动优化的因素

  • 数组大小不可被向量大小整除
  • 包含不可被向量化的函数,如 std::rand
  • 大数据类型,int64_t, double: 占用8B, 而常用的sse4.2指令集中向量化寄存器长度才16B, 向量化优化得不偿失。
  • 未对齐的数据:读写未按照向量化寄存器长度对齐的内存区域可能会降低执行速度
  • 循环中包含复杂的分支代码:如switch-case, if-else。
  • 编译器没有足够的指针对齐和别名信息:编译器在无法确定两个指针是否指向同一地址的情况下会按照最坏情况处理,此时很可能不会自动向量化
  • 指令集中缺少合适类型的向量操作,如SSE4.1之前,没有 32位整数乘法和整数除法:这是由硬件决定的。

总结

本文介绍了几种诊断cpu自动向量化的工具,包括clang, gcc, objdump, godbolt等。最后总结了几种常见的阻碍编译器自动优化的因素。

参考

《Optimizing software in C++ An optimization guide for Windows, Linux, and Mac platforms》

https://llvm.org/docs/Vectorizers.html#slp-vectorizer