返回知识库

渲染

调试与性能优化

调试与性能优化 封面
WebGPU/WebGLShaderGLSL/WGSL调试性能优化GPU Profiling

直觉问题:为什么 Shader 调试这么难?

Q1: 为什么 Shader 代码没有断点调试器?如何追踪 Shader 变量?

Q2: 为什么同样的 Shader 在不同显卡上性能差异巨大?


核心概念白话讲

Shader 调试的挑战

传统调试:断点、单步、变量检查 Shader 调试:大规模并行、无断点、无变量检查

根本原因

  • Shader 同时运行数千线程
  • 中间结果无法直接访问
  • GPU 指令流水线化

TIP

  • 使用颜色输出调试:frag_color = vec4(normal, 1.0)
  • 使用纹理存储中间结果

调试工具对比

工具WebGLWebGPU功能
Spector.js帧捕获、Shader 查看
RenderDoc深度调试、内存检查
Nsight Graphics深度性能分析
Chrome DevTools基础调试
WebGPU InspectorWebGPU 专用

Spector.js

功能

  • 捕获 WebGL 帧并查看所有 Draw Call
  • 查看每个 Draw Call 的 Shader、纹理、Uniform
  • 导出 Shader 代码进行分析

使用方法

  1. 安装浏览器扩展
  2. 打开开发者工具 → Spector
  3. 刷新页面,点击”Capture Frame”
  4. 查看 Draw Call 列表

RenderDoc

功能

  • 捕获 GPU 帧并调试 Shader
  • 单步执行 Shader 指令
  • 查看所有纹理、缓冲区、Uniform
  • 性能分析(Pipeline State、Draw Call 时间)

使用方法

  1. 启动应用并捕获帧
  2. 查看 Event Browser 选择 Draw Call
  3. 在 Shader Viewer 中设置断点
  4. 单步执行并查看变量

GPU 性能分析

性能瓶颈类型

graph LR
    A[性能瓶颈] --> B[CPU 瓶颈<br/>Draw Call 过多]
    A --> C[内存瓶颈<br/>带宽不足]
    A --> D[计算瓶颈<br/>Shader 过于复杂]
    A --> E[光栅瓶颈<br/>像素过多]

瓶颈检测

  1. CPU 瓶颈gl.finish() 时间长,GPU 利用率低
  2. 内存瓶颈:带宽监控显示高利用率
  3. 计算瓶颈:Shader 编译时间长,执行时间长
  4. 光栅瓶颈:像素填充率监控

Benchmark 方法

Frame Time 监控

  • 使用 requestAnimationFrame 记录每帧时间
  • 计算 99% 分位数,避免峰值干扰
let frameTimes = [];
function render() {
  const start = performance.now();
  renderScene();
  const end = performance.now();

  frameTimes.push(end - start);
  if (frameTimes.length > 100) frameTimes.shift();

  const avg = frameTimes.reduce((a, b) => a + b) / frameTimes.length;
  const p99 = frameTimes.sort((a, b) => a - b)[99];

  console.log(`Avg: ${avg.toFixed(2)}ms, P99: ${p99.toFixed(2)}ms`);
}

GPU 时间查询

  • WebGL 2.0 使用 gl.getQueryParameter()
  • WebGPU 使用 timestamp 查询

原理与数学机制

分支发散

概念:Warp/Wavefront 内线程执行不同分支,导致性能下降

影响

  • 执行所有分支代码
  • 只有活跃线程参与计算

优化

  • 避免在 Warp 内使用 if-else
  • 使用 step()mix() 替代分支
result=mix(a,b,step(threshold,value))\text{result} = \text{mix}(a, b, \text{step}(threshold, value))

内存合并

概念:连续线程访问连续内存,提高缓存命中率

优化策略

  • SoA(Structure of Arrays):每个属性单独数组
  • AoS(Array of Structures):每个属性打包

SoA 布局

struct Particles {
  vec3 positions[10000];
  vec3 velocities[10000];
  float lifetimes[10000];
};

AoS 布局

struct Particle {
  vec3 position;
  vec3 velocity;
  float lifetime;
};
Particle particles[10000];

TIP

  • SoA 适合 Compute Shader
  • AoS 适合 Vertex Shader

共享内存

概念:Workgroup 内线程共享的高速内存

性能提升

  • 比全局内存快 10-100 倍
  • 减少全局内存访问

使用场景

  • 粒子排序
  • 图像卷积
  • 矩阵乘法

GLSL vs WGSL 代码对照

分支优化

GLSL 版本

#version 300 es
precision highp float;

in vec3 v_normal;
in vec3 v_light_dir;
out vec4 frag_color;

// 分支版本(性能差)
vec3 lighting_branching(vec3 normal, vec3 light_dir) {
  float diff;

  if (dot(normal, light_dir) > 0.0) {
    diff = dot(normal, light_dir);
  } else {
    diff = 0.0;
  }

  return vec3(diff);
}

// 优化版本(无分支)
vec3 lighting_optimized(vec3 normal, vec3 light_dir) {
  float diff = max(dot(normal, light_dir), 0.0);
  return vec3(diff);
}

void main() {
  vec3 color = lighting_optimized(v_normal, v_light_dir);
  frag_color = vec4(color, 1.0);
}

WGSL 版本

