假设你是一家大型团餐公司的后厨主管,要在午饭前供应一千份盒饭。如果只让一位厨师从头忙到尾,就算他手脚再麻利,学生们也会饿得前胸贴后背。聪明的做法,是组建一支“团餐协作团队”。团队有两种截然不同的运作方式:一种是把十位厨师请进同一个大厨房,大家共用同一个案板、同一口锅、同一批食材——谁放了盐、谁加了辣椒,所有人抬头就看得见;另一种是让厨师们分散在十个独立的小厨房里,彼此看不到对方的灶台,只能通过传菜口的小纸条和托盘互换半成品。前者,就是共享内存模型下的 OpenMP;后者,就是分布式内存模型下的 MPI。本文用这个“团餐团队”的故事贯穿始终,带你从零搞懂这两种并行编程模型的工作原理、核心 API、数据分发策略,以及实战中的选型智慧。

岗位关联

对 AI 从业者而言,OpenMP 是榨干单机多核算力的螺丝刀,MPI 是搭建大模型分布式训练的脚手架——掌握它们,你才能真正理解“算力如何被组织起来”。

目录
  1. 可视化演示
  2. 并行计算基础
  3. OpenMP编程模型
  4. OpenMP核心用法
  5. MPI编程模型
  6. MPI核心API
  7. 数据分发策略
  8. 混合编程实践
  9. 选型与优化
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

OpenMP基于共享内存通过编译制导指令实现多线程并行,MPI面向分布式内存通过消息传递构建并行程序。本文讲解... 切换模式查看不同维度。

本节配有交互动效(进入视口后自动加载,约 3KB)。

OpenMP:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现

并行计算基础

想象后厨只有一位叫小明的厨师。他独自做一千份盒饭要花十个小时,于是你请来了九位帮手。理论上,速度应该提高十倍,可实际却发现根本不是这么回事:小明把菜切好,其他人才能下锅炒;炒好的菜要装盒,装盒的速度又跟不上。总有一些环节没法拆成十份同时进行——切菜、装盒这种必须严格按顺序做的事,就是“串行部分”。并行计算的第一课,就是接受一个残酷的现实:无论你请多少帮手,串行部分都会拖住整个团队的后腿。

计算机里这个规律被称为 Amdahl 定律(阿姆达尔定律)。它用一条简洁的公式告诉我们:一个程序最多能加速多少倍,取决于“可并行部分”占了多少比例。假设一个程序中 80% 的代码可以并行运行,剩下 20% 必须串行,那么哪怕你调用一万个 CPU 核心,最高加速比也不会超过 5 倍。新增帮手的收益,会随着人数增长而快速递减。

回到团餐厨房:如果你发现瓶颈是装盒环节,请再多的炒菜师傅也没用,反而会让厨房变得拥挤。学习并行编程,第一件事就是学会用 Amdahl 定律“算一笔账”,判断某个环节是否真的值得并行化。

S=1(1−P)+PN\displaystyle S = \frac{1}{(1 - P) + \frac{P}{N}}
公式说明:S 是加速比(快了多少倍),P 是可并行部分占总体运行时间的比例,N 是处理器(帮手)数量。
  1. P可并行比例:表示代码中能被多个处理器同时执行的部分占总时间的比例,取值范围 0 到 1。
    0≤P≤10 \le P \le 1
  2. 1-P串行比例:无论多少处理器都只能顺序执行的部分,比如切菜、装盒。
    1−P1 - P
  3. P/N并行部分的理想耗时:把可并行的工作量均分给 N 个处理器后,这部分耗时缩为原来的 N 分之一。
    PN\frac{P}{N}
  4. 整体加速比串行部分加上并行部分的新耗时,用 1 除以它,就得到加速比 S。
    S=1(1−P)+PNS = \frac{1}{(1-P) + \frac{P}{N}}

OpenMP编程模型

既然共享大厨房有天然优势——所有厨师抬头就能看到共用案板上的食材,那何不让一群厨师在同一间厨房里协作?OpenMP 就是这样一个“共享案板”的编程工具。它面向共享内存硬件,允许你在 C/C++ 或 Fortran 代码里插入一行“厨师长口令”,比如 #pragma omp parallel,原本串行执行的代码段就会被多个线程同时执行。

