美文网首页
[062][译]Auto-Vectorization in LL

[062][译]Auto-Vectorization in LL

作者: 王小二的技术栈 | 来源:发表于2020-12-11 11:41 被阅读0次

前言

最近遇到一个性能问题,与Auto-Vectorization in LLVM有关,翻译一下官方介绍
http://llvm.org/docs/Vectorizers.html

简单一句话概括:将代码通过编译矢量优化,编译成运行速度更快的机器码。

一、Auto-Vectorization in LLVM

LLVM有两个矢量器:The Loop Vectorizer 循环矢量器(在循环上运行)和The SLP Vectorizer SLP矢量器。这些矢量器关注不同的优化机会,使用不同的技术。SLP矢量器将代码中发现的多个标量合并为向量,而循环向量器则扩展循环中的指令,以在多个连续迭代中操作。

默认情况下,循环矢量器和SLP矢量器都处于启用状态。

二、The Loop Vectorizer

2.1 使用方法

默认情况下启用循环矢量器,但可以使用命令行标志通过clang禁用它:

$ clang ... -fno-vectorize  file.c
Command line flags

循环矢量器使用成本模型来确定最佳矢量化因子和展开因子。但是,矢量器的用户可以强制矢量器使用特定的值。“clang”和“opt”都支持下面的标志。

用户可以使用命令行标志“-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语言拓展

2.2 诊断

许多循环无法矢量化,包括具有复杂控制流、不可分割类型和不可分割调用的循环。循环矢量器生成优化注释,可以使用命令行选项查询这些注释,以识别和诊断循环矢量器跳过的循环。

优化备注使用以下方式启用:

-Rpass=loop vectorize标识成功矢量化的循环。

-Rpass missed=loop vectorize标识矢量化失败的循环,并指示是否指定了矢量化。

-Rpass analysis=loop vectorize标识导致矢量化失败的语句。如果另外提供了-fsave优化记录,则可能会列出导致矢量化失败的多种原因(这种行为在将来可能会发生变化)。

考虑以下循环:

#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]

而命令行-rpassanalysis=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用户手册》

2.3 功能

LLVM循环矢量器有许多功能,允许它对复杂的循环进行矢量化。

Loops with unknown trip count

循环矢量器支持具有未知行程计数的循环。在下面的循环中,迭代的开始点和结束点是未知的,循环向量器有一种机制来对不从零开始的循环进行矢量化。在这个例子中,“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;
}

Runtime Checks of Pointers

在下面的例子中,如果指针A和B指向连续的地址,那么将代码矢量化是非法的,因为A的某些元素将在从数组B读取之前被写入。

有些程序员使用'restrict'关键字来通知编译器指针是分离的,但是在我们的示例中,循环向量器无法知道指针A和B是唯一的。循环向量器通过放置代码来处理这个循环,在运行时检查数组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变量由循环的连续迭代使用。通常,这会阻止矢量化,但矢量器可以检测到“sum”是一个缩减变量。变量“sum”变成一个整数向量,在循环结束时,数组的元素被加在一起以创建正确的结果。我们支持许多不同的归约运算,例如加法、乘法、异或和或。

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 Conversion

循环向量器能够“展平”代码中的IF语句并生成单个指令流。循环向量器支持最内层循环中的任何控制流。最里面的循环可能包含IFs、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;
}
Pointer Induction Variables

这个例子使用标准c++库的“累加”函数。这个循环使用C++迭代器,这些指针是指针,而不是整数索引。循环矢量器检测指针感应变量,并对该循环进行矢量化。这个特性很重要,因为许多C++程序使用迭代器。

int baz(int *A, int n) {
  return std::accumulate(A, A + n, 0);
}
Reverse Iterators

循环向量器可以对倒数循环进行矢量化。

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

循环向量器可以将代码矢量化,使其成为分散/聚集内存的标量指令序列。

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

在许多情况下,成本模型会通知LLVM这是不有益的,并且LLVM只会在强制使用“-mllvm-force vector width=#”时将这些代码矢量化。

Vectorization of Mixed Types

循环矢量器可以对混合类型的程序进行矢量化。矢量化成本模型可以估计类型转换的成本,并决定矢量化是否有益。

