LLVM - 矢量化器

LLVM 有两个矢量化器(vectorizer):循环矢量化器(Loop Vectorizer),作用于循环,以及 SLP 矢量化器(SLP Vectorizer)。这两种矢量化器关注不同的优化机会并采用不同的技术。SLP 矢量化器会将代码中出现的多个标量合并为向量,而循环矢量化器则会扩展循环体中的指令,实现对多次连续迭代的共同处理。

Loop Vectorizer 和 SLP Vectorizer 默认都是开启的。

循环矢量化器(The Loop Vectorizer)

用法

循环矢量化器默认启用,但可以通过 clang 命令行选项关闭:

$ clang ... -fno-vectorize file.c
命令行选项

循环矢量化器基于 cost model(代价模型)来决定最优的矢量化因子和展开因子。不过,用户可以通过命令行强制指定这两者。clangopt 都支持下述参数。

用户可以用命令行参数 -force-vector-width 控制矢量化 SIMD 宽度。

$ clang -mllvm -force-vector-width=8 ...
$ opt -loop-vectorize -force-vector-width=8 ...

用户可以用命令行参数 -force-vector-interleave 控制展开因子。

$ clang -mllvm -force-vector-interleave=2 ...
$ opt -loop-vectorize -force-vector-interleave=2 ...
Pragma 循环提示指令(loop hint directives)

#pragma clang loop 指令允许为随后的 for、while、do-while 或 C++11 范围 for 循环显式指定矢量化提示。可以启用或禁用矢量化和展开;还可以手动指定矢量宽度和展开计数。例如:

#pragma clang loop vectorize(enable) interleave(enable)
while(...) {
  ...
}

下面的示例通过指定矢量宽度和展开因子隐式启用矢量化和展开:

#pragma clang loop vectorize_width(2) interleave_count(2)
for(...) {
  ...
}

详细信息见 Clang 语言扩展

诊断

并非所有循环都能被矢量化,包括复杂控制流、不可矢量化类型、不可矢量化调用等。循环矢量化器会产生优化备注(optimization remarks),可通过命令行选项查询,以定位和分析未被矢量化的循环。

启用优化备注的方法:

  • -Rpass=loop-vectorize 标识成功矢量化的循环。
  • -Rpass-missed=loop-vectorize 标识矢量化失败的循环,并指明是否已请求矢量化。
  • -Rpass-analysis=loop-vectorize 标识导致矢量化失败的代码行。如果同时加上 -fsave-optimization-record,还可以列出多种失败原因(未来可能改变)。

举例:

#pragma clang loop vectorize(enable)
for (int i = 0; i < Length; i++) {
  switch(A[i]) {
  case 0: A[i] = i*2; break;
  case 1: A[i] = i;   break;
  default: A[i] = 0;
  }
}

命令行 -Rpass-missed=loop-vectorize 的输出如下:

no_switch.cpp:4:5: remark: loop not vectorized: vectorization is explicitly enabled [-Rpass-missed=loop-vectorize]

-Rpass-analysis=loop-vectorize 会指出 switch 语句不能矢量化。

no_switch.cpp:4:5: remark: loop not vectorized: loop contains a switch statement [-Rpass-analysis=loop-vectorize]
  switch(A[i]) {
  ^

要输出行号和列号,可加上 -gline-tables-only -gcolumn-info 参数。参见 Clang 用户手册

特性

LLVM 循环矢量化器有许多功能,方便矢量化复杂循环。

未知循环次数的循环

循环矢量化器支持未知循环次数的循环。如下例所示,startend 点未知,矢量化器提供机制来矢量化非零起始点的循环。比如 ‘n’ 可能不是矢量宽度的整数倍,矢量化器会为剩余的迭代保留标量代码,这会稍微增加代码体积。

void bar(float *A, float* B, float K, int start, int end) {
  for (int i = start; i < end; ++i)
    A[i] *= B[i] + K;
}
指针的运行时检查

如下例,若 A 和 B 指向连续地址,则矢量化可能导致读写冲突。部分编程者会通过 restrict 关键字声明 A、B 相互独立,但如果未声明,矢量化器会自动生成运行时检查,如果 A、B 有重叠就执行标量版本:

void bar(float *A, float* B, float K, int n) {
  for (int i = 0; i < n; ++i)
    A[i] *= B[i] + K;
}
归约(reductions)

如例子所示,sum 在循环内每次迭代间有关联,正常情况下无法矢量化。但矢量化器可识别它为 reduction 变量,将其当作一个向量进行累加,最后执行归约操作得出结果。支持加、乘、异或、与、或等多种操作。

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

浮点型归约通过 -ffast-math 开启时支持。

自增变量(inductions)

如例子所示,循环变量 i 的每步值写入数组,可被矢量化。

void bar(float *A, int n) {
  for (int i = 0; i < n; ++i)
    A[i] = i;
}
If 语句转换

矢量化器可将 if 语句“扁平化”,合并为单条指令流。支持内层循环体中任意控制流,包括嵌套 if、else 乃至 goto。

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;
}
指针型自增变量

如使用 C++ STL accumulate 的循环,变量为指针而非整数,自增指针同样可矢量化。这对大量基于迭代器的 C++ 程序尤为重要。

int baz(int *A, int n) {
  return std::accumulate(A, A + n, 0);
}
逆序迭代器