这段口令的背后是 fork-join(分叉-合并)执行模型。程序启动时只有一个主线程,就像只有一位厨师长。遇到 #pragma omp parallel 时,厨师长“分叉”出一支线程队伍,大家各领一摊活儿;干完后线程归队,“合并”回主线程,继续往下走。与 MPI 的进程不同,这些线程共享同一个内存地址空间——案板上的食材和调料人人可见,但也正因如此,如果两个厨师同时去抢同一个盐罐,就会引发冲突。

共享案板的优势是“零拷贝”——线程之间传数据不用专门打包搬运,写一下内存大家就都看到了。代价则是同步责任重大,一旦两个厨师同时改同一本账本,数据就会错乱。下一节我们看看 OpenMP 如何用“归约”和“临界区”来化解这些冲突。

T(N)=Ts+TpN+To\displaystyle T(N) = T_s + \frac{T_p}{N} + T_o
公式说明:N 个线程并行执行的总耗时,等于串行部分耗时、并行部分均分耗时、以及线程创建与同步等开销之和。
  1. T_s串行部分耗时:无法并行、只能由主线程完成的部分,如读入数据、输出结果。
    TsT_s
  2. T_p/N并行的理想耗时:总工作量 T_p 均分给 N 个线程后,每个线程承担的部分。
    TpN\frac{T_p}{N}
  3. T_o并行开销:创建线程、唤醒线程、结束时同步合并所花费的额外时间。
    ToT_o
  4. 总耗时公式三者相加,就是实际并行执行的总时间。
    T(N)=Ts+TpN+ToT(N) = T_s + \frac{T_p}{N} + T_o

OpenMP核心用法

厨师长面对一千道菜,最常用的口号是“每人领一百道,开工!”。对应到 OpenMP,就是 #pragma omp parallel for:编译器自动把 for 循环的迭代分发给各个线程,每个线程处理其中一段。菜做完后,还需要汇总每道菜的成本。假如十个厨师各记在本子上,最后要算出总成本,OpenMP 提供了 reduction(归约)子句,让每个线程先算自己的局部和,再自动合并成最终结果。

归约操作本质上是把一个序列的所有元素累加起来。在 AI 训练里,最常见的归约就是损失值的求和:几千个小批量样本各自算出 loss,最后汇总成一个标量。

除了归约,OpenMP 还有 critical(临界区)指令。它像厨房里唯一的盐罐,规定“一次只允许一位厨师拿取”。数据竞争(data race)问题——多个线程同时读写同一变量且没有同步——就可以用 critical 或 atomic 来规避。归约适合“各算各的、最后合并”的场景,临界区适合“多个线程都要改同一份数据”的场景。选错工具,要么性能暴跌,要么数据错乱,就像厨师们排队领盐罐导致出菜速度骤降。

R=∑i=0n−1ai\displaystyle R = \sum_{i=0}^{n-1} a_i
公式说明:R 是最终汇总结果,a_i 是第 i 份局部结果,把 n 份局部结果全部累加就是归约。
  1. 求和符号表示把从 i=0 到 i=n-1 的所有 a_i 依次相加。
    ∑i=0n−1\sum_{i=0}^{n-1}
  2. 局部结果 a_i每个线程/进程独立计算出的部分和,好比每个厨师本子上记的那笔账。
    aia_i
  3. 归约结果 R所有局部结果累加后的最终值,一个标量。
    R=∑i=0n−1aiR = \sum_{i=0}^{n-1} a_i

MPI编程模型

现在换一种合作方式:十个厨师各自守着独立的小厨房,谁也看不见谁的案板。这就是分布式内存系统——每台机器(或每个进程)拥有完全独立的内存地址空间。MPI(Message Passing Interface,消息传递接口)就是为这种“隔空协作”设计的标准。每个参与计算的进程就像一个小厨房,有自己的门牌号(rank),也有自己的食材库存,想用别人的食材,只能通过传菜口递纸条和托盘。

MPI 程序的生命周期由四个关键调用掌管:MPI_Init 开启整个通信系统,MPI_Comm_size 告诉你总共有多少个小厨房在协作,MPI_Comm_rank 问你自己的门牌号是多少,MPI_Finalize 在任务结束时关闭通信系统。这很像团餐团队开工前的点名:先确认一共来了几个厨房,再确认自己是第几号,然后各就各位开始干活。