int foo(int *A, char *B, int n) {
  for (int i = 0; i < n; ++i)
    A[i] += 4 * B[i];
}
Global Structures Alias Analysis

对全局结构的访问也可以矢量化,使用别名分析来确保访问不会出现别名。还可以在对结构成员的指针访问上添加运行时检查。

支持许多变体,但是有些依赖于未定义行为被忽略的变体(就像其他编译器一样),仍然没有被矢量化。

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

int foo() {
  for (int i = 0; i < 100; ++i)
    Foo.A[i] = Foo.B[i] + 100;
}
Vectorization of function calls

循环矢量器可以对内部数学函数进行矢量化。有关这些函数的列表,请参见下表。

请注意,如果库调用访问外部状态(如“errno”),优化器可能无法将与这些内部函数对应的数学库函数矢量化。为了更好地优化C/C++数学库函数,使用“-fNO数学ErrNO”。

循环向量器知道目标上的特殊指令,并将对包含映射到指令的函数调用的循环进行矢量化。例如,如果SSE4.1 roundps指令可用,则以下循环将在Intel x86上矢量化。

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

现代处理器具有多个执行单元,只有具有高度并行性的程序才能充分利用机器的整个宽度。循环向量器通过执行循环的部分展开来提高指令级并行度(ILP)。

在下面的示例中,整个数组被累加到变量“sum”中。这是低效的,因为处理器只能使用一个执行端口。通过展开代码,循环向量器允许同时使用两个或多个执行端口。

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

循环向量器使用成本模型来决定何时展开循环是有益的。展开循环的决定取决于寄存器压力和生成的代码大小。

Epilogue Vectorization

在对循环进行矢量化时,如果循环行程计数未知或不能平均分配矢量化和展开因子,则通常需要一个标量余数(epilogue)循环来执行循环的尾部迭代。当向量化和展开因子较大时,行程计数较小的循环可能会将大部分时间花费在标量(而不是矢量)代码中。为了解决这个问题,内环矢量器被增强了一个特性,允许它用矢量化和展开因子组合对尾数循环进行矢量化,这使得小行程计数循环更有可能仍然在矢量化代码中执行。下图显示了带有运行时检查的典型尾声矢量化循环的CFG。如图所示,控制流的结构避免了重复运行时指针检查,并优化了具有非常小跳闸计数的循环的路径长度。


2.3 性能提升

本节将在一个简单的基准测试gcc循环上显示Clang的执行时间。这个基准测试是来自doritnuzman的GCC自动矢量化页面的循环集合。

下面的图表比较了GCC-4.7、ICC-13和Clang SVN在-O3下有无循环矢量化,针对“corei7-avx”,运行在Sandybridge iMac上。Y轴以毫秒为单位显示时间。越低越好。最后一列显示了所有内核的几何平均值。


和配置相同的Linpack pc。结果是Mflops,越高越好。


可以看到Clang如果无循环矢量化,被GCC和ICC吊打,最好还是开启。

2.4 持续发展方向

对LLVM循环向量器的流程进行建模和基础设施升级。

三、The SLP Vectorizer

3.1 详情

SLP向量化的目标是将相似的独立指令组合成向量指令。内存访问、算术运算、比较运算、PHI节点都可以使用这种技术进行矢量化。

例如,以下函数对其输入(a1,b1)和(a2,b2)执行非常相似的操作。基本块向量器可以将这些组合成向量操作。

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矢量器处理代码自下而上,跨越基本块,搜索标量进行组合。

3.2 用法

默认情况下,SLP矢量器处于启用状态,但可以使用命令行标志通过clang禁用它:

$ clang -fno-slp-vectorize file.c

四、尾巴

处理了好多性能优化的问题,有锁竞争的问题,有代码逻辑的问题,有跨进程等待的问题,还有各色各样的问题,我是第一次遇到相同的代码在同一个型号的cpu下运行速度有差异的问题,最后分析出来是编译器优化的问题。虽然分析的过程很曲折,但是结果很满意,自己的格局又变大了一点。

相关文章

网友评论

      本文标题:[062][译]Auto-Vectorization in LL

      本文链接:https://www.haomeiwen.com/subject/erteiktx.html