矢量化器能处理倒序的循环。

void foo(int *A, int n) {
  for (int i = n; i > 0; --i)
    A[i] += 1;
}
Scatter / Gather

矢量化器可矢量化包含 scatter/gather 操作的循环(即访问模式非简单递增)。

void foo(int *A, int *B, int n) {
  for (intptr_t i = 0; i < n; ++i)
      A[i] += B[i * 4];
}

许多场景下 cost model 会判断这样做不划算,只有通过 -mllvm -force-vector-width=# 强制开启时才会矢量化。

混合类型矢量化

矢量化器支持含多种类型变量的循环,cost model 会评估类型转换代价再决定是否矢量化。

void foo(int *A, char *B, int n) {
  for (int i = 0; i < n; ++i)
    A[i] += 4 * B[i];
}
全局结构别名分析

对全局结构体成员的访问同样可矢量化,由别名分析确保不会冲突,也可对指针成员做运行时检查。

许多变种得到支持,但对于依赖未定义行为的(如其他编译器实现方式),仍未矢量化。

struct { int A[100], K, B[100]; } Foo;

void foo() {
  for (int i = 0; i < 100; ++i)
    Foo.A[i] = Foo.B[i] + 100;
}
函数调用的矢量化

矢量化器能矢量化部分内建数学函数,如下表所列:

pow
exp
exp2
sin
cos
sqrt
log
log2
log10
fabs
floor
ceil
fma
trunc
nearbyint
fmuladd

但注意,如果对应的 math 库函数可能访问全局状态(如 errno),优化器或许无法矢量化这些函数。可通过 -fno-math-errno 优化 math 库调用。

矢量化器还会识别目标平台特殊指令,例如在 Intel x86 支持 SSE4.1 roundps 时,循环调用 floor 会被矢量化:

void foo(float *f) {
  for (int i = 0; i != 1024; ++i)
    f[i] = floorf(f[i]);
}

很多 math 函数只有在用指定 veclib 时才被矢量化。可用 clang 的 -fveclib 标志指定加速库,如 accelerate,libmvec,massv,svml,sleef,darwin_libsystem_m,armpl,amdlibm

$ clang ... -fno-math-errno -fveclib=libmvec file.c
矢量化中的部分循环展开

现代处理器有多执行单元,只有并行度高的程序才能充分利用硬件。循环矢量化器会进行部分展开(unrolling),以增加指令级并行度。

如例,累加数组到 sum,仅用单寄存器端口执行。展开后可同时用多端口,加快累加速度。

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

矢量化器会用 cost model 判断是否值得展开,依据寄存器压力及代码体积。

结尾(Epilogue)矢量化

当循环次数未知或不能整除矢量宽度/展开因子时,剩余迭代(epilogue)需用标量代码执行。若矢量化/展开因子较大,小循环可能大部分时间都跑在 epilogue。为此,内层矢量化器加了 epilogue 矢量化特性,使小循环也能尽量运行到 vector 代码路径上。如下图所示(CFG 结构),巧妙避免了重复的指针检查,对小 trip count 路径进行了优化。

_images/epilogue-vectorization-cfg.png

提前退出矢量化

若有单一提前退出(early exit),相关 block 以谓词方式处理,vector loop 总能从 latch 退出。如已提前退出,则 vector 循环的 successor block(如 middle.split)将跳到 early exit block(如 vector.early.exit),负责递补计算所有需要提前返回的变量;否则继续选择正常退出或剩余标量循环路径。

_images/vplan-early-exit.png

性能

本节展示 Clang 在简单测试中的执行时间:gcc-loops。这是从 GCC 自动矢量化 页面 收录的循环。

下图比较了 GCC-4.7、ICC-13 及 Clang-SVN(包含/不包含循环矢量化)在 -O3 及 corei7-avx 下的执行时间(单位毫秒,越低越好,最后一栏为所有 kernel 的几何平均)。

_images/gcc-loops.png

Linpack-pc 同样配置下的测试,结果为 Mflops(越高越好):

_images/linpack-pc.png

持续开发方向

矢量化计划(Vectorization Plan)
用于建模 LLVM 循环矢量化器流程和基础设施升级。

SLP 矢量化器(The SLP Vectorizer)

细节

SLP(superword-level parallelism)矢量化的目标是将类似、独立的指令合并为一条向量指令。内存访问、算术、比较、PHI 节点等都能用此方法矢量化。

例如下例,(a1, b1) 和 (a2, b2) 参与了类似运算,SLP 矢量化器可将它们合并成向量计算:

void foo(int a1, int a2, int b1, int b2, int *A) {
  A[0] = a1*(a1 + b1);
  A[1] = a2*(a2 + b2);
  A[2] = a1*(a1 + b1);
  A[3] = a2*(a2 + b2);
}

SLP 矢量化器自底向上(bottom-up)跨 basic block 搜索可合并的标量。

用法

SLP 矢量化器默认启用,也可通过 clang 命令行关闭:

$ clang -fno-slp-vectorize file.c

Sandbox 矢量化器(The Sandbox Vectorizer)

Sandbox Vectorizer 是一个实验性的框架,用于在 Sandbox IR 之上构建模块化的矢量化流水线,侧重于测试和开发便捷性。

原文地址:https://llvm.org/docs/Vectorizers.html

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

csdddn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值