MPI 里没有“共享案板”,进程间的数据交换必须显式地通过消息完成。这听起来麻烦,却带来了极强的扩展性:小厨房可以分布在同一台机器的多个核上,也可以分布在不同机房的几千台机器上,彼此毫无差别。上一节 OpenMP 的共享案板适合单机,MPI 的独立厨房则打开了通往“超级计算机”的大门。

id∈{0,1,2,…,p−1}\displaystyle id \in \{0, 1, 2, \dots, p-1\}
公式说明:每个 MPI 进程在通信子(communicator)中都有一个唯一的编号 id,范围从 0 到进程总数 p-1。
  1. 通信子所有参与协作的进程组成的一个群体,MPI_COMM_WORLD 代表默认的全局通信子。
    communicator\text{communicator}
  2. 进程总数 pMPI_Comm_size 查询得到,表示团队里一共有几个小厨房。
    pp
  3. 进程编号 idMPI_Comm_rank 查询得到,是每个进程的门牌号,从 0 开始编号。
    id∈{0,1,…,p−1}id \in \{0, 1, \dots, p-1\}

MPI核心API

独立小厨房之间怎么传菜?MPI 提供了一套完整的“传菜工具”。最基础的是点对点通信:MPI_Send 像把托盘从 A 厨房的传菜口推出去,MPI_Recv 则表示 B 厨房在传菜口接住托盘。两者必须配对使用,发送方要指定“发给谁”(目标进程 rank)和“发了什么”(消息标签 tag),接收方也要指定“从谁那儿收”。

除了点对点,还有一批“集体通信”工具。MPI_Bcast 是广播:厨师长把一份菜单用大喇叭同时喊给所有厨房;MPI_Scatter 是分发:把一整堆食材按人头分给各个厨房;MPI_Gather 是收集:把各厨房做好的菜收拢回主厨房;MPI_Reduce 是归约:各厨房把份数报上来,汇总成一个总数。在大模型分布式训练中,MPI_Allreduce(全体归约)更是梯度同步的基石——所有 GPU 把自己的梯度“报数”并汇总后,再广播回每一张卡。

传菜口不会自动帮你搬货,每一条 MPI_Send 都需要对应一次 MPI_Recv,否则消息就可能“漂在通道里”。这正是 MPI 编程的难点:程序员必须亲手编排所有数据的流动。但也是它的魅力所在——你能精确控制每一次通信,为极致性能调优留足了空间。

tcomm=α+β⋅m\displaystyle t_{comm} = \alpha + \beta \cdot m
公式说明:发送一条长度为 m 的消息所需的时间,等于固定的启动开销 α 加上每单位数据的传输时间 β 乘以数据量 m。
  1. 启动开销 α无论消息多短,都要先花时间建立连接、封装信封,这部分固定不变。
    α\alpha
  2. 单位传输时间 β每传送一个字节数据所需的时间,取决于网络带宽。
    β\beta
  3. 数据量 m消息的长度,比如传 100 个浮点数,m 就是 100。
    mm
  4. 总通信时间启动开销加传输耗时,消息越长,通信时间越长。
    tcomm=α+βmt_{comm} = \alpha + \beta m

数据分发策略

一千份盒饭的食材要怎么分给十个厨房?最直接的办法是“按编号均分”:0 号厨房拿前 100 份,1 号厨房拿接下来 100 份……这叫静态分发(block distribution),每个厨房开局就拿到自己那一份,互不打扰。可问题是,有些食材难处理(比如切海参),有些很简单(比如洗土豆)。如果 0 号厨房分到的全是难处理的菜,它就会拖慢整体进度,而其他厨房早就闲着喝茶了。

动态分发(dynamic distribution)则换了一种思路:所有食材先堆在中心仓库,谁做完手头的活,就去仓库领取下一批。这类似于“按需领取”,天然解决负载不均的问题,但每次领取都要多一次“仓库协调”的通信开销。静态分发省通信,动态分发更均衡,工程师需要在两者之间权衡。

