WebGPU Verlet积分计算着色器性能劣于CPU的优化问询
WebGPU计算着色器vs CPU粒子模拟性能异常问题
我在对比WebGPU计算着色器与CPU等效实现的Verlet积分粒子模拟时,得到了意外结果——CPU速度居然是GPU的10倍,二者模拟结果均正确。
CPU版本实现(Verlet积分)
IntegrateVerlet(dt: number) { console.time("verlet"); const count = this.positions.length / 4; /* eslint-disable */ for (let i = 0; i < count; i++) { const x = this.positions[i * 4 + 0]; const y = this.positions[i * 4 + 1]; const z = this.positions[i * 4 + 2]; const dx = x! - this.prevPositions[i * 4 + 0]!; const dy = y! - this.prevPositions[i * 4 + 1]!; const dz = z! - this.prevPositions[i * 4 + 2]!; const ax = this.forces[i * 4 + 0]! / this.mass; const ay = this.forces[i * 4 + 1]! / this.mass; const az = this.forces[i * 4 + 2]! / this.mass; this.prevPositions[i * 4 + 0] = x!; this.prevPositions[i * 4 + 1] = y!; this.prevPositions[i * 4 + 2] = z!; this.positions[i * 4 + 0] = x! + dx + ax! * dt * dt; this.positions[i * 4 + 1] = y! + dy + ay! * dt * dt; this.positions[i * 4 + 2] = z! + dz + az! * dt * dt; } /* eslint-enable */ console.timeEnd("verlet"); }
WebGPU计算着色器实现
@group(0) @binding(0) var<storage, read_write> positions: array<vec3<f32>>; @group(0) @binding(1) var<storage, read_write> prevPositions: array<vec3<f32>>; @group(0) @binding(2) var<storage, read> forces: array<vec3<f32>>; @group(0) @binding(3) var<uniform> mass: f32; @group(0) @binding(4) var<uniform> dt: f32; @compute @workgroup_size(${workgroupSize}) fn main( @builtin(workgroup_id) workgroup_id : vec3<u32>, @builtin(local_invocation_index) local_invocation_index: u32, @builtin(num_workgroups) num_workgroups: vec3<u32> ){ // workgroup_index is similar to local_invocation_index except for // workgroups, not threads inside a workgroup. // It is not a builtin so we compute it ourselves. let workgroup_index = workgroup_id.x + workgroup_id.y * num_workgroups.x + workgroup_id.z * num_workgroups.x * num_workgroups.y; // global_invocation_index is like local_invocation_index // except linear across all invocations across all dispatched // workgroups. It is not a builtin so we compute it ourselves. let i = workgroup_index * ${numThreadsPerWorkgroup} + local_invocation_index; if (i < arrayLength(&positions)) { let dPos = positions[i] - prevPositions[i]; let a = forces[i] / mass; prevPositions[i] = positions[i]; positions[i] = positions[i] + dPos + a * dt * dt; } }
测试环境与结果
- 粒子数量:2000×2000=400万个3D向量
- 当前GPU配置:
workgroupSize = 8,8,4 dispatchCount = 25,25,25 - 性能对比:
| avg cpu integration time | avg gpu integration time |
|---|---|
| 16 ms | 200 ms |
- 硬件:7800x3D + RTX4090
- 软件:Microsoft Edge 125.0.2535.92(64位正式版)
性能瓶颈分析
- 工作组配置不合理:RTX4090的SM更适配一维、线程数为32倍数的工作组(比如256线程),当前三维工作组会增加索引计算开销,且不利于GPU调度,无法最大化SM占用率。
- 线程索引计算冗余:手动计算全局索引的逻辑会占用每个线程的执行周期,WebGPU提供了原生的全局索引内置变量,完全可以替代手动计算。
- 内存访问效率低:三维索引映射可能导致线程访问的内存地址不连续,引发存储体冲突,降低内存带宽利用率;而CPU代码是连续线性访问,缓存命中率极高。
- 计时精度问题:用
console.time统计GPU时间可能包含了JS命令调度、队列等待的额外开销,没有精准测量计算着色器的实际执行时间。
优化调整方案
- 改用一维工作组:将工作组大小设为
@workgroup_size(256),调度数量计算为Math.ceil(4000000 / 256) = 15625,这样能提升GPU SM的占用率,简化索引逻辑。 - 简化全局索引计算:直接使用内置变量
@builtin(global_invocation_id)获取索引,替换手动计算:@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { let i = global_id.x; if (i < arrayLength(&positions)) { let dPos = positions[i] - prevPositions[i]; let a = forces[i] / mass; prevPositions[i] = positions[i]; positions[i] = positions[i] + dPos + a * dt * dt; } } - 预计算常量:在CPU端计算
dtSquared = dt * dt,作为uniform传入GPU,避免每个线程重复执行乘法运算。 - 精准GPU计时:使用WebGPU的
timestamp-query扩展来测量计算着色器的实际执行时间,排除调度等额外开销。 - 启用性能调试:用Edge DevTools的WebGPU性能分析工具,查看工作组调度、内存访问、SM占用率等指标,定位具体瓶颈。
内容的提问来源于stack exchange,提问作者Victor
相关产品推荐
相关产品推荐

