跳到主要内容
3D Playground
GPU 与 WebGPU 计算第 1 / 11 节

CPU 与 GPU:并行的力量

入门WebGL 2

为什么要用 GPU

CPU 只有少数几个强大的核心,擅长复杂的分支和顺序逻辑;GPU 有成千上万个简单的核心,擅长把同一段程序同时跑在海量数据上。图像正好是这种数据:每个像素都按同一个公式计算,彼此独立。

左边:一个线程,逐个像素

JavaScript 版本是两层 for 循环,一个像素算完再算下一个。因为太慢,它只在降低后的分辨率(CPU resolution)下计算,再拉伸到左半屏。

右边:每个像素一个线程

片元着色器的 main() 只描述一个像素怎么算,GPU 会为屏幕上的每个像素各启动一次:

glsl
vec2 c = uCenter + px * uScale;
for (; i < uIter; i++) {
  z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;
  if (dot(z, z) > 256.0) break;
}

画一个盖满视口的大三角形,就等于“对每个像素运行一次这个函数”,这是 GPU 通用计算最古老的技巧。

SIMT 与分支发散

GPU 把线程按 32 或 64 个一组(warp / wavefront)调度,同组线程同时执行同一条指令。如果组内有的像素早早逃逸、有的要跑满迭代,先结束的线程只能空等,这叫分支发散。分形边界附近最慢,就是这个原因。

计时的陷阱

GPU 命令是异步提交的:drawArrays 返回时活可能还没干完。这里用 readPixels 读回一个像素,强迫 CPU 等 GPU 完成,测到的才是真实耗时。第一次绘制还包含着色器编译,所以先做一次预热。

在无头测试环境里,GPU 往往由 SwiftShader 这类软件渲染器模拟(其实还是在 CPU 上跑),测出的 GPU 时间可能比 JS 还慢。在真正的显卡上,右半边通常快几十到几百倍。

打开 auto zoom,CPU 每帧都要重算,你能直观感受到两者差距。

核心 API

canvas.getContext('webgl2')gl.drawArraysgl_FragCoordgl_VertexIDgl.readPixelsgl.texImage2DImageDataperformance.now

动手试试

  1. 1把 iterations 调到 2000,对比左右两边的耗时怎么变化。
  2. 2把 CPU resolution 调到 1,看看 JS 在全分辨率下要花多久。
  3. 3删掉 drawGPU 后面的 readPixels,再看 GPU 计时变成了多少,想想为什么。
运行中