struct FragmentInput {
  @location(0) normal: vec3<f32>,
  @location(1) light_dir: vec3<f32>,
}

struct FragmentOutput {
  @location(0) color: vec4<f32>,
}

// 优化版本(无分支)
fn lighting_optimized(normal: vec3<f32>, light_dir: vec3<f32>) -> vec3<f32> {
  let diff = max(dot(normal, light_dir), 0.0);
  return vec3<f32>(diff);
}

@fragment
fn fs_main(input: FragmentInput) -> FragmentOutput {
  var output: FragmentOutput;

  let color = lighting_optimized(input.normal, input.light_dir);
  output.color = vec4<f32>(color, 1.0);

  return output;
}

差异点

  • WGSL 使用 let 定义不可变变量
  • WGSL 函数参数需要显式类型标注
  • WGSL 的 max 函数与 GLSL 相同

共享内存优化

GLSL 版本

#version 300 es
precision highp float;

layout(local_size_x = 8, local_size_y = 8) in;

uniform sampler2D u_texture;
uniform vec2 u_resolution;

layout(rgba8) uniform image2D u_output_image;

shared vec4 shared_data[64];

void main() {
  ivec2 global_id = ivec2(gl_GlobalInvocationID.xy);
  ivec2 local_id = ivec2(gl_LocalInvocationID.xy);
  int local_index = local_id.y * 8 + local_id.x;

  vec4 pixel = texelFetch(u_texture, global_id, 0);
  shared_data[local_index] = pixel;

  memoryBarrierShared();
  barrier();

  // 使用共享数据计算
  vec4 blurred = vec4(0.0);
  for (int i = -1; i <= 1; i++) {
    for (int j = -1; j <= 1; j++) {
      ivec2 offset = ivec2(i, j);
      blurred += shared_data[local_index + offset.x * 8 + offset.y];
    }
  }

  imageStore(u_output_image, global_id, blurred / 9.0);
}

WGSL 版本

@group(0) @binding(0)
var texture_input: texture_2d<f32>;

@group(0) @binding(1)
var texture_sampler: sampler;

@group(0) @binding(2)
var output_image: texture_storage_2d<rgba8unorm, read_write>;

var<workgroup> shared_data: array<vec4<f32>, 64>;

@compute @workgroup_size(8, 8, 1)
fn cs_main(@builtin(global_invocation_id) global_id: vec3<u32>,
           @builtin(local_invocation_id) local_id: vec3<u32>,
           @builtin(local_invocation_index) local_index: u32) {
  let pixel = textureLoad(texture_input, vec2<i32>(global_id.xy));

  shared_data[local_index] = pixel;

  workgroupBarrier();

  var blurred = vec4<f32>(0.0);
  for (var i: i32 = -1; i <= 1; i++) {
    for (var j: i32 = -1; j <= 1; j++) {
      let offset = i32(i) * 8 + i32(j);
      blurred = blurred + shared_data[local_index + u32(offset)];
    }
  }

  textureStore(output_image, vec2<i32>(global_id.xy), blurred / 9.0);
}

差异点

  • WGSL 使用 var<workgroup> 声明共享内存
  • WGSL 使用 workgroupBarrier() 替代 memoryBarrierShared()
  • WGSL 使用 textureLoad() 替代 texelFetch()

常见误区与陷阱

  1. 过度使用分支

    • 陷阱:Shader 中大量使用 if-else 导致性能下降
    • 解决:使用 step()mix() 替代分支
  2. 纹理采样数过多

    • 陷阱:Fragment Shader 采样纹理 10+ 次
    • 解决:使用纹理渐变、降低采样数
  3. 共享内存越界

    • 陷阱:访问 shared_data 超出范围导致崩溃
    • 解决:添加边界检查
  4. GPU Instancing 矩阵计算错误

    • 陷阱:在 Vertex Shader 中计算矩阵
    • 解决:在 CPU 或 Compute Shader 中预计算
  5. Raymarching 步进过多

    • 陷阱:MAX_STEPS 设置为 1000+ 导致性能崩溃
    • 解决:根据场景复杂度调整,通常 64-128 足够
  6. 未使用 Mipmap

    • 陷阱:远处物体采样全分辨率纹理
    • 解决:生成 Mipmap 并使用 textureLod()
  7. Compute Shader 线程组大小不当

    • 陷阱:使用 @workgroup_size(1, 1, 1) 导致严重性能下降
    • 解决:根据 GPU warp/wavefront 大小调整(32 或 64)
  8. WebGPU Inspector 兼容性

    • 陷阱:WebGPU Inspector 不支持所有特性
    • 解决:使用 RenderDoc 或 Chrome DevTools

延伸阅读与自测

权威资料

开源实现参考

自测题

  1. 思考题: 为什么 Shader 调试比 CPU 调试难 100 倍?有什么方法简化 Shader 调试?

  2. 对比题: 分支发散如何影响 GPU 性能?为什么 step()if-else 快?

  3. 实践题: 如何使用 Spector.js 调试 WebGL 应用的 Draw Call?如何找到性能瓶颈?

  4. 扩展题: Compute Shader 的共享内存如何优化性能?什么场景适合使用共享内存?

  5. 优化题: 如何优化 Raymarching 的性能?是否可以使用 GPU Instancing 加速?


参考资料获取时间: 2026-07-06,通过 web-search-prime_web_search_prime 工具检索。