并行计算编程
并行计算编程
一、概述
并行计算(Parallel Computing)是指将原本按时间顺序逐步执行的计算任务,拆分为多个可以同时推进的子任务,并交由多个计算资源协同完成的一种计算模式。在 MATLAB 的编程实践中,并行计算最常见的落脚点并不是大规模分布式集群,而是单机环境下对多核 CPU、GPU 以及本地并行工作进程的利用。
对于日常科研与工程开发而言,并行计算的核心价值主要体现在以下几个方面:
- 缩短程序运行时间:对于重复实验、参数搜索、蒙特卡洛模拟、批量数据处理等任务,并行化往往能够显著降低总耗时。
- 提升硬件利用率:现代计算机普遍具备多核 CPU,但串行程序通常无法充分利用这些硬件资源。
- 支持更大规模计算:当问题规模持续增大时,单纯依赖串行执行常常难以满足效率要求。
- 改善算法实验流程:在智能优化、数值模拟、信号处理、图像处理等任务中,并行化可以提高实验吞吐量,使算法设计与调参过程更加高效。
从实践角度看,MATLAB 并行计算并不意味着必须引入复杂的高性能计算平台。对于多数使用者而言,单机多核并行已经能够覆盖大部分常见需求。因此,理解 MATLAB 并行编程,首先应建立以下基本认识:
- 并行计算的前提不是“代码很多”,而是“任务可拆分”;
- 并行计算的收益并非必然出现,而取决于任务粒度、数据传输成本与调度开销;
- 并行化应建立在程序逻辑正确、热点明确、数据依赖清晰的基础上。
二、MATLAB 并行计算的基本支撑
MATLAB 的并行能力主要建立在 Parallel Computing Toolbox 之上。对于单机环境,该工具箱已经能够支持以下典型能力:
- 本地多核 CPU 并行;
parfor并行循环;parfeval异步任务;spmd多工作进程协同执行;- GPU 计算;
- 本地分布式数组与部分高级并行结构。
如果进一步扩展到多机集群,则通常需要 MATLAB Parallel Server。不过对于以单机编程实践为主的场景,核心工具主要集中在以下三个层面:
1. 并行池(Parallel Pool)
并行池可以理解为一组由 MATLAB 启动的本地工作进程(worker)。这些 worker 独立于当前 MATLAB 主会话运行,负责执行被分发的并行任务。
2. 并行循环(parfor)
parfor 是最常用、最容易上手的并行工具。它适用于大量相互独立的循环迭代,是单机并行开发中最重要的入口。
3. 异步与显式控制工具
包括 parfeval、spmd 等,用于处理更复杂的任务调度、异步执行与进程间协作问题。
三、单机并行计算的核心思想
在单机环境下,并行计算的本质可以概括为:
将一个可以分解的任务拆分为多个相互独立或弱依赖的子任务,再交给本机多个 CPU 核心上的 worker 同时完成。
这一思想的关键并不在于“同时运行”本身,而在于任务划分方式。若一个问题可以自然拆成多个彼此独立的部分,那么就适合并行;反之,若计算过程存在强顺序依赖,则并行化的空间就会非常有限。
从程序结构上看,单机并行最典型的应用对象包括:
- 多次独立重复实验;
- 参数组合遍历;
- 多个测试样例批处理;
- 种群算法中的个体适应度评估;
- 批量文件处理;
- 大规模矩阵运算。
因此,在 MATLAB 中讨论并行编程,最关键的问题不是“如何调用并行语法”,而是“如何识别程序中可并行的部分”。
四、并行池与基本执行流程
1. 启动并行池
在 MATLAB 中,执行并行任务通常需要先建立并行池:
parpool若不指定 worker 数量,MATLAB 会按照本地默认配置建立并行池。也可以显式指定 worker 数:
parpool(4)上述语句表示启动 4 个本地 worker。
2. 查看当前并行池
gcp如果当前已有并行池,上述命令会返回当前池对象;若不存在,则可能按配置自动创建。
3. 关闭并行池
delete(gcp('nocreate'))其中,gcp('nocreate') 表示若池不存在则不新建,仅返回现有池对象。
4. 基本执行流程
单机并行任务通常遵循以下过程:
- 启动本地并行池;
- 将总任务划分为多个子任务;
- MATLAB 将子任务分发给各个 worker;
- worker 并行执行;
- 主会话收集并整理结果。
从编程实现上看,最常见的形式就是:先开池,再用 parfor 执行并行循环。
五、parfor:单机并行编程的核心工具
1. 基本形式
普通 for 循环写法如下:
for i = 1:N
result(i) = heavyFunction(i);
end若每次迭代互不依赖,则可改写为:
parfor i = 1:N
result(i) = heavyFunction(i);
end此时 MATLAB 会自动将循环迭代划分给多个 worker 并行执行。
2. parfor 的适用条件
parfor 并不是对所有 for 循环都适用。其成立前提主要包括:
- 每轮循环应尽量相互独立;
- 某一轮迭代不能依赖上一轮结果;
- 循环体内对变量的读写方式必须满足 MATLAB 的并行分析规则;
- 输出最好能够自然写成“第
i轮只写第i个位置”的形式。
例如下面的代码适合 parfor:
A = zeros(100,1);
parfor i = 1:100
A(i) = i^2;
end因为每轮只写 A(i),不同迭代之间不会互相冲突。
而下面的代码则不适合:
A = zeros(100,1);
parfor i = 2:100
A(i) = A(i-1) + i;
end因为第 i 次迭代依赖 A(i-1),存在顺序依赖关系,无法直接并行。
3. parfor 的变量分类思想
在 parfor 中,MATLAB 会自动分析循环中的变量使用方式。虽然初学阶段不必过度强调术语,但应理解几类典型变量:
- 切片变量(sliced variables):如
A(i)、C{i},每轮操作自身对应位置; - 广播变量(broadcast variables):循环中只读取、不修改的共享输入;
- 归约变量(reduction variables):如求和、求最大值这类可规约的变量;
- 临时变量(temporary variables):仅在每轮循环体内局部存在。
实际编写时,最稳妥的策略是:
让每次迭代只处理自己的输入,并只写自己的输出位置。
六、单机并行的典型应用场景
1. 重复实验并行
对于需要重复执行多次的独立实验,并行化非常自然:
numRuns = 50;
result = zeros(numRuns,1);
parfor k = 1:numRuns
result(k) = runExperiment(k);
end适用于:
- 随机实验重复运行;
- 不同初始化条件测试;
- 多次统计评估。
2. 参数遍历并行
参数扫描是 MATLAB 并行中最常见的应用之一:
params = 1:100;
scores = zeros(size(params));
parfor i = 1:length(params)
p = params(i);
scores(i) = runModel(p);
end适用于:
- 模型参数搜索;
- 算法参数敏感性分析;
- 超参数实验。
3. 种群算法适应度评估并行
在遗传算法、粒子群优化、差分进化等算法中,个体适应度评估通常彼此独立,因此特别适合并行:
popsize = 50;
fitness = zeros(popsize,1);
parfor i = 1:popsize
fitness(i) = objfun(pop(i,:));
end这一模式在算法研究中非常常见,也是 MATLAB 单机并行最有实践价值的应用之一。
4. 批量文件处理并行
files = dir('data/*.mat');
n = length(files);
result = cell(n,1);
parfor i = 1:n
data = load(fullfile(files(i).folder, files(i).name));
result{i} = processData(data);
end适用于:
- 多文件批处理;
- 多图像批量分析;
- 多数据样本预处理。
但需注意,若程序瓶颈主要来自磁盘读写,则并行收益可能有限。
七、从串行程序到并行程序的改造流程
在工程与科研实践中,并行化应遵循循序渐进的流程,而不是直接将所有 for 循环替换为 parfor。较为稳妥的路径如下。
1. 先保证串行程序正确
首先确保普通串行版逻辑无误:
for i = 1:N
result(i) = heavyFunction(i);
end2. 用计时工具建立基线
tic
for i = 1:N
result(i) = heavyFunction(i);
end
toc明确串行耗时之后,才能客观比较并行收益。
3. 找到真正的热点代码
并行化应优先作用于主要耗时部分。若并行化对象并非瓶颈,则总体提速可能并不明显。
4. 判断循环是否具备独立性
应重点分析:
- 是否存在前后迭代依赖;
- 是否存在共享变量写冲突;
- 是否可以将输出整理为数组或单元格中对应位置。
5. 改写为 parfor 并重新测试
parpool(4);
tic
parfor i = 1:N
result(i) = heavyFunction(i);
end
toc6. 对比结果正确性与性能
并行程序不仅需要更快,也必须保持结果正确。对于涉及随机过程的程序,还应重点检查可重复性问题。
八、并行不一定更快:性能收益的判断
并行计算并不会自动带来加速,原因在于其本身存在额外开销,包括:
- 启动 worker 的成本;
- 任务划分与调度成本;
- 数据从客户端传递到 worker 的成本;
- 结果回收与合并成本。
因此,当单个任务过小、计算量不足时,并行收益会被这些额外成本抵消,甚至出现“并行比串行更慢”的情况。
例如下面这种极轻量任务通常没有并行价值:
parfor i = 1:1000
a = i + 1;
end为了更合理地判断是否适合并行,可以从以下几个角度分析:
1. 任务粒度是否足够大
单轮迭代最好具有一定计算量,而不是只有几条简单语句。
2. 迭代次数是否足够多
并行池中的 worker 需要足够多的任务才能被充分利用。
3. 任务之间是否真正独立
若存在复杂同步或共享数据写入,则并行成本会明显增加。
4. 数据传输是否成为瓶颈
若每轮都需要复制巨大的共享数据,则可能“计算很快,搬运很慢”。
九、单机并行开发中的常见问题
1. 任务过轻导致提速不明显
这是最常见问题之一。并行更适合计算量大、每轮耗时显著的任务。
2. 循环中频繁输出信息
在 parfor 中使用 disp、fprintf 等输出语句,往往会导致输出顺序混乱,并拖慢执行速度。更稳妥的方式是:
- 将中间信息写入数组;
- 循环结束后统一打印;
- 必要时为每个 worker 单独记录日志。
3. 多 worker 同时写同一文件
多个 worker 同时写同一个文件容易产生冲突。建议改为:
- 每个 worker 写独立文件;
- 或统一收集结果后由主线程集中写出。
4. 大数组广播造成额外负担
例如:
bigData = rand(10000,10000);
parfor i = 1:100
result(i) = myFunc(bigData, i);
end若 bigData 很大,则 worker 可能需要重复接收同一份大数据,这会显著增加开销。更合理的做法包括:
- 尽量缩小每轮所需数据范围;
- 只传递必要子块;
- 将计算逻辑改为局部处理。
5. 随机数管理不当
并行随机实验中,若随机数流设置不合理,可能导致不同 worker 上出现重复随机序列,影响实验可信度。较简单的控制方式是:
parfor i = 1:30
rng(i);
result(i) = myRandomExperiment();
end这样至少可以使不同轮次对应不同随机种子,并提高实验可复现性。
十、GPU 计算与 CPU 并行的关系
除了基于多个 CPU worker 的并行外,MATLAB 还支持 GPU 加速。GPU 更适合:
- 大规模矩阵计算;
- 卷积、FFT、线性代数;
- 高并行度数值运算;
- 深度学习训练相关任务。
MATLAB 中的基本写法为:
A = gpuArray(rand(1000));
B = fft(A);
C = gather(B);其中:
gpuArray:将数据转移到 GPU;gather:将结果从 GPU 取回 CPU 内存。
需要明确的是,GPU 与 parfor 并不是简单替代关系:
parfor更适合大量彼此独立的任务并发;- GPU 更适合单个任务内部具有强数据并行特征的运算。
因此,在单机环境中,应根据问题结构而非“哪种方式更高级”来选择。
十一、总结
MATLAB 中的并行计算,本质上是对可拆分任务进行多资源协同求解的过程。在单机环境下,这一思想最直接的实现方式就是:利用本机多核 CPU,通过并行池与 parfor 对独立循环任务进行加速。
从编程实践角度看,MATLAB 单机并行的核心结论可以概括为以下几点:
- 并行化的前提是任务可拆分,而不是代码规模大;
parfor是单机并行开发中最重要、最常用的工具;- 并行不必然加速,其收益取决于任务粒度、独立性与数据传输成本;
- 开发中应优先保证程序正确、结果可复现,再考虑并行提速;
- 对于大量独立重复任务、参数遍历与种群适应度评估,并行化通常具有很高的实际价值。