Archipelago

Lec 11并行化(Parallelization)

讲义完成更新于 2026-09-16

Lec 11 并行化(Parallelization)

内容:为何并行、并行执行、并行化编译器、依赖分析、提升并行机会

TL;DR

  • 自动并行化首先是合法性问题:循环迭代间的真依赖、反依赖和输出依赖决定执行顺序能否改变。
  • 距离向量与整数约束用于判断循环携带依赖;私有化、归约识别和幺模循环变换可以暴露新的并行维度。
  • 合法不等于有利可图:Amdahl 定律、负载均衡、并行区启停与同步开销共同决定应在内层还是外层循环并行。

1. 为什么需要并行

单处理器性能增长放缓(晶体管仍按摩尔定律增长,但 SPECint 单核性能曲线趋平),多核已成现实。要继续获得性能收益,必须利用并行——由程序员显式定义,或由编译器自动抽取。

定义(并行的三个核心问题)

  • Amdahl 定律:计算分顺序部分 TsT_s 与可并行部分 TpT_p,则 T(n)=Ts+Tp/nT(n) = T_s + T_p/n,T(∞)=TsT(\infty) = T_s,最大加速比 Ts+TpTs\dfrac{T_s + T_p}{T_s}。
  • 负载均衡 (load balancing):工作均摊到各处理器,使并行执行时都保持忙碌。
  • 粒度 (granularity):同步间并行区域的大小,即计算(有用功)与通信(开销)之比。

并行类型:指令级 (ILP,靠调度与硬件)、任务级 (TLP,多靠手工)、循环级/数据并行 (LLP,手工或编译器)、流水线 (硬件/流式)、分治 (递归)。重点是循环:90% 时间在 10% 代码(多为循环),并行后易负载均衡且相对易分析。


2. 并行执行(Parallel Execution)

程序员定义的并行循环:FORALL(无循环携带依赖,完全并行)、FORACROSS(有部分循环携带依赖)。

FORPAR I=0..N: A[I]=A[I]+1 的块分布 (block distribution) 与 SPMD(单程序多数据)映射:把迭代按 Iters=ceil(N/NUMPROC) 分给各处理器,配合 Barrier() 同步;或 fork 函数 ParallelExecute(func1, P)。

并行线程基础:用 pthread_create 创建 OS 线程(盼望落在独立核上),创建有开销(独立栈、OS 分配);常用线程池 (thread pool)——开始即建好(= 核数)个线程,顺序段时 N−1 个在屏障上空转,并行段各跑一个函数,完成后回到屏障。


3. 并行化编译器:迭代空间与数据空间

目标:从 FOR 循环中找出 FORALL 循环。

定义(迭代空间、仿射循环嵌套、字典序)

NN 层循环 ⟹ NN 维离散迭代空间 (iteration space)(归一化:步长=1),迭代记为坐标 ıˉ=[i1,…,in]\bar{\imath} = [i_1,\dots,i_n]。顺序执行即字典序 (lexicographic order):ıˉ<ȷˉ\bar{\imath} < \bar{\jmath} 当存在 cc 使 i1=j1,…,ic−1=jc−1i_1=j_1,\dots,i_{c-1}=j_{c-1} 且 ic<jci_c < j_c。仿射循环嵌套 (affine loop nest):循环界与数组下标都是常量、循环不变量、外层下标的整数线性函数 ⟹ 迭代空间可表为一组线性不等式。

数据空间 (data space):MM 维数组 ⟹ MM 维离散笛卡尔空间(超立方)。


4. 依赖分析(Dependence Analysis)

定义(数据依赖的三种类型)

对动态实例 ii、jj,数据依赖存在 当且仅当:ii 或 jj 至少一个是写、二者引用同一变量、ii 先于 jj 执行。

  • 真依赖 (true / RAW):a=…; …=a(先写后读)
  • 反依赖 (anti / WAR):…=a; a=…(先读后写)
  • 输出依赖 (output / WAW):a=…; a=…(先写后写)

4.1 距离向量(Distance Vectors)

定义(距离向量)

若存在从迭代 ii 到 jj 的数据依赖,则距离 d=j−id = j - i。

例题(一维与二维距离向量)

A[I]=A[0]+1   → dv=[0]      A[I+1]=A[I]+1 → dv=[1]
A[I]=A[I+2]+1 → dv=[2]      A[I]=A[I]+1   → dv=[0]
二维:
A[I,J]=A[I,J-1]+1 → dv=[0,1]    A[I,J]=A[I+1,J]+1 → dv=[1,0]
A[I,J]=A[I-1,J+1]+1 → dv=[1,-1]
A[i,j]=A[i,j-1]+A[i-1,j] → dv={[0,1],[1,0]}

4.2 识别 FORALL 循环

定义(循环携带依赖与可并行性)

循环携带依赖 (loop-carried dependence) 是跨越循环边界的依赖。**若无循环携带依赖,则循环可并行。**对每对访问同一数组的访问,若第一个访问在某迭代引用的位置、第二个访问在之后某迭代也引用,则存在数据依赖(同一数组也可自依赖——输出依赖)。