选择分发策略,本质上是在“通信代价”和“负载均衡”之间找平衡。任务整齐划一,就选静态分发,干净利落;任务长短不一,就选动态分发,宁可多花点通信费,也要让每个厨房都忙起来。

B=⌈Np⌉\displaystyle B = \left\lceil \frac{N}{p} \right\rceil
公式说明:把 N 个任务均分给 p 个进程,每个进程分到的块大小 B 等于 N 除以 p 后向上取整。
  1. 任务总数 N需要被并行处理的工作单元总量,比如一千份食材。
    NN
  2. 进程数 p参与计算的进程个数。
    pp
  3. 平均每进程任务数N 除以 p 得到理论上的平均分块大小。
    Np\frac{N}{p}
  4. 向上取整如果除不尽,则向上取整,保证所有任务都被分配完。
    B=⌈Np⌉B = \left\lceil \frac{N}{p} \right\rceil

混合编程实践

真实的高性能计算场景,往往不是“只开一种厨房”那么简单。一栋大楼里有十个大厨房,每个大厨房里又有十个灶台。最合理的安排是:厨房之间用 MPI 传递半成品(进程间消息传递),每个厨房内部的灶台用 OpenMP 分工合作(线程间共享内存)。这就是混合编程模型——MPI + OpenMP 两层并行。

以矩阵乘法为例:一个大矩阵 C = A × B,先按行把 A 分给 p 个进程,每个进程负责其中一段行;在每个进程内部,再调用 OpenMP 让多个线程并行计算局部矩阵乘积。编译时,mpicc 负责 MPI 部分的编译,-fopenmp 标志开启 OpenMP 支持;运行时,mpirun -np 4 启动 4 个进程,每个进程再通过环境变量 OMP_NUM_THREADS=4 创建 4 个线程。

混合编程并非银弹。进程间的通信成本、线程间的同步开销、以及两层调度之间的相互干扰,都可能让性能不升反降。最佳实践是先确定进程数和线程数的比例,再小步调优,就像先定好厨房之间传菜频率,再微调每个厨房内部的分工。

Ptotal=p×t\displaystyle P_{total} = p \times t
公式说明:混合编程的理论最大并行度等于进程数 p 乘以每个进程内的线程数 t。
  1. 进程数 pMPI 层创建的独立进程数量,每个进程有独立内存。
    pp
  2. 线程数 tOpenMP 层在每个进程内创建的线程数量,线程共享进程内存。
    tt
  3. 总并行度两者相乘,就是整个程序同时在跑的“工作单元”上限。
    Ptotal=p×tP_{total} = p \times t

选型与优化

站在后厨调度室,你该在什么时候选 OpenMP,什么时候选 MPI?答案取决于“厨房之间的距离”:如果任务只在一台机器上跑,OpenMP 是最优选——改动小、开销低、共享内存近乎零拷贝;如果任务要跨越几十台机器,MPI 是唯一选择——分布式内存让每个进程各管一摊数据,通过消息协作完成大规模计算。OpenMP 的短板是无法跨节点扩展,MPI 的短板是编程复杂度高、通信延迟大。

评判并行系统好坏,除了加速比,还要看并行效率。效率衡量的是“处理器算力的利用程度”,效率越高,说明增加处理器带来的收益越接近理想值。现实世界中,由于串行部分和通信开销的存在,效率总是随着处理器数量增加而下降,这就是并行计算中的“边际收益递减”。

优化方向因此变得清晰:一是提高 P,想办法让更多代码进入可并行区域;二是减少通信,把频繁的小消息合并成大消息;三是做好负载均衡,别让任何一个厨房闲着。两条路汇总成一句话——并行编程的本质,不是把活儿扔给很多人,而是科学地组织人力,让每一双手都物尽其用。

E=Sp=1p(1−P)+P\displaystyle E = \frac{S}{p} = \frac{1}{p(1-P) + P}
公式说明:并行效率 E 等于加速比 S 除以处理器数 p,它反映处理器资源的平均利用率。
  1. 加速比 S并行后相对串行快了多少倍,来自 Amdahl 定律。
    S=1(1−P)+PpS = \frac{1}{(1-P) + \frac{P}{p}}
  2. 效率定义加速比除以处理器数量,衡量每个处理器平均贡献了多少加速。
    E=SpE = \frac{S}{p}
  3. 展开后的效率代入加速比公式,得到效率关于 P 和 p 的表达式。
    E=1p(1−P)+PE = \frac{1}{p(1-P) + P}

