矢量化编码_安卓应用编译优化的三种方式

矢量化编码_安卓应用编译优化的三种方式跟随《ExploitingSuperwordLevelParallelismwithMultimediaInstructionSets》这篇经典论文探究SLP矢量化的奥秘_算法slp指什么

0.Introduction

Superword Level Parallelism (SLP)矢量化是llvm auto-vectorization中的一种,另一种是loop vectorizer,详见于Auto-Vectorization in LLVM[1]。 它在2000年由Larsen 和 Amarasinghe首次作为basic block矢量化提出。SLP矢量化的目标是将相似的独立指令组合成向量指令,内存访问、算术运算、比较运算、PHI节点都可以使用这种技术进行矢量化。它和循环矢量化最大的差异在于,循环矢量化关注迭代间的矢量化机会,而SLP更关注于迭代内basic block中的矢量化的机会。

一个简单的小例子 case.cpp[1]

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

命令:clang++ case.cpp -O3 -S ;SLP在clang中是默认使能的,可以看到汇编中已出现使用矢量寄存器的fadd和fmul。

在这里插入图片描述

如果编译命令中加上选项-fno-slp-vectorize 或者 -mllvm -vectorize-slp=false 关闭该优化,则只能得到标量的版本。

在这里插入图片描述

让我们来跟随《Exploiting Superword Level Parallelism with Multimedia Instruction Sets》[2]这篇经典论文来探究一下SLP矢量化的奥秘。

1.原始SLP算法介绍

1.1概述

论文中用一张图来解释了SLP要做的事情:

在这里插入图片描述


原始SLP例子
[2]

这四条语句中的位置相对应的操作数,比如(b,e,s,x)可以pack到一个向量寄存器 Vb中,同样的,(c,f,t,y)可以pack到 Vc,(z[i+0]~z[i+3])可以到 Vd。然后可以利用simd指令进行相应的矢量化计算。最后根据Va 中(a,d,r,w)的被使用方式,可能还需要将他们从向量寄存器中load出来,称为unpack。

所以,如果pack操作数的开销 + 矢量化执行的开销 + unpack操作数的开销小于原本执行的开销,那就证明SLP矢量化具有性能收益[3]

1.2 优化场景

为了进一步说明SLP和循环矢量化在优化场景上的差异,论文[2]中给了两个例子(可以通过https://godbolt.org/z/EWr4zTc3P直接查看汇编情况)。

(1)对于原始循环 a,既可以通过 scalar expansion (a method of converting scalar data to match the dimensions of vector or matrix data.) 和 loop fission (the opposite of loop fusion: a loop is split into two or more loops. ) 后被转换为可以进行循环向量化的形式 b,一个induction和一个reduction;也可以经过unroll和rename之后变为 d 这样的形式,做SLP。但其实由于论文比较老了,目前llvm编译器对于a这样形式的循环可以直接做矢量化。

for (i=0; i<16; i++) {
  localdiff = ref[i] - curr[i];
  diff += abs(localdiff);
}


(a) Original loop.

for (i=0; i<16; i++) {
  T[i] = ref[i] - curr[i];
}

for (i=0; i<16; i++) {
  diff += abs(T[i]);
}


(b) After scalar expansion and loop fission.

for (i=0; i<16; i+=4) {
  localdiff = ref[i+0] - curr[i+0];
  diff += abs(localdiff);
    
  localdiff = ref[i+1] - curr[i+1];
  diff += abs(localdiff);    
  
  localdiff = ref[i+2] - curr[i+2];
  diff += abs(localdiff); 
    
  localdiff = ref[i+3] - curr[i+3];
  diff += abs(localdiff);
}


(c) Superword level parallelism exposed after unrolling.

for (i=0; i<16; i+=4) {
  localdiff0 = ref[i+0] - curr[i+0];
  localdiff1 = ref[i+1] - curr[i+1];
  localdiff2 = ref[i+2] - curr[i+2]; 
  localdiff3 = ref[i+3] - curr[i+3];
    
  diff += abs(localdiff0);
  diff += abs(localdiff1);
  diff += abs(localdiff2);
  diff += abs(localdiff3);  
}


(d) Packable statements grouped together after renaming.

(2)但是对于如下例子,循环向量化需要将do while循环转换为for循环,恢复归纳变量,将展开后的循环恢复为未展开的形式(loop rerolling)。而SLP只需要将计算 dst[{0, 1, 2, 3}] 的这四条语句组合成一条 使用向量化指令的语句即可。

do {
  dst[0] = (src1[0] + src2[0]) >> 1;
  dst[1] = (src1[1] + src2[1]) >> 1;
  dst[2] = (src1[2] + src2[2]) >> 1; 
  dst[3] = (src1[3] + src2[3]) >> 1;
    
  dst += 4;
  src1 += 4;
  src2 += 4;
}
while (dst != end);

看到这里,可以了解到哪些是SLP的优化机会。论文中提出了一种简单的算法来实现,简而言之是通过寻找independent(无数据依赖)、isomorphic(相同操作)的指令组合成一条向量化指令。

那么如何找呢?

1.3 算法描述

作者注意到如果被 pack 的指令的操作数引用的是相邻的内存,那么特别适合 SLP 执行。所以核心算法就是从识别 adjacent memory references 开始的。

当然寻找这样的相邻内存引用前也需要做一些准备工作,主要是三部分:(1) Loop Unrolling;(2) Alignment analysis;(3) Pre-Optimization(主要是一些死代码和冗余代码消除)。具体不展开讲。

接下来我们来看看核心算法,主要分为以下4步:

  1. Identifying Adjacent Memory References
  2. Extending the PackSet
  3. Combination
  4. Scheduling

伪代码[4]是:
S L P _ e x t r a c t : B a s i c B l o c k B → B a s i c B l o c k P a c k S e t P ← ∅ P ← f i n d _ a d j _ r e f s ( B , P ) P ← e x t e n d _ p a c k l i s t ( B , P ) P ← c o m b i n e _ p a c k s ( P ) r e t u r n s c h e d u l e ( B , [ ] , P ) \begin{array}{l} SLP\_extract: BasicBlock B \rightarrow BasicBlock \\ \quad \quad PackSet P \leftarrow \emptyset \\ \quad \quad P \leftarrow find\_adj\_refs (B, P) \\ \quad \quad P \leftarrow extend\_packlist (B, P) \\ \quad \quad P \leftarrow combine\_packs (P) \\ \quad \quad return \quad schedule (B,[], P) \\ \end{array} SLP_extract:BasicBlockBBasicBlockPackSetPPfind_adj_refs(B,P)Pextend_packlist(B,P)Pcombine_packs(P)returnschedule(B,[],P)

(1)第一步 find_adj_refs

先来看第一步:Identifying Adjacent Memory References
f i n d _ a d j _ r e f s : B a s i c B l o c k B × P a c k S e t P → P a c k S e t f o r e a c h   S t m t   s ∈ B   d o f o r e a c h   S t m t   s ′ ∈ B   w h e r e   s ≠ s ′   d o i f   h a s _ m e m _ r e f ( s ) ∧ h a s _ m e m _ r e f ( s ′ )   t h e n i f   a d j a c e n t   ( s , s ′ )   t h e n I n t   a l i g n   ← g e t _ a l i g n m e n t ( s ) i f   s t m t s _ c a n _ p a c k ( B , P , s , s ′ , a l i g n )   t h e n P ← P ∪ { ⟨ s , s ′ ⟩ } r e t u r n

今天的文章矢量化编码_安卓应用编译优化的三种方式分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/88125.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注