所谓流水线
前言
流水线设计模式(Pipeline Pattern)是一种通过分段逻辑电路来提高系统吞吐率和工作频率的设计方法。它将复杂的任务分解为多个子步骤,每个步骤处理数据后传递给下一个步骤,直到所有步骤完成处理
请不要管前面的屁话,搞些专业名词云遮雾罩。
在可并行的程序中,流水线设计可以将一个共需要 5 步顺序运行的工作,压缩到只需要 1 步的时间(5 并行)。设计,很神奇吧。
最近给同伙讲解流水线,画了一个还不错的 excalidraw ,不用在博客上太浪费了。
讨论场景
让我们假设这样一个计算过程,有一系列初始条件,随后基于一个随机条件条件迭代计算。其中的 +1,只是意味着需要基于原条件做一个消耗算力和时间的操作。
让我们再假设这样一种硬件设施,可供 5 线程并行计算,方便后面的讲述。
/* 初始条件 */
A = 0
B = 0
C = 0
D = 0
E = 0
/* 不断迭代计算 */
while( 1e20 次 )
{
A = x + 1; // x 表示一个迭代条件值,在这里可以视为随机
B = A + 1;
C = B + 1;
D = C + 1;
E = D + 1;
}
串行操作
如果将上述计算过程写成 C 语言单线程运行,那么显然类似如下,横轴看作时钟。
其中的 1、2、3、4、5 赛道可以看作上面的计算步骤。 在这个简单的示例下也可以看作用于并行运行的 CPU 核心,核心1 专注于 线程A,核心2 专注于 线程B......
可能有聪明的小伙伴要问了,既然是 CPU 核心,那应该可以线程调度啊?怎么能让核心闲着,跑满!必须跑满!
但是观察我们的讨论场景,线程B 的运行是需要等待 线程A 出结果的,线程C 的运行需要等待 线程B 出结果......
所以只能放任 CPU 摸鱼了吗?
并行操作
与串行操作一样,预先计算5次迭代,挑选出其中的部分值,作为并行操作的基础。
随后的每个计算周期就可以并行计算啦,CPU 满满当当。
另一个讨论场景
以上是一个小模块的串行转并行的思路。
让我们另外假设一个更科学的计算过程,来讨论所谓的流水线在自迭代中的可行性。这种是更大众意义上的迭代。
/* 不断迭代计算 */
while( 1e20 次 )
{
A = E + 1; // 线程A 需要 线程E 的结果
B = A + 1;
C = B + 1;
D = C + 1;
E = D + 1;
}
在这种场景下,因为自迭代的结果作为下一步的初始条件,完全没有流水线式优化的空间。如果按照我们此前的并行优化,将所有横线叠在一起,那么 线程A 应该基于5个周期后 线程E 的结果,需要对未来进行预测,如果增加这样的算法,反而会拖慢运行效率。
时间步 | 线程1 (A) | 线程2 (B) | 线程3 (C) | 线程4 (D) | 线程5 (E) | 依赖关系说明 |
---|---|---|---|---|---|---|
t=1 | Iter1.A | — | — | — | — | Iter1.A 依赖Iter0.E=0 (初始条件) |
t=2 | — | Iter1.B | — | — | — | 线程1需等待Iter1.E 完成才能继续 |
t=3 | — | — | Iter1.C | — | — | |
t=4 | — | — | — | Iter1.D | — | |
t=5 | — | — | — | — | Iter1.E | |
t=6 | Iter2.A | — | — | — | — | Iter2.A 依赖Iter1.E (此时已就绪) |
t=7 | — | Iter2.B | — | — | — | 线程2需等待Iter2.A 完成 |
t=8 | — | — | Iter2.C | — | — | |
t=9 | — | — | — | Iter2.D | — | |
t=10 | — | — | — | — | Iter2.E | |
t=11 | Iter3.A | — | — | — | — | Iter3.A 依赖Iter2.E |
总结
考虑运算模块的输入输出,如果运算模块的输入来自于外部,那么适用流水线的并行化;如果运算模块是自迭代的,则不适用。