AI 应用场景

大模型分布式训练中的梯度同步

阶段:模型训练(分布式数据并行)

解决的问题:大语言模型的参数动辄百亿,单张 GPU 显存放不下完整模型,训练数据也必须切分到多台机器。传统单机训练根本无法在合理时间内完成。MPI 的 MPI_Allreduce 被用来在每个训练迭代结束后,把所有 worker 计算的梯度做全局归约:先求和,再广播回每张卡,保证所有副本的模型参数保持一致。

具体案例:在 PyTorch 的 torch.distributed 后端中,NCCL 通信库的底层实现与 MPI 的 Allreduce 语义完全同构。训练 1750 亿参数的 GPT-3 时,数千张 GPU 通过类似“传菜统计”的机制,每步迭代同步梯度,最终完成大规模并行训练。

特征工程加速处理

阶段:数据预处理

解决的问题:AI 项目中 80% 的时间花在数据清洗与特征构造上,单核 CPU 处理千万级样本的特征提取非常缓慢。OpenMP 在多核 CPU 上对 for 循环做并行化,让特征计算、缺失值填充、归一化等操作同时跑在多个核心上,几乎不改变原有代码结构。

具体案例:某推荐系统团队用 #pragma omp parallel for 对用户行为日志逐行计算统计特征,在 32 核服务器上把特征工程耗时从 2 小时压缩到 8 分钟,且代码改动不超过 20 行。

气象数值预报的网格计算

阶段:科学计算与仿真

解决的问题:气象预报的物理方程是典型的网格计算:把地球表面划分为数百万个网格点,每个网格点都要独立计算温度、气压、湿度。传统串行程序在时效上完全不可用。MPI 把网格按经度切片分给不同计算节点,节点内再用 OpenMP 对每个切片的网格行做多线程并行。

具体案例:国家气象中心的数值预报系统中,水平分辨率 10 公里的全球模式使用数万 CPU 核心,MPI 负责跨节点数据交换,OpenMP 负责节点内加速,实现“分钟级”的短期预报更新。

Python 演示

python32 行
# -*- coding: utf-8 -*-import mathimport randomimport timeimport multiprocessing as mp  def local_sum(seq):    return sum(seq)  def parallel_sum(data, workers=4):    size = math.ceil(len(data) / workers)    chunks = [data[i * size:(i + 1) * size] for i in range(workers)]    with mp.Pool(processes=workers) as pool:        parts = pool.map(local_sum, chunks)    return sum(parts), parts  if __name__ == '__main__':    random.seed(42)    data = [random.random() for _ in range(1000000)]    t0 = time.time()    result, parts = parallel_sum(data, workers=4)    t1 = time.time() - t0    t2 = time.time()    expected = sum(data)    t3 = time.time() - t2    assert abs(result - expected) < 1e-6    print('parallel sum: %.4f, time: %.4fs' % (result, t1))    print('serial sum:   %.4f, time: %.4fs' % (expected, t3))    print('local sums:', [round(x, 4) for x in parts])
目的:用 Python 的 multiprocessing 模拟 MPI 风格的“分发—计算—归约”流程:把一百万个随机数均分给 4 个子进程求和,再在主进程合并局部结果,并与单线程求和对比耗时。
  1. size = math.ceil(len(data) / workers)按“块大小 = N/p”的公式切分数据,向上取整保证分完。
  2. chunks = [data[i * size:(i + 1) * size] for i in range(workers)]对应 MPI_Scatter,把数据广播分发到各进程。
  3. with mp.Pool(processes=workers) as pool:创建进程池,相当于 MPI_Init 拉起一批“小厨房”。
  4. parts = pool.map(local_sum, chunks)每个进程独立计算局部和,对应 local_sum 在各 worker 上运行。
  5. return sum(parts), parts用 sum 对局部和做归约,正是 MPI_Reduce 的 Python 版实现。
  6. assert abs(result - expected) < 1e-6校验并行结果与串行结果一致,防止数据竞争导致错误。

常见误区

