LLVM 项目博客

LLVM 项目新闻和来自实战的细节

新的循环向量化器

我想简要介绍一下循环向量化器的开发情况。LLVM 现在有两个向量化器:循环向量化器,它作用于循环;基本块向量化器,它优化直线代码。这些向量化器侧重于不同的优化机会,并使用不同的技术。BB 向量化器将代码中的多个标量合并为向量,而循环向量化器将原始循环中的指令扩展以操作多个连续的循环迭代。

LLVM 的循环向量化器现已推出,将对许多人有用。它默认情况下未启用,但可以通过 clang 使用命令行标志 "-mllvm -vectorize-loops" 启用。我们计划在 LLVM 3.3 版本中默认启用循环向量化器。

循环向量化器可以提高许多循环的性能,包括一些 GCC 无法向量化的循环。在一个基准测试中,Linpack-pc,循环向量化器将单精度矩阵高斯消元的性能从 984 MFlops 提升到 2539 MFlops,性能提升了 2.6 倍。向量化器还将“GCC 向量化示例”基准测试 的几何平均值提升了 2.15 倍。

LLVM 循环向量化器具有许多功能,使其能够向量化复杂的循环。本文中描述的大多数功能都作为 LLVM 3.2 版本的一部分提供,但某些功能是在截止日期后添加的。以下是一个关于 LLVM 循环向量化器可以向量化的循环的小例子。

int foo(int *A, int *B, int n) {
unsigned sum = 0;
for (int i = 0; i < n; ++i)
if (A[i] > B[i])
sum += A[i] + 5;
return sum;
}

在这个例子中,循环向量化器使用了一些非平凡的功能来向量化循环。变量 “sum” 被循环的连续迭代使用。通常,这会阻止向量化,但向量化器可以检测到 “sum” 是一个归约变量。变量 “sum” 变为一个整数向量,并在循环结束时将数组的元素加在一起以创建正确的结果。我们支持多种不同的归约运算,例如乘法。

循环向量化器需要克服的另一个挑战是循环中控制流的存在。循环向量化器能够“扁平化”代码中的 IF 语句并生成单个指令流。另一个重要功能是向量化具有未知循环次数的循环。在这个例子中,'n' 可能不是向量宽度的倍数,向量化器必须以标量代码执行最后几次迭代。保留循环的标量副本会增加代码大小。
上面的循环被编译成下面的 ARMv7s 汇编序列。注意,IF 结构被 "vcgt" 和 "vbsl" 指令替换。

LBB0_3:
vld1.32 {d26, d27}, [r3]
vadd.i32 q12, q8, q9
subs r2, #4
add.w r3, r3, #16
vcgt.s32 q0, q13 , q10
vmla.i32 q12, q13, q11
vbsl q0, q12, q8
vorr q8, q0, q0
bne LBB0_3

在下面的第二个示例中,循环向量化器必须使用两个更多功能才能向量化循环。在下面的循环中,迭代的起始和结束点是未知的,循环向量化器有一种机制可以向量化不从零开始的循环。此功能对于从 Fortran 转换的循环很重要,因为 Fortran 循环从 1 开始。
此循环中的另一个主要挑战是内存安全。在我们的示例中,如果指针 A 和 B 指向连续的地址,那么向量化代码是非法的,因为 A 的一些元素将在从数组 B 读取之前被写入。

一些程序员使用 “restrict” 关键字来通知编译器指针是不重叠的,但在我们的示例中,循环向量化器无法知道指针 A 和 B 是唯一的。循环向量化器通过放置代码来处理此循环,该代码在运行时检查数组 A 和 B 是否指向不重叠的内存位置。如果数组 A 和 B 重叠,则执行循环的标量版本。

void bar(float *A, float *B, float K, int start, int end) {
 for (int i = start; i < end; ++i)
   A[i] *= B[i] + K;
}

上面的循环被编译成这个 X86 汇编序列。注意在支持 AVX 的系统上使用 8 宽 YMM 寄存器。


LBB1_4:
vmovups (%rdx), %ymm2
vaddps  %ymm1, %ymm2, %ymm2
vmovups (%rax), %ymm3
vmulps  %ymm2, %ymm3, %ymm2
vmovups %ymm2, (%rax)
addq   $32, %rax
addq   $32, %rdx
addq   $-8, %r11
jne LBB1_4

在最后一个例子中,我们没有看到循环,因为它隐藏在标准 c++ 库的 "accumulate" 函数中。此循环使用 c++ 迭代器,它们是指针,而不是像我们在前面的例子中看到的整数索引。循环向量化器检测指针归纳变量,可以向量化此循环。此功能很重要,因为许多 C++ 程序使用迭代器。

int baz(int *A, int n) {
return std::accumulate(A, A + n, 0);
}
上面的循环被编译成这个 x86 汇编序列。

LBB2_8:
vmovdqu (%rcx,%rdx,4), %xmm1
vpaddd %xmm0, %xmm1, %xmm0
addq $4, %rdx
cmpq %rdx, %rsi
jne LBB2_8

循环向量化器是一个与目标无关的 IR 级优化,它依赖于不同后端的特定于目标的信息。它需要选择最佳向量宽度,并确定向量化是否值得。用户可以使用命令行标志 "-mllvm -force-vector-width=X" 强制使用特定的向量宽度,其中 X 是向量元素的数量。目前,只有 X86 后端提供详细的成本信息,而其他目标使用不太准确的方法。
循环向量化器的开发工作尚未完成,向量化器还有很长的路要走。我们计划添加额外的向量化功能,例如自动对齐缓冲区、函数调用的向量化以及对用户编译指令的支持。我们还计划提高生成代码的质量。