“We should forget about small efficiencies, say about 97% of the time: Premature optimization is the root of all evil.” —Donald Knuth
首先问一个问题: 批量渲染是什么?
在三维渲染中,批量渲染(Batch Rendering)指的是将原本分散的多个渲染对象或绘制命令合并在一起,在一次或更少的绘制调用(Draw Calls)中完成渲染。这样做的核心目的是减少 CPU 和 GPU 之间的往返指令(即 Draw Call 数量),从而提高渲染效率。
CPU 与 GPU 之间的通信可以视为一条“数据高速公路”。只要数据(顶点、纹理、指令等)在这条公路上来回奔跑,就一定会消耗时间和带宽。频繁的小批量通信不仅会让 CPU 和 GPU 相互等待、切换状态,还可能造成带宽被零碎占用,导致整体“堵车”,从而降低渲染效率。
下面从几个维度分析 CPU 与 GPU 之间的频繁通信带来的问题:
-
带宽与时延的开销
-
CPU 和 GPU 通常通过 PCIe 或类似的总线进行数据交互。
-
带宽有限:如果频繁地在 CPU 与 GPU 之间传输大块或很多小块数据,就有可能“挤占”带宽,造成排队延迟。
-
时延累积:一次数据传输并不会马上完成,往返时延叠加多了会降低吞吐量。
-
-
命令开销与调度
-
除了传输数据本身,每条渲染命令(如 Draw Call)都要经过 CPU 的准备、驱动层打包、再发往 GPU。
-
如果命令过于零碎、频繁,就会在 CPU 端积累非常多的小调用和上下文切换。
-
-
同步阻塞(Synchronization Overhead)
-
当 CPU 与 GPU 之间需要对同一段资源进行读写时,需要做同步或等待。
-
如果频繁地更新 GPU 正在使用的数据,或从 GPU 读回数据到 CPU,都可能触发阻塞,导致流水线停顿。
-
综上所述,频繁、小规模、零散的通信 就是性能的“隐性杀手”;不断在 CPU 和 GPU 之间“跑腿”的过程,会拉低整体的渲染效率。
为什么减少 CPU 和 GPU 的往返通信, 就可以提高渲染效率?
下面从三个维度分析:
-
降低“指令发起”成本
-
当通信次数少时,CPU 不必不停地执行状态切换、数据传输命令、同步命令等,能够更快地发起下一步渲染或逻辑处理。
-
减少 API 调用的频度(如每帧要发送给 GPU 的命令数减半),就能让 CPU 空出更多时间用于游戏逻辑、物理模拟或其他并行任务。
-
-
减少总线与驱动的负载
-
如果把许多小数据合并成一次大传输,往往比让 CPU 多次传输小数据更有效率,能更好地利用总线的带宽、减少驱动层开销。
-
GPU 也不需要因接收“碎片”数据而多次切换上下文。
-
-
减轻同步等待
-
频繁的小数据更新通常需要 CPU 等待 GPU 或 GPU 等待 CPU,以确保数据在正确的时机读写。
-
当通信次数减少后,流水线得以更好地并行运作,CPU 和 GPU 能更持续地“各干各的事”,避免互相等待。
-
下面用“物流运输”作简单对比:
-
频繁通信的情况:
-
好比有很多小包裹,每次都要单独装车、运送、卸货,然后再回头取下一个包裹。
-
在这个过程里,卡车司机(相当于 CPU)要反复地发车、停车,等待装卸;公路(PCIe 总线)上也一直被占用,但利用率又并不高。
-
结果就是整体的效率很差。
-
-
减少通信后的情况:
-
如果将这些小包裹汇总打包到一个或几个大包裹,一次装车、一趟送完,司机(CPU)就能在装车后立刻专注做别的事(例如下一个订单的处理或其他工作),公路(总线)的利用率也更高。
-
卡车(GPU)一旦接到大包裹,就能持续处理里面的内容,不用因为频繁装卸和路上折返而闲置。
-
所以,减少 CPU–GPU 通信解决的问题就是:
-
避免 CPU 和 GPU 因为频繁的“单次、零散”传输和命令执行而浪费时间在命令调度、带宽占用、同步等待等方面。
-
让整个流水线更流畅、减少阻塞,提高并行度,从而提升整体渲染帧率或降低延迟。
那么, 有哪些 批量渲染(Batch Rendering) 的方案?
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
静态批处理 (Static Batching) | - 场景中静止不动或很少变动的物体 - 例如:固定建筑、远处景观 | - 大幅减少 Draw Call 数量 - 一次合并后使用方 便 - 对静态场景优化效果显著 | - 内存占用增加(合并后网格大)- 无法单独剔除或移动子物体(粒度变粗)- 视锥剔除效率变低 |
动态批处理 (Dynamic Batching) | - 需要在运行时移动或变化的中小型物体 - 例如:游戏中的小道具、交互物体 | - 减少绘制调用,减少渲染状态切换 - 能在场景变化时灵活“合批” | - CPU 开销较高,需要实时合并网格或数 据 - 对物体材质、网格格式要求相似 - 如果物体变化过于频繁,收益可能有限 |
实例化渲染 (GPU Instancing) | - 几何形状相同但位置、朝向、颜色等不同的海量物 体 - 例如:森林中的树、草地、粒子系统 | - 显著降低 Draw Call 数量 - 充分利用 GPU 并行处理能力 - 只需存一份几何数据 | - 要求实例间的网格完全相同 - 需管理大量实例参数(矩阵、材质索引等) - 实例过多时,需注意缓冲区传输和更新效率 |
Multi-Draw Indirect / Indirect Drawing | - 同一渲染管线下需要绘制大量子对象 - 可在 GPU 侧生成或更新绘制指令 - 如:GPU 实时剔除后批量发起绘制 | - 减少 CPU提交的绘制调用 - 结合 GPU 计算可批量剔除无用对象 - CPU 干预更少,渲染管线更顺畅 | - 对引擎和 API 支持要求较高 - 需要定制化的数据结构与管线 - 实现较为复杂,需管理 GPU 侧的绘制参数缓冲 |
纹理图集 (Texture Atlas) | - 大量尺寸较小、多种不同贴图的场 合 - 例如:游戏 UI、2D Tilemap、贴图种类多且单张图很小 | - 减少纹理绑定的次数 - 提高渲染批次合并效率 - 适合贴图数量庞多而单个贴图很小的情况 | - 需要对 UV 坐标做偏移管理 - Mipmaps 处理和纹理边缘过渡可能复杂 - 一张图集过大时,会有浪费或调度复杂度 |
批量渲染的关键思路在于:
-
合并:将相似或相同类型的渲染数据合并到同一份绘制调用或数据提交中;
-
减少:缩减 CPU 发给 GPU 的命令(Draw Calls)和状态切换,从而降低通信与调度的成本;
-
充分利用 GPU 并行:一旦 GPU 获得大批量的数据或指令,可更有效地利用内部的并行计算与流水线。
下一篇说说 Cesium的批量渲染