误区我用了 `#pragma omp parallel for`,编译器就会自动处理好所有变量的并发问题,不会出错。

正解编译器只负责把循环迭代分发给线程,它不会替你做同步。多个线程同时写同一个共享变量,就像两个厨师同时抢一个盐罐,结果必然错乱。程序员必须主动用 critical、atomic 或 reduction 来管理共享变量的访问。在 C/C++ 中,这种错误往往不是直接崩溃,而是“偶发性数值错误”,极难排查。

为何常见:看到“并行”两个字,初学者容易把底层细节交给编译器,忽视了 OpenMP 本质上是“共享内存 + 显式同步”的模型。

误区MPI 程序里,不同进程的全局变量应该也是共享的吧?我直接访问别的进程的数组不就行了吗?

正解MPI 的进程拥有完全独立的内存地址空间——每个“小厨房”的案板都是私有的。进程 A 的 x = 3 不会影响进程 B 的 x。要想把 A 的值给 B,必须显式调用 MPI_Send / MPI_Recv 传递消息。试图跨进程直接读写内存,轻则读到无意义数据,重则段错误崩溃。

为何常见:从 OpenMP 或普通多线程编程切换到 MPI 时,最容易带着“全局变量谁都可见”的惯性思维,忽略了分布式内存的根本隔离性。

误区我有 64 核 CPU,`OMP_NUM_THREADS=64`,程序肯定比 4 线程快 16 倍吧?

正解Amdahl 定律告诉我们,串行部分和并行开销(线程创建、同步、内存带宽竞争)会共同压制加速比。当线程数超过某个阈值后,性能不再提升甚至下降——就像厨房太小,塞进一百个厨师只会互相撞肘。正确做法是用 Amdahl 定律估算上限,并用小规模实验扫描不同线程数,找到拐点。

为何常见:直觉上“更多人干活更快”,但忽略了串行瓶颈和资源争抢这些并行计算的物理约束。

误区我调用了 `MPI_Send`,消息就肯定能到达对方,我可以立刻去干别的事了。

正解MPI 的 MPI_Send 在缓冲区满时可能阻塞,等待接收方调用 MPI_Recv;如果发送方和接收方互相等待,还可能引发死锁。更常见的是“消息漂在通道里”——发送方在接收方还没调用 MPI_Recv 前就发了两条消息,接收方却按错误的顺序接收。每条消息都有消息标签(tag),发送与接收必须严格配对。

为何常见:初学者把消息传递类比成“打电话”,以为对话一旦拨出就完成了,忽略了 MPI 消息需要收发双方协同配合的本质。

误区MPI + OpenMP 是“双重并行”,两层加起来肯定比一层快,我拿到任何程序都直接上混合编程。

正解混合编程增加了一层调度和同步开销。如果进程间的通信频率很高,线程的存在反而会加剧共享资源的争抢。正确做法是先分析程序的可扩展性:若单机多核就能充分利用,用 OpenMP;若需要跨节点扩展,优先纯 MPI;只有在进程通信较稀疏、节点内共享数据访问频繁时,混合编程才真正发挥优势。

为何常见:“层数越多越强大”的直觉误导了工程师,忽略了每层并行都伴随额外的通信与调度成本。

面试题

Q1. OpenMP 基于哪种内存模型实现并行?

单选题 · easy

  • A.分布式内存模型
  • B.共享内存模型← 正确答案
  • C.数据库缓存模型
  • D.函数式编程模型

答案:B。OpenMP 就像共享一个大案板的厨房,所有线程共享同一内存地址空间,通过编译制导指令创建多线程。MPI 才是分布式内存模型,分布式内存和数据库缓存与本题无关。

Q2. MPI 中用于把根进程的数据均匀分发到各个进程的集体通信调用是哪个?

单选题 · easy

  • A.MPI_Bcast
  • B.MPI_Reduce
  • C.MPI_Scatter← 正确答案
  • D.MPI_Gather

答案:C。MPI_Scatter 是把一整堆食材分给各厨房的分发操作;MPI_Bcast 是广播同一份菜单,MPI_Reduce 是汇总归约,MPI_Gather 是收集数据。分发数据的语义对应 Scatter。

Q3. 某程序的可并行部分占 80%,串行部分占 20%,在 4 个处理器上运行的加速比约为多少?

