2、理解并行计算:Flynn分类法、数据并行与任务并行、GPU为何适合并行计算

2.1 从串行到并行:一个老工程师的视角

说实话,我刚入行那会儿,CPU还是单核的天下。写程序就是一条指令一条指令地跑,简单直接。但后来发现,摩尔定律快撑不住了——频率上不去,功耗压不住。怎么办?大家开始往芯片里塞更多的核心。

这就引出一个问题:什么样的计算能被并行化?

我个人习惯用Flynn分类法来思考这个问题。它把计算机架构分成四类,但对我们做GPU编程来说,真正需要关心的只有两类:

  • SISD(单指令单数据):就是传统的CPU串行执行。一条指令处理一个数据。比如你写个a = b + c,CPU一次就干这一件事。
  • SIMD(单指令多数据):一条指令同时处理多个数据。GPU就是典型的SIMD架构。比如你对1000个像素做同样的颜色变换,一条指令下去,所有像素同时开工。
  • MISD(多指令单数据):这个基本没人用,跳过。
  • MIMD(多指令多数据):多核CPU属于这类。每个核心可以跑不同的指令,处理不同的数据。

关键点:GPU之所以快,不是因为它单个计算单元有多强,而是因为它能同时让成千上万个计算单元做同一件事。说白了,就是人多力量大

2.2 数据并行 vs 任务并行

搞清楚了Flynn分类,我们再来看两种常见的并行模式。我在项目中遇到过不少新手把这两者搞混,结果代码性能惨不忍睹。

数据并行

数据并行,顾名思义,就是把数据切分成小块,每个线程处理其中一块。所有线程执行相同的操作。

举个例子:你有一张1024x1024的图片,想把它转成灰度图。每个像素的计算公式是一样的:gray = 0.299*R + 0.587*G + 0.114*B。你可以开1024x1024个线程,每个线程处理一个像素。

// 数据并行示例:每个线程处理一个像素
__global__ void rgbToGray(const float* r, const float* g, 
                          const float* b, float* gray, int width) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    gray[idx] = 0.299f * r[idx] + 0.587f * g[idx] + 0.114f * b[idx];
}

这种模式在GPU上效率极高。为什么?因为所有线程执行同样的指令,只是处理的数据不同。GPU的SIMD特性正好派上用场。

任务并行

任务并行则是把不同的任务分配给不同的线程。每个线程做的事情可能完全不同。

比如,一个线程在做矩阵乘法,另一个线程在排序,第三个线程在渲染UI。这在CPU上很常见,但在GPU上——嗯,我得说,这不是GPU的强项。

避坑指南:我曾经在一个项目里试图在GPU上做任务并行,让不同的block执行不同的kernel函数。结果发现性能还不如CPU。后来才意识到,GPU的线程调度器是为大量同质化线程设计的。如果线程执行路径差异太大,会导致严重的分支发散和资源浪费。

2.3 GPU为何天生适合并行计算?

你想想看,GPU最初是干嘛用的?渲染图形。一张3D场景里,有成千上万个顶点和像素需要处理,而且每个顶点的变换矩阵是一样的,每个像素的着色公式也是一样的。这不就是典型的数据并行吗?

所以GPU的设计哲学从一开始就和CPU不同:

特性 CPU GPU
核心数量 几个到几十个 几千个
缓存大小 大(MB级别) 小(KB级别)
控制单元 复杂(分支预测、乱序执行) 简单(顺序执行为主)
适合场景 任务并行、延迟敏感 数据并行、吞吐量优先

说白了,CPU像个全能选手,什么活都能干,但一次只能干几件。GPU像个专业流水线工人,只会干一种活,但能同时干几千件。

2.4 GPU并行计算的三个核心优势

根据我这些年的经验,GPU在并行计算上有三个不可替代的优势:

  1. 大规模线程并发:一个现代GPU可以同时管理数万个线程。CPU呢?一个物理核心通常只能同时跑1-2个线程。GPU通过快速切换线程来隐藏延迟——当一个线程在等内存数据时,立刻切换到另一个就绪的线程。
  2. 高内存带宽:GPU使用GDDR显存,带宽可以达到几百GB/s甚至TB/s级别。CPU的DDR内存带宽通常只有几十GB/s。对于数据密集型应用,这差距是数量级的。
  3. 计算密度高:GPU芯片上大部分面积都是计算单元(ALU),而CPU有大量面积被缓存和控制逻辑占据。这意味着在同样的芯片面积和功耗下,GPU能提供更高的计算吞吐量。

个人经验:我刚开始用CUDA时,总想着怎么让每个线程跑得更快。后来发现,真正重要的是如何让所有线程都保持忙碌。GPU的延迟隐藏能力很强,只要你提供足够的并行度,它就能把内存访问延迟"藏"起来。所以,别怕开太多线程——只要不超出寄存器限制,线程越多越好。

2.5 什么时候不该用GPU?

说了这么多GPU的好,我也得泼点冷水。不是所有问题都适合用GPU加速。我见过有人把简单的链表遍历也搬到GPU上,结果比CPU还慢。

以下几种情况,我建议你慎重考虑:

  • 数据量太小:如果数据只有几百个元素,CPU处理可能更快。因为GPU有启动开销(kernel launch latency),数据太少的话,这个开销占比太大。
  • 分支逻辑复杂:如果每个数据点的处理路径都不同,GPU的分支发散会严重拖慢性能。这时候CPU的乱序执行反而更有优势。
  • 依赖关系强:如果计算步骤之间有强依赖,比如后一步必须等前一步的结果,那GPU的并行优势就发挥不出来。
  • 频繁的CPU-GPU数据传输:PCIe带宽有限,而且传输延迟大。如果每帧都要把数据从CPU拷到GPU再拷回来,那大部分时间都花在传输上了。

嗯,说到这里,我想起一个教训。曾经有个项目,客户坚持要把一个递归的八叉树遍历放到GPU上跑。我劝了三次没劝住,结果花了两个月优化,性能还不如CPU单核。最后老老实实改回CPU版本,一周就搞定了。

所以,选对工具比用好工具更重要。GPU是并行计算的利器,但它不是万能的。理解它的优势和局限,才能做出正确的设计决策。