计算机图形学 27:为什么更多物体会变慢
同一个四边形重复绘制 4,096 次,可以提交 4,096 条绘制命令,也可以提交一条带有 4,096 个实例的命令。两种方法都要处理 24,576 个顶点,都覆盖同样的像素。减少绘制命令主要改变的是命令组织成本,不能据此推断三角形处理、着色或显示速度一定提高。
第 26 篇已经检查了坐标、颜色、布局与深度。本篇保留它的 WebGPU 与线性颜色约定,用互不重叠的小四边形隔离提交方式的影响。实验先检查两种方法生成相同的字节,再测量 CPU 编码提交、GPU render pass 和连续绘制时的帧回调间隔。
固定画面,再改变提交方式
打开实例化实验。点击“运行完整对照”,页面依次检查 64、1,024、4,096 个物体。完整原始样本保留在页面下方,可另查看本次实际测量 JSON。
图中每个小块都是 GPU 绘制的两个三角形。所有场景使用 512×512 附件,每像素一个样本,没有纹理、光照和 MSAA;背景黑色,不启用混合。每个格子的宽高占格距的 72%,避免交叠与排序影响。物体增多时单个小块会缩小,因此只在同一物体数内比较两种提交方式,不把不同物体数的耗时当作相同几何负载。
实例颜色是线性 RGB。片元着色器沿用第 25、26 篇的显式 sRGB 编码,将结果写入非 sRGB 的 unorm 附件。测试机选择 bgra8unorm,数值检查按其通道顺序读取;没有再次应用颜色转换。
2026-10-01 的真实浏览器运行报告适配器 vendor 为 apple,architecture 为 metal-3,支持 timestamp-query。device、description 为空,不能从这些字段确定具体 GPU 型号。浏览器报告 Chrome/154.0.0.0;兼容性用户代理中的平台文本也不能用来判断实际 CPU 型号。
三组场景中,两种方法的完整附件读回逐字节相同。各物体中心的颜色另外与独立算出的 sRGB 码值比较,每通道允许 1 个码值偏差;三组均没有错误。这两个检查用途不同:相互一致可以排除提交方式引入的差异,中心参考值可以检出两条路径共有的颜色或索引错误。
一条命令怎样描述多个实例
单个四边形用六个顶点描述,顶点缓冲只存局部位置。实例缓冲存平移、缩放与颜色。它们不是复制出的 4,096 套顶点,也没有为每个物体创建一个 pipeline。
| 数据 | 步长 | 内容 |
|---|---|---|
| 顶点 | 8 字节 | 两个 float32 局部坐标 |
| 实例 | 32 字节 | 四个 float32 变换量,四个 float32 颜色量 |
实例记录的前 16 字节按 offset.x, offset.y, scale.x, scale.y 排列,后 16 字节为 RGBA。顶点位置计算为:
这里只做二维缩放和平移,所以不上传完整矩阵。扩展到第 16 篇的三维场景时,可换成实例模型矩阵,但要重新核对步长、属性数量和法线变换。不要把本实验的两个缩放值直接当作完整的三维变换。
实例缓冲布局指定 stepMode: 'instance'。普通顶点属性按顶点前进,这个缓冲按实例前进。WebGPU 的 GPUVertexBufferLayout 定义了这两种步进方式。实际地址是实例编号乘以 32,再加相应属性偏移;它不是 uniform 的自动打包布局。
逐物体方式用一条 render pass,循环调用:
1 | |
实例化方式保持同一套缓冲与 pipeline,只调用:
1 | |
两种方式都只调用一次 queue.submit。变化的是 render pass 内的 draw 数量,不是 CPU 向队列提交 command buffer 的次数。把 draw call 与 queue submission 混在一起,就无法解释这组对照。
draw(vertexCount, instanceCount, firstVertex, firstInstance) 的第四个参数决定起始实例。逐物体路径用 i,因此能读取不同记录;实例化路径从默认的 0 开始。规范的 draw 算法 与 WGSL 的 instance_index 都把起始实例纳入编号语义。
点击“错误 firstInstance 负对照”会把所有逐物体绘制的起始编号改成 0。64 个物体重复画在第一个格子里,中心检查发现其余 63 个位置缺失。本次真实操作得到的错误数正是 63。这个反例说明画出了某个图形,不能证明实例索引正确。
实例化减少哪一部分成本
若每条 draw 的编码成本近似为 ,每帧固定成本为 ,逐物体路径的 CPU 成本可粗略写成 。实例化把 draw 部分变成 ,但动态实例的数据准备与上传仍可能是 。
这个模型只用于定位成本,不能拿它替代测量。浏览器、驱动可能延迟处理命令;每条 draw 的成本也不一定相同。绑定切换、验证、资源状态与线程调度都可能影响实际时间。
GPU 顶点调用在两条路径中仍为 ,覆盖与片元运算也没有减少。GPU 调度可能从批次组织中获益,也可能已经被其他工作限制。若片元着色器很重,减少 CPU draw 编码未必改变帧率。
可合并的物体还必须兼容同一 pipeline、顶点布局与资源组织。本实验颜色作为实例属性,所以颜色不同不需要切换绑定。真实材质使用不同纹理时,需要纹理数组、图集或其他合法索引方案;这些方案又受格式、尺寸和采样条件限制。
透明物体按深度排序时,不能任意打乱顺序来凑大批次。一个巨大的实例批次也不会自动解决逐物体剔除:把已经不可见的物体全部交给 GPU,可能抵消提交节省。实例化成立的条件是重复几何和兼容状态,而不是“物体越多越应该全部合成一批”。
三种时间必须分开
CPU 编码提交时间从创建 encoder 开始,到 queue.submit 返回结束。它包含命令编码、finish 和提交调用,排除实例生成、上传与读回。submit 返回只代表 CPU 调用结束,不代表 GPU 完成。
GPU 时间取 render pass 起止 timestamp 差,单位从纳秒换成毫秒。它排除实例上传、查询结果拷贝、映射等待和呈现。timestamp-query 是可选特性;不支持时页面保留像素与 CPU 检查,GPU 时间显示为不可用,不用队列等待时间代替。
实验另外保留 onSubmittedWorkDone 的 Promise 等待墙钟时间。这个数包括队列进度与异步通知延迟,定义见队列完成规范。它既不是纯 GPU 执行时间,也不是从帧开始到最终显示的完整延迟。
为分开每个测量样本,隔离测量每次提交后等待完成,再读回时间戳。这样会减少 CPU、GPU 连续工作的重叠,因此不能把隔离样本倒数当成游戏帧率。连续绘制另走一条没有逐帧读回和队列等待的路径。
连续路径在每个 requestAnimationFrame 回调里绘制画布,记录相邻回调间隔。它观察浏览器的帧调度,不证明每一帧都已经被显示器扫描出来,也不能直接测量 GPU pass。HTML 的渲染机会会受可见性和浏览器调度影响,因此实验在页面隐藏或设备丢失时作废。
真实结果怎样读
每种方式先预热 8 次,再运行 5 轮,每轮每种方式 12 个样本,轮内交替顺序,共 60 个样本。场景确定性生成,没有随机种子或随机终止条件。连续路径另丢弃前 10 个回调,保存 60 个间隔。
下表的数字为毫秒,中位数仅按显示精度舍入,原始值与 p10、p90 在 JSON 中保留。分位数统一取排序样本的下标 floor(p × (n−1));偶数样本的中位数取中间较小者,不对两个中间值取平均。
| 物体数 | draw 方式 | CPU 编码提交中位数 | GPU pass 中位数 |
|---|---|---|---|
| 64 | 逐物体 | 0.0000 | 0.000000 |
| 64 | 实例化 | 0.0000 | 0.000000 |
| 1,024 | 逐物体 | 0.0000 | 0.065536 |
| 1,024 | 实例化 | 0.0000 | 0.065536 |
| 4,096 | 逐物体 | 0.1000 | 0.131072 |
| 4,096 | 实例化 | 0.0000 | 0.065536 |
零值暴露的是分辨率限制。CPU 样本集中在约 0.1 ms 的间隔,GPU 差值大量落在 0.065536 ms 的整数倍上。规范允许时间戳降低精度;本次观察到的量化不能解释成硬件执行没有耗时。
4,096 个物体的 CPU 五轮中位数,逐物体约为 0.1, 0, 0, 0.1, 0 ms,实例化均为 0。整体中位数差异可以报告,但逐轮结果不足以支持稳定的精确加速倍数。将 0.1 除以 0 得到“无限加速”是错误分析。
同组 GPU 的 p10–p90 为:逐物体 0.131072–1.703936 ms,实例化 0–1.245184 ms。尾部波动远大于中位数差异,不能只截取两个中位数宣称稳定两倍。这里的可确认结果是命令数量确实下降、画面一致,以及本次隔离测量出现了上述分布。
4,096 个物体连续绘制的回调间隔中位数,两种方式均约 8.3 ms。逐物体 p10–p90 约 7.6–9.2 ms,实例化约 7.5–9.0 ms。在这个很轻的场景中,回调统计没有显示明显提升;不能从中声称具体显示器刷新率,更不能把它解释成 GPU 耗时 8.3 ms。
上传调用也可以成为成本
draw 数变少以后,实例数据仍要到达 GPU。实验把相同的 32N 字节分两种方式写入同一个缓冲:一次上传全部,或每个实例调用一次 writeBuffer。数据内容、目标字节数相同,只改变调用数量。
1 | |
这段代码中目标偏移的单位是字节;TypedArray 的源偏移和数量单位是元素,八个 float32 恰好 32 字节。规范的 writeBuffer 还规定目标范围、用途和四字节对齐等验证条件。把源 size 写成 32 会传入 32 个元素,可能越界,而不是传入 32 字节。
每组采用 5 次上传样本,交替顺序。4,096 个实例的总量为 131,072 字节,一次写入的 CPU 调用时间中位数为 0,多次写入约 0.3 ms;1,024 个实例的多次写入约 0.1 ms,64 个实例无法用这次计时分辨。这里测的是 CPU 调用墙钟时间,排除完成等待,不能把它当作总线带宽测量。
静态实例可以只上传一次。动态实例需要考虑修改范围、CPU 数据生成、缓冲寿命与在途帧。本篇没有实现流式资源管理,也没有据五个短样本推导通用吞吐极限。选择一次连续上传,是当前相同数据布局下可以直接检验的改动。
练习与自检
练习一:定位实例颜色。 一个实例记录 32 字节,颜色从记录内偏移 16 开始。实例编号为 7 时,颜色属性从哪一个字节开始?逐物体 draw 的第四个参数应是多少?
解题要点:地址为 ,第四个参数为 7。起始实例编号参与实例属性读取;如果始终用 0,其他记录即使上传正确也不会被读取。
练习二:解释零中位数。 A 的 CPU 中位数为 0.1 ms,B 为 0。能否报告 B 无限快?还缺哪些证据?
解题要点:不能。零是当前计时粒度下的结果。应保留原始样本、分布与多轮结果,增加可分辨负载时仍保持两条路径画质相同。若改变测量为多个 pass 总计,也必须写清新计时区间及其批次重叠效应,不能混用旧口径。
练习三:决定批次边界。 两组四边形使用不同混合状态,但顶点布局相同,能否直接放进本文的一条 draw?如果仅颜色不同呢?
解题要点:一条 draw 使用当前 pipeline 的混合状态,不能直接保留两种不同状态,应拆批次或重新设计等价算法。仅颜色不同可以用实例属性,因为 pipeline 与绑定兼容。透明排序仍是另外的约束。
复跑与导航
累计入口为 examples/computer-graphics/gpu27.html,实例数据与统计函数在 instances27.mjs,GPU 管线与测量在 gpu27.mjs。运行 node examples/computer-graphics/instances27_check.mjs 可检查记录长度、范围与统计;该检查不能替代 GPU 绘制。
通过本地 HTTP 服务打开页面,保持可见,运行完整对照后检查负对照,再用数量选择器显示场景。页面内原始 JSON 是运行结果;文章附带 JSON 是 2026-10-01 的记录,不会随读者运行自动改变。截图为真实浏览器结果,不是示意图。
事实核验位置为 writing-plans/computer-graphics/evidence/research-27.md,数值与浏览器验收记录在同目录。源文件、原始样本与页面素材一起保存,便于判断测量条件是否相同。