单选题 · mid

  • A.1.25
  • B.2.5← 正确答案
  • C.3.2
  • D.4.0

答案:B。代入 Amdahl 定律:S = 1 / (0.2 + 0.8/4) = 1 / 0.4 = 2.5。即使有 4 个处理器,串行 20% 的部分拖累了整体,无法达到线性加速比 4.0,这与“装盒环节拖慢整个厨房”是同一个道理。

Q4. OpenMP 中,多个线程同时修改同一共享变量时,用哪个指令来保护临界区?

单选题 · mid

  • A.omp barrier
  • B.omp critical← 正确答案
  • C.omp flush
  • D.omp master

答案:B。omp critical 限定一次只有一个线程进入临界区,像让厨师们排队用盐罐;omp barrier 是屏障同步,omp flush 是刷新内存视图,omp master 仅指定主线程执行,都不能保证对共享变量修改的互斥。

Q5. 以下哪些情况会导致数据竞争(data race)?

多选题 · mid

  • A.多个线程同时读同一个变量
  • B.多个线程同时写同一个变量且无同步← 正确答案
  • C.一个线程写、另一个线程读同一变量且无同步← 正确答案
  • D.每个线程只访问自己的私有变量

答案:BC。数据竞争要求“至少一个写操作 + 无同步机制”。多个线程同时读不会冲突,各自访问私有变量更不会共享冲突。只有写-写或读-写同时发生且缺乏同步时才会竞争,就像两个厨师同时抢同一个盐罐。

Q6. 在 MPI 中,进程的 rank 表示什么?

单选题 · mid

  • A.进程的 CPU 优先级
  • B.进程在通信子中的唯一编号← 正确答案
  • C.进程占用的内存大小
  • D.进程的系统 PID

答案:B。rank 是通信子内从 0 开始的整数编号,相当于每个小厨房的门牌号,用于指定消息的发送目标和来源。CPU 优先级、内存大小和系统 PID 都不是 rank 的含义。

Q7. 哪些场景下,动态分发策略优于静态分发策略?

多选题 · hard

  • A.各任务的计算量差异很大← 正确答案
  • B.任务运行时间完全相等
  • C.任务耗时不可预测,随机性大← 正确答案
  • D.所有任务都在同一进程内完成

答案:AC。动态分发是“谁做完谁领下一批”,当任务计算量差异大或耗时不可预测时能有效负载均衡;若任务时间完全相同,静态均分反而省去协调开销。任务在同一进程内完成与分发策略无关。

Q8. OpenMP 的 reduction 子句的主要作用是什么?

单选题 · easy

  • A.删除不再使用的共享变量
  • B.把各线程的局部结果合并为最终值← 正确答案
  • C.降低线程的优先级
  • D.阻止编译器优化代码

答案:B。reduction 让每个线程先算自己的局部和,再自动合并,就像各厨师把账本报给主厨汇总。它既保证了正确性(避免竞争),又保持了并行效率,是归约操作的标准实现。

Q9. 以下哪些场景适合 MPI + OpenMP 混合编程?

多选题 · hard

  • A.计算集群的节点间需要频繁交换数据← 正确答案
  • B.每个节点内有多核 CPU 且共享数据访问频繁← 正确答案
  • C.问题规模小到单核几分钟就能跑完
  • D.任务需要扩展到几千个节点的大规模并行← 正确答案

答案:ABD。混合编程适合“跨节点用 MPI 通信 + 节点内用 OpenMP 共享内存”的结构。A、D 依赖 MPI 的跨节点能力,B 依赖 OpenMP 的多核共享能力。问题规模过小则并行开销反而得不偿失,不适合混合编程。

Q10. 若程序中串行部分占 20%,处理器数量趋于无穷大时,最大加速比的极限是多少?

单选题 · extreme

  • A.2.5
  • B.5.0← 正确答案
  • C.10.0
  • D.无穷大

答案:B。Amdahl 定律中,当 N 趋于无穷时,P/N 趋于 0,加速比 S 趋近于 1/(1-P) = 1/0.2 = 5。即使有无限多个处理器,串行 20% 的瓶颈也把加速比锁死在 5 倍——这就是“装盒环节决定整个厨房效率”的数学极限。