美文网首页
[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