概述

现代计算机拥有多核 CPU 已经不属于罕见,在本文,我们介绍两种简单的并行编程范式(Parallel paradigms):Data parallelism, 和 Pipeline parallelism, 并且通过简单的 C++ 代码来对例子进行实现。通过学习这些例子,你可以对如何有效利用 CPU 的每一个核心有一个初步的了解。

问题描述

对于一个有限长的数组 int nums[N], 我们要对 nums 的每一项套用这样一个多项式函数进行计算:

f(x) = (x+1)^2+3

我们希望以一种能够充分利用多核 CPU 中的每一个核心的方式来实现这样的计算过程。

并行策略

很显然,这样的一个任务是可以并行的,并且有多种并行的思路。

数据并行

其中一种思路是,假设我们有 m 个 workers, worker 之间的工作互不影响互不依赖,那么我们可以将 nums 数组拆分成 m 份,每份的长度最大为 $d=\lceil N / m \rceil$, m 个 workers 的编号分别记为 0, 1, …, m-1, 则编号为 i 的 worker 负责下标在 $[i*d, \min((i+1)*d, N) )$ 左闭右开区间内的 nums 元素。

举例来说,假设 N = 10, m = 4, 那么 worker 0 负责的下标为 { 0, 1, 2 }, worker 1 负责的下标为 { 3, 4, 5 }, worker 2 负责的下标为 { 6, 7, 8 }, worker 3 负责的下标为 { 9, 10 }.

标号为 i 的 worker 负责的下标构成的集合记为 I(i), 则 worker 在工作时就对每一个 I(i) 中的元素 j, 计算 f(nums[j]).

这种思路称为”数据并行“,它将任务按照数量分派给各个 worker, 类似于公司的 CEO 将任务拆分成若干个子任务,然后再将每一个子任务分派到具体的业务部门。

管线并行

管线并行可以类比于工厂加工流水线,这里的 worker 则相当于流水线上的工人,每个 worker 只完成自己的那一部分简单工作,做完了之后其产出会被该工人的下一位工人继续加工。

据一个不恰当的例子,考虑一条 iPhone 装配流水线,流水线的输入是 iPhone 主板,有 3 个 worker 分别负责:1)安装摄像头、2)安装电池,以及3)安装外壳,次序是 1)、2)、3)。

流水线每经过 t 秒钟会向右移动一格,这时对于一个 worker 而言,上一位 worker 加工过的半成品会随着传送带移到自己面前,而自己刚加工好的半成品也随着传送带移动到下一位 worker. 当然,每个 worker 必须在 t 秒钟内完成自己那部分加工工作。

对于上文中的批量计算任务(对 nums 数组的每个元素应用多项式函数 f),也可以应用这种并行思路,其中 nums 的元素就相当于流水线上的加工物,我们将多项式函数 f(x) = (x+1)^2+3 拆分成三个更加细粒度的函数:f1(x) = x+1, f2(x) = x^2, f3(x) = x+3, 那么 f 就可以简单地表示为 f1, f2, f3 的复合,然后,就可以将 nums 数组的元素依次放入流水线,依次经过 3 个 worker 分别负责的 3 道工序:f1 加工、f2 加工,以及 f3 加工。

为了使计算过程更加容易描述,我们会拿 nums 以及一些填充元素构建一个临时数组 buf,流水线会在这个 buf 上进行工作,假设 worker 的数量是 m, 则我们会在 buf 的最左边、最右边都填充 m-1 个 0 元素,中间是 nums 元素的原样复制。