方法一:距离向量法

定理(第 i 层循环可并行的判据)

对所有依赖 d=[d1,…,di,…,dn]d = [d_1,\dots,d_i,\dots,d_n],第 ii 层循环可并行 当且仅当:d1,…,di−1d_1,\dots,d_{i-1} 中有一个 > 0,或者 d1,…,did_1,\dots,d_i 全为 0。

例:A[I]=A[I]+1(dv=[0])可并行;A[I+1]=A[I]+1(dv=[1])不可并行。

方法二:整数规划法 (Integer Programming)

定义(整数规划表述)

判断是否存在整数向量 ıˉ\bar{\imath} 使 Aıˉ≤bˉA\bar{\imath} \le \bar{b}(AA 整数矩阵、bˉ\bar{b} 整数向量)。对 A[I+1]=A[I]+1:问是否存在 iw≠iri_w \ne i_r、0≤iw,ir≤50\le i_w,i_r\le5、iw+1=iri_w+1=i_r。因 iw≠iri_w\ne i_r 非仿射,拆成 iw<iri_w<i_r 与 ir<iwi_r<i_w 两个子问题;等式 iw+1=iri_w+1=i_r 拆成 iw+1≤iri_w+1\le i_r 与 ir≤iw+1i_r\le i_w+1 两个不等式;任一子问题有解即有依赖。

对 nn 层仿射嵌套,解 2n2n 个形如 i1=j1,…,ik−1=jk−1,ik≶jki_1=j_1,\dots,i_{k-1}=j_{k-1}, i_k \lessgtr j_k 的子问题。


5. 提升并行机会(Increasing Parallelization)

  • 标量私有化 (Scalar Privatization):消除反/输出循环携带依赖——把标量在局部上下文赋值(每迭代私有 Xtmp),或扩展为数组 Xtmp[i];循环后需"最终赋值"维持正确值(if(i==n) X=Xtmp 或 X=Xtmp[n])。
  • 归约识别 (Reduction Recognition):X = X + A[i] 是循环携带真依赖,但若运算可结合且结果在循环内不被使用,可并行——各处理器先算局部 Xtmp[myPid],屏障后由 0 号汇总。
  • 归纳变量 (Induction Variable):A[i]=2^i 强度削弱成 t=1; …A[i]=t; t=t*2; 反而引入循环携带依赖——并行化需做反向操作:识别归纳变量、把它重写成循环变量的函数。
  • 数组私有化 (Array Privatization):类似标量但分析更复杂(数组数据依赖分析查"是否访问同一位置",数据流分析查"是否访问同一值")。

5.1 循环变换与幺模变换(Unimodular Transformations)

某些循环原样不可并行,可先变换。例 A[i,j]=A[i,j-1]+A[i-1,j](dv={[0,1],[1,0]})经循环倾斜 (loop skewing) 后内层可并行。

定义(幺模变换:交换 / 反转 / 倾斜)

用矩阵变换 ıˉnew=A ıˉold\bar{\imath}_{new} = A\,\bar{\imath}_{old}:

交换 [0110],反转 [−1001],倾斜 [1011]\text{交换 } \begin{bmatrix}0&1\\1&0\end{bmatrix},\quad \text{反转 } \begin{bmatrix}-1&0\\0&1\end{bmatrix},\quad \text{倾斜 } \begin{bmatrix}1&0\\1&1\end{bmatrix}

定理(幺模变换的合法性)

矩阵 AA 的幺模变换合法 当且仅当:对所有依赖向量 vv,AvAv 的第一个非零分量为正(保证变换后仍满足字典序的依赖方向)。

5.2 粒度与过程间并行

  • 粒度:内层循环并行代价高(并行区启停开销、大量同步,甚至变慢)。对策:工作量太小则不并行,或转成外层并行(如循环转置 loop transpose 把可并行维换到外层)。
  • 过程间并行化:函数调用使循环不可并行(减少可用并行、内层并行多)。对策:过程间分析(需更新所有分析、复杂昂贵)或内联(沿用现有分析,但代码膨胀大)。注意带副作用容器(如 HashSet.insert)的循环——迭代未必独立,即便能并行执行也未必所有并行执行给出相同结果。

我的理解

并行化要连续回答两个问题:变换后是否仍与串行程序等价,以及额外管理成本是否小于并行收益。依赖分析只回答第一个,剖析与成本模型才能回答第二个。

6. 依赖分析与循环并行化小结

  • 多核时代需并行;Amdahl 定律限制加速比,需顾及负载均衡与粒度;循环是主战场。
  • 仿射循环嵌套 → 迭代空间(线性不等式)与数据空间;依赖分三种(真/反/输出),用距离向量刻画。
  • 识别 FORALL:无循环携带依赖即可并行;判据用距离向量法或整数规划法(解 2n2n 个 Aıˉ≤bˉA\bar{\imath}\le\bar{b} 子问题)。
  • 提升并行:标量/数组私有化、归约识别、归纳变量反变换;幺模变换(交换/反转/倾斜,合法性看 AvAv 首非零为正)调整循环;权衡粒度(外层并行优于内层)与过程间策略。