Lec 11并行化(Parallelization)
Lec 11 并行化(Parallelization)
内容:为何并行、并行执行、并行化编译器、依赖分析、提升并行机会
TL;DR
- 自动并行化首先是合法性问题:循环迭代间的真依赖、反依赖和输出依赖决定执行顺序能否改变。
- 距离向量与整数约束用于判断循环携带依赖;私有化、归约识别和幺模循环变换可以暴露新的并行维度。
- 合法不等于有利可图:Amdahl 定律、负载均衡、并行区启停与同步开销共同决定应在内层还是外层循环并行。
1. 为什么需要并行
单处理器性能增长放缓(晶体管仍按摩尔定律增长,但 SPECint 单核性能曲线趋平),多核已成现实。要继续获得性能收益,必须利用并行——由程序员显式定义,或由编译器自动抽取。
定义(并行的三个核心问题)
- Amdahl 定律:计算分顺序部分 与可并行部分 ,则 ,,最大加速比 。
- 负载均衡 (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 循环。
定义(迭代空间、仿射循环嵌套、字典序)
层循环 ⟹ 维离散迭代空间 (iteration space)(归一化:步长=1),迭代记为坐标 。顺序执行即字典序 (lexicographic order): 当存在 使 且 。仿射循环嵌套 (affine loop nest):循环界与数组下标都是常量、循环不变量、外层下标的整数线性函数 ⟹ 迭代空间可表为一组线性不等式。
数据空间 (data space): 维数组 ⟹ 维离散笛卡尔空间(超立方)。
4. 依赖分析(Dependence Analysis)
定义(数据依赖的三种类型)
对动态实例 、,数据依赖存在 当且仅当: 或 至少一个是写、二者引用同一变量、 先于 执行。
- 真依赖 (true / RAW):
a=…; …=a(先写后读) - 反依赖 (anti / WAR):
…=a; a=…(先读后写) - 输出依赖 (output / WAW):
a=…; a=…(先写后写)
4.1 距离向量(Distance Vectors)
定义(距离向量)
若存在从迭代 到 的数据依赖,则距离 。
例题(一维与二维距离向量)
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 层循环可并行的判据)
对所有依赖 ,第 层循环可并行 当且仅当: 中有一个 > 0,或者 全为 0。
例:A[I]=A[I]+1(dv=[0])可并行;A[I+1]=A[I]+1(dv=[1])不可并行。
方法二:整数规划法 (Integer Programming)
定义(整数规划表述)
判断是否存在整数向量 使 ( 整数矩阵、 整数向量)。对 A[I+1]=A[I]+1:问是否存在 、、。因 非仿射,拆成 与 两个子问题;等式 拆成 与 两个不等式;任一子问题有解即有依赖。
对 层仿射嵌套,解 个形如 的子问题。
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) 后内层可并行。
定义(幺模变换:交换 / 反转 / 倾斜)
用矩阵变换 :
定理(幺模变换的合法性)
矩阵 的幺模变换合法 当且仅当:对所有依赖向量 , 的第一个非零分量为正(保证变换后仍满足字典序的依赖方向)。
5.2 粒度与过程间并行
- 粒度:内层循环并行代价高(并行区启停开销、大量同步,甚至变慢)。对策:工作量太小则不并行,或转成外层并行(如循环转置 loop transpose 把可并行维换到外层)。
- 过程间并行化:函数调用使循环不可并行(减少可用并行、内层并行多)。对策:过程间分析(需更新所有分析、复杂昂贵)或内联(沿用现有分析,但代码膨胀大)。注意带副作用容器(如
HashSet.insert)的循环——迭代未必独立,即便能并行执行也未必所有并行执行给出相同结果。
我的理解
并行化要连续回答两个问题:变换后是否仍与串行程序等价,以及额外管理成本是否小于并行收益。依赖分析只回答第一个,剖析与成本模型才能回答第二个。
6. 依赖分析与循环并行化小结
- 多核时代需并行;Amdahl 定律限制加速比,需顾及负载均衡与粒度;循环是主战场。
- 仿射循环嵌套 → 迭代空间(线性不等式)与数据空间;依赖分三种(真/反/输出),用距离向量刻画。
- 识别 FORALL:无循环携带依赖即可并行;判据用距离向量法或整数规划法(解 个 子问题)。
- 提升并行:标量/数组私有化、归约识别、归纳变量反变换;幺模变换(交换/反转/倾斜,合法性看 首非零为正)调整循环;权衡粒度(外层并行优于内层)与过程间策略。