编译器优化那些事儿(15):数组访问依赖分析

引言

如果两个内存访问会触碰一段相同的内存,且其中至少有一个是写操作,则认为它们之间存在依赖(即不是独立的)。

本文假定数组总是 row-major 的,考察如下形式的循环:

for (i1 = d1; i1 <= u1; i1 += s1) {
    for (i2 = d2; i2 <= u2; i2 += s2) {
        // 第 n 层循环
        for (in = dn; in <= un; in += sn) {
      // 循环体
     }
    }
}

数组访问依赖分析要解决的核心问题:

(1)数组访问间是否独立

(2)能否通过等效变换,将有依赖关系的数组访问安排在一起

编译器作各种变换都需要保持内存访问间的依赖。数组访问依赖分析可用于判断 Loop Interchange、Loop Vectorization 等许多变换的合法性;通过将有依赖关系的数组访问安排在一起,可以提升程序的时间/空间局部性,相互独立的部分也能并行。

下面看一个例子:

for (i = 1; i < 100; i += 1)
    for (j = 0; j < i; j += 1)
        a[j, i+1] = a[j, i] + 1

作如下 Loop Interchange 变换以提升程序的空间局部性:

for (j = 0; j < 99; j += 1)
    for (i = j + 1; i < 100; i += 1)
        a[j, i+1] = a[j, i] + 1

数组访问的数学表述

for (i = d; i <= U; i += S)
for (j = 1; j <= (U - L + S) / S; j += 1)
    i = j * S - S + L

不失一般性,令迭代变量均为归一化的。

看一个例子:

for (i = 1; i <= 100; i += 1)
    for (j = 1; j <= 3 * i; j += 1)
        a[i, i + j * 2 + 1] += 1

静态数组访问 a[i, i + j * 2 + 1] 对应:

判断数组访问间是否独立


 

上述方程的求解是典型的整数规划问题,可参阅相关资料,不再赘述。此类问题是 NP 完全问题,无法精确求解。实践中通常根据一系列启发式的依赖关系测试作出判断。

仿射划分

为了将有依赖关系的动态数组访问安排在一起,我们需要找到一个划分:若两个动态数组访问之间存在依赖,则它们应当属于同一个集合。


 


 

每一个仿射划分都可以分解为以下几种基本转换之叠加,每种基本转换对应一类对源码的简单修改。

1. Fusion

转换前:

for (i = 1; i <= N; i++)
    Y[i] = Z[i]
for (i = 1; i <= N; i++)
    X[i] = Y[i]

划分:

转换后:

for (j = 1; j <= N; j++)
    Y[j] = Z[j]
    X[j] = Y[j]

2. Fission

转换前:

for (i = 1; i <= N; i++)
    Y[i] = Z[i]
    X[i] = Y[i]

划分:

转换后:

for (j = 1; j <= N; j++)
    Y[j] = Z[j]
for (j = 1; j <= N; j++)
    X[j] = Y[j]

3. Re-indexing

转换前:

for (i = 1; i <= N; i++)
    Y[i] = Z[i]
    X[i] = Y[i-1]

划分:

转换后:

if (N >= 1)
 X[1] = Y[0];
for (j = 1; j <= N - 1; j++)
 Y[j] = Z[j]
 X[j+1] = Y[j]
if (N >= 1)
 Y[N] = Z[N]

4. Scaling

转换前:

for (i = 1; i <= N; i++)
    Y[2*i] = Z[2*i];
for (i = 1; i <= 2*N; i++)
    X[i] = Y[i];

划分:

转换后:

for (j = 1; j <= 2*N; i++)
 if (j % 2 == 0)
  Y[j] = Z[j]
    X[j] = Y[j];

5. Reversal

转换前:

for (i = 1; i <= N; i++)
    Y[N-i] = Z[i];
for (i = 1; i <= N; i++)
    X[i] = Y[i];

划分:

转换后:

for (j = 1; j <= N; j++)
    Y[j] = Z[N-j];
    X[j] = Y[j];

6. Permutation

转换前:

for (i1 = 1; i1 <= N; i1++)
 for (i2 = 0; i2 <= M; i2++)
  Z[i1, i2] = Z[i1-1, i2]

划分:

转换后:

for (j1 = 0; j1 <= M; j1++)
 for (j2 = 1; j2 <= N; j2++)
  Z[j2, j1] = Z[j2-1, j1]

7. Skewing

转换前:

for (i1 = 0; i1 <= N+M-1; i1++)
 for (i2 = max(1, i1+N); i2 <= min(i1, M); i2++)
  Z[i1, i2] = Z[i1-1, i2-1]

划分:

转换后:

for (j1 = 1; j1 <= N; j1++)
 for (j2 = 1; j2 <= M; j2++)
  Z[j1, j2-j1] = Z[j1-1, j2-j1-1]

在 LLVM 中的应用

DependenceAnalysis pass 实现了基本的数组访问依赖分析,可用于依赖关系判断,也能进行简单的仿射划分。LoopFuse、LoopInterchange、LoopTiling 等 transformation pass 均需要 DependenceAnalysis 提供的信息才能工作。

在 MLIR 'affine' dialect 中,数组访问依赖分析的实现更加完备(因为在 MLIR 中数组访问的相关信息被完全保留了,而非像在 IR 中那样需要从 getelementptr 等指令反向推导出来),不仅支持依赖关系判断,更支持较复杂的仿射划分。MLIR 'affine' dialect 尤其适用于存在大量复杂的高维数组操作的场景,通过仿射划分自动分割出可以并行的部分,应用常常能因此得到成倍性能提升。

参考

1. Compilers: Principles, Techniques, & Tools, Second Edition. Jeffrey D. Ullman

2. Practical Dependence Testing. Goff, Kennedy, Tseng

3. 'affine' Dialect - MLIR

转载自鲲鹏社区:https://www.hikunpeng.com/zh/developer/techArticles/20231123-2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值