WebAssembly 性能优化 — 内存、并发、体积与剖析¶
1. 这一章讲什么¶
前四章把 Wasm 跑通、把数据传顺了,这一章回答「怎么让它真的快」。 全书的方法论在这一章定调:优化不是猜,是循环——剖析找到瓶颈 → 针对瓶颈下药 → 复测验证。药有四类:内存(预分配与复用)、并发(共享内存多线程)、指令(SIMD 与 wasm-opt)、 体积(编译期到压缩的全链路)。每类都配了带真实数字的走查。1
2. 顶层全景¶
先量:DevTools / performance.mark() 找出瓶颈
│
├── CPU -bound → SIMD 一次算四个数 / wasm-opt 精简指令 / Web Workers 拆给多核
├── I/O-bound → JS-Wasm 边界按批过数据,千次调用变一次
├── 内存问题 → 预分配池、紧凑排布、顺序访问、grow 后重算指针
└── 体积问题 → -O3 + --gc-sections + wasm-opt -O2 + Brotli
│
▼
复测:同一把尺子量前后,数字说话
图说:一个循环四种药;整章每一节都是「量 → 药 → 复测」的一次迭代。
3. 核心原理¶
3.1 内存模型:力量与麻烦同源¶
线性内存是一块从零开始的原始字节数组,起始一页(64KB),32 位 Wasm 下最多能长到 4GB;
访问全靠 i32.load/i32.store 这类显式指令,偏移自己算。2
两条最基本的指令管动态分配:memory.size 报当前页数;memory.grow(n) 申请加 n 页,
成功返回增长前的页数,失败返回 -1。3
这段代码书里给了,值得逐行记住,因为它是「没有 GC 的世界」的完整缩影:4
uint8_t pool[1024 * 1024]; // 1MB 粒子池
int offset = 0;
void* alloc(size_t size) {
if (offset + size > sizeof(pool)) return NULL; // 满了就拒绝
void* ptr = pool + offset;
offset += size;
return ptr;
}
void reset() { offset = 0; } // 整体重置,逐个 free 都省了
这叫内存池(arena):一口气预分配一大块,临时对象依次往里取,用完整体清零。 代价书里也写了:池子划大了浪费,要用浏览器 Memory 页盯着实际用量调大小。5
3.2 一个真要命的坑:grow 之后指针作废¶
memory.grow() 成功后,模块内所有指针都可能失效——数据可能被整体挪动,
旧偏移指向的是过去的地盘。书里的例子:6
配套纪律:性能关键的循环里尽量事前规划好分配,别在循环里 grow;
grow 之后所有存着的指针都要重算;测试时「分配直到失败」,确认失败路径优雅。
浏览器还有 4GB 的增长上限,grow 返回 -1 是常态不是意外。7
3.3 GC 的现状(本书时间坐标:2025 年 3 月)¶
Wasm 当时没有原生垃圾回收,Rust、C 编译过去照旧手工管理。 Wasm GC 提案「接近实现」,目标是把 Kotlin、Python 这类语言接进来,但书里明确说还不是标准。8
过渡方案两种,书里都给了:把对象传给 JS、让 JS 的 GC 收;或者干脆用内存池。 书里对未来的预测是「混合模型:性能敏感处手工,便利处交给 GC」。9
3.4 并发:SharedArrayBuffer + Atomics¶
SharedArrayBuffer(共享数组缓冲区)是一块多个线程都能直接读写的内存—— 和普通 ArrayBuffer 的区别就是「复制一份」变成「共用一份」。 配合 Wasm 的用法:把 SAB 作为模块的线性内存,几个 Web Worker(浏览器里跑在 主线程之外的线程)同时操作它,真正的并行。10
但共享就要同步,同步靠 Atomics 一组原子操作:11
| 操作 | 干什么 |
|---|---|
Atomics.store / Atomics.load |
线程安全地写/读,不会被半途打断 |
Atomics.wait / Atomics.notify |
生产者-消费者:一个睡等信号,一个发信号唤醒 |
Atomics.compareExchange |
无锁更新的基石,书上用它写自旋锁:while (compareExchange(view,0,0,1) !== 0) {}——「抢到 0→1 这个翻转变换才放行」 |
三个风险点名:竞态(两个 Worker 同时加同一个计数器,不加同步必然丢更新)、 死锁(wait 的条件永远不来,整个页面冻住)、调试难(Worker 线程在工具里分开显示, 得自己记中间状态)。12
安全门槛一句话:2018 年 Spectre/Meltdown 之后,SharedArrayBuffer 只在安全上下文 (HTTPS) 里可用——这是政策不是技术。13
3.5 找瓶颈:先分类,再量¶
书里把瓶颈分成两类,药方完全不同:14
- CPU 瓶颈:物理模拟这种紧循环。药在指令层(SIMD、循环展开);
- I/O 瓶颈:出在 JS-Wasm 边界——每一次跨边界调用都要做数据整理,成本约 10–100 微秒。 循环里逐元素调 Wasm 是最典型的自杀写法。药在批处理。
量法:performance.now() 或 performance.mark()/measure() 掐表,
Chrome Performance 页里 Wasm 函数会显示成 wasm-function[123] 这样的条目,
Firefox Profiler 的 Call Tree 按 Wasm/JS 分开计时。15
3.6 主线例子:灰度滤镜,从 80% 到 4 倍(原书案例,数字来自书)¶
输入:一张图像,RGBA 四字节一个像素,C 函数逐像素算灰度
gray = r×0.3 + g×0.59 + b×0.11 写回。16
第 1 次剖析。 Chrome Performance 显示 80% 的时间耗在这个循环里: 每像素三次读三次写,内存被来回搅动——这是 CPU + 访存双重瓶颈。17
第一味药:SIMD。 SIMD(单指令多数据)是一条指令同时处理一串数——Wasm 的 SIMD 扩展用
128 位向量(v128),比如一次装 4 个 32 位浮点。灰度循环改成向量版后,
运行时间砍掉 60%。前提:数据按 16 字节对齐,且各浏览器都已支持(本书坐标 2025 年)。18
第二味药:治 I/O。 另一个瓶颈是每一帧都从 JS 把图像数组拷给 Wasm。 改法:分配一块常驻的 Wasm 缓冲区,图像数据复制一次、反复复用。 改之前那个「逐帧传参」的写法,正是 3.5 说的 I/O 瓶颈。19
复测。 剖析确认:整体 4 倍加速。书里从这一例总结出的方法论只有一句: CPU 和 I/O 的药要一起下。20
3.7 体积:下载时间和解析时间都是钱¶
数字先立起来(书里的):1MB 的 .wasm 在 5Mbps 下约 200ms 下载,现代浏览器还要 每 MB 约 50ms 解析;手机网(1Mbps)直接膨胀到秒级。21
压缩链路四连,书里给的组合战绩是 800KB → 150KB,加载时间 -80%:22
- 编译期:
emcc -O3 --gc-sections(死代码消除)——示例 500KB 掉到 300KB; wasm-opt -O2再过一遍;- Brotli 压缩:
brotli -Z app.wasm -o app.wasm.br,服务器配Content-Encoding: br——300KB 变 100KB; - 源头审计:把源码里不用的函数删掉。
代价:-O3 会改掉函数名,堆栈跟踪变难读——开发期留未压缩构建。23
3.8 内存排布:紧凑、复用、顺着走¶
三组手法,各有数字:24
- 紧凑排布:一千个粒子别用一千个小对象(每个都要指针跳转),
摊进一个
float[4000]大数组——少一次间接寻址,还天然对齐; - 复用:
malloc/free每帧一千次会把堆搅烂;预分配池 +reset_pool()把 grow 调用清零。书里游戏循环的例子:3 倍提速,内存从动态峰值降到固定 16KB,堆增长归零; - 顺着走:二维数组按行优先访问(
data[y*width+x]y 在外层), 缓存行被充分利用;列优先的访问模式慢 20–50%。对齐到 4 字节(i32)或 16 字节(SIMD)边界,错了要付惩罚甚至触发陷阱。25
3.9 I/O 与计算的配平¶
批处理的账,书里算给你看:循环里对 1000 个元素逐个调 Wasm 是 1000 次跨界,
每次 10–100µs;改成一次 malloc + 一批 set 进共享内存 + 一次 processBatch(ptr, len)
+ 一次 free——开销砍掉 90%。26
JSON 管线的完整版也给了:fetch 拿 arrayBuffer 而不是 res.json()(把解析的活也挪进
Wasm),整块拷进线性内存,一个 parseJSON(ptr, len) 收尾——1MB JSON 快 3 倍,JS 开销 -50%。
配平的口诀书里写了:批量太大压垮内存,太小浪费跨界,批量要对准计算能力。27
3.10 benchmark:数字要有统计的样子¶
方法三件套:performance.mark() 掐点、performance.measure() 出数、
跑 100 次取平均压掉随机波动;看 95 分位别只看均值(均值 10ms 里藏着一个 100ms 的毛刺,
那通常是 I/O 卡顿)。28
参照系:60fps 的帧预算是 16.6ms。书里 H.264 解码器(把压缩视频还原成画面的程序)的对比实测: Wasm 每帧 15ms,JS 每帧 35ms——2.3 倍,15ms 正好卡在 60fps 线内,35ms 出局。29
3.11 工具与自动化¶
静态侧:wasm-objdump -x 看代码段大小、内存段、导出清单;wasm2wat 反汇编人读;
wasm-gdb 带符号(-g)时做底层剖析。动态侧:DevTools Performance + Firefox Profiler。
自动化:把 benchmark 编进 CI,超阈值(比如 20ms)直接让构建失败,防性能回归。30
4. 作者的判断与证据¶
这一章数字密集,证据等级要分开看:
- 机制类(grow 语义、Atomics 行为、SIMD 128 位向量):规范定义,可靠;
- 书里实测类(灰度滤镜 4x、解码器 15ms vs 35ms、JSON 3x、池化 3x): 有前后对照和工具名,但没给可复现的完整环境(硬件、浏览器版本、样本大小), 当「方向性实测」用;
- 引用类(每跨界调用 10–100µs、体积 800→150KB):量级与业界经验一致,可信但别当精确值。
判断(我们的,不是书里的): 这章的「先剖析再优化」循环是全书最该带走的方法论, 比任何单个技巧都保值。相对地,那些「-60%」「4x」的百分比绑定了具体硬件与浏览器版本, 复现性差;真正可移植的是诊断顺序(先分清 CPU 还是 I/O,再选药)。 如果错,会错在: 如果未来 JS 引擎把 Wasm 调用的固定开销降到接近普通函数调用 (已有 JIT 内联提案讨论过),「批处理治 I/O」这一整节的紧迫性会下降——但截至本书时间坐标,它仍成立。
5. 边界与局限¶
- SIMD 只讲了入门:128 位向量、对齐、开关,没有再深(比如查表、shuffle 这类高阶用法);
- 多线程没有完整工程走查:Worker 池怎么管、任务怎么分、结果怎么合,只有射线追踪四象限的轮廓;
- Wasm GC 一节注定快速过时:本书写时「还不是标准」,落地进度要查最新规范;
- wasm-gdb 的实用性书里自己承认「浏览器里不太实用」,却没收尾讲什么场景它真有用(服务器端 Wasm 才用得上);
- 「WebAssembly 3.0」的提法(结论段)比业界通行说法激进——主流仍是 2.0 + 各提案,3.0 并无正式定版时间表(补充,不在书里,依据通用知识)。
6. 可带走的¶
- 优化循环:剖析 → 分类(CPU/I/O/内存/体积)→ 下药 → 同尺复测。不许跳步。
memory.grow成功返回旧页数、失败返回 -1;grow 后所有指针重算,这是红线。- 没有 GC 的年代(本书坐标):内存池 + 整体 reset 是最省心的分配策略;泄漏不会自愈。
- 跨界调用每次 10–100µs:循环里逐元素调是自杀,攒成批一次过。
- SIMD = 128 位向量一次算 4 个数;数据要 16 字节对齐;灰度滤镜靠它 -60%。
- 体积四连:
-O3+--gc-sections+wasm-opt -O2+ Brotli;800KB→150KB 是书里战绩。 - 访存按行优先,乱序访问慢 20–50%;小对象摊平进大数组,粒子系统从动态峰值降到 16KB 固定。
- SharedArrayBuffer 只在 HTTPS 可用;同步三件套 store/load、wait/notify、compareExchange;防死锁、防竞态。
- benchmark 跑 100 次取平均、看 95 分位;60fps = 16.6ms 帧预算;和 JS 同尺对比才算数。
- CI 里放性能门槛:超 20ms 构建失败,回归在合入前就被拦下。
7. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 内存模型与原语 | Optimizing WebAssembly Performance | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:69(搜「heap and stack」) · text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:115(搜「memory.size」) |
| 内存池 C 例 | 同上 | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:193(搜「uint8_t pool」) |
| GC 现状与过渡方案 | 同上 | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:181(搜「lacks native garbage collection」) |
| grow 后指针失效 | 同上 | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:167(搜「invalidating pointers」) · text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:235(搜「malloc(4)」) |
| SharedArrayBuffer 与 Atomics | 同上 | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:257(搜「share a single memory buffer」) · text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:265(搜「Atomics.add」) |
| 自旋锁与竞态/死锁 | 同上 | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:363(搜「Spin lock」) · text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:365(搜「Deadlocks」) |
| HTTPS 限制 | 同上 | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:287(搜「secure contexts」) |
| 瓶颈分类与跨界成本 | 同上 | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:383(搜「CPU-bound」) · text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:387(搜「10-100µs」) |
| 灰度滤镜案例 | 同上 | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:435(搜「void grayscale」) · text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:449(搜「80% of the time」) · text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:457(搜「4x speedup」) |
| 体积数字与四连 | 同上 | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:465(搜「200ms on a 5Mbps」) · text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:515(搜「800KB to 150KB」) |
| wasm-opt 矩阵乘 | 同上 | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:555(搜「void matmul」) · text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:569(搜「reducing instructions by ~30%」) |
| SIMD 机制与对齐 | 同上 | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:593(搜「128-bit vectors」) · text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:653(搜「16-byte boundaries」) |
| 精灵池案例 | 同上 | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:807(搜「1,000 sprites」) · text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:839(搜「3x faster」) |
| 批处理 90% 与 JSON 3x | 同上 | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:877(搜「slashes overhead by 90%」) · text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:963(搜「1MB of JSON 3x faster」) |
| benchmark 与解码器对比 | 同上 | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:989(搜「100 iterations」) · text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:1039(搜「15ms/frame」) |
| 工具与 CI 门槛 | 同上 | text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:1079(搜「wasm-objdump -x」) · text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:1151(搜「fails if thresholds」) |
-
出处:「Optimizing WebAssembly Performance」第 13–23 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:17,搜「careful optimization across memory, concurrency, and computation」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 65–87 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:71,搜「expandable up to 4GB」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 113–129 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:123,搜「current size in 64KB pages」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 191–211 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:193,搜「uint8_t pool」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 213–215 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:213,搜「fixed pools waste space」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 167 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:167,搜「invalidating pointers」)与text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:237(搜「Shifts heap」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 169–177 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:175,搜「Memory growth failed」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 181–183 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:181,搜「lacks native garbage collection」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 185–217 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:187,搜「offloading GC to JavaScript」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 257–285 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:259,搜「copied between threads」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 263–281 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:265,搜「Atomics.add」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 353–377 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:355,搜「race conditions」)与text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:365(搜「Deadlocks」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 287 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:287,搜「restricted SharedArrayBuffer to secure contexts」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 381–393 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:387,搜「costing ~10-100µs」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 395–429 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:401,搜「wasm-function[123]」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 431–447 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:435,搜「void grayscale」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 449 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:449,搜「80% of the time in this loop」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 451 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:451,搜「cutting runtime by 60%」);SIMD 向量描述见text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:593(搜「128-bit vectors」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 453–455 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:455,搜「persistent Wasm buffer」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 457–459 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:457,搜「4x speedup」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 463–471 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:465,搜「200ms on a 5Mbps」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 473–515 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:515,搜「800KB to 150KB」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 517–527 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:523,搜「mangles function names」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 719–763 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:731,搜「store x, y, and velocity」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 765–803 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:783,搜「fragments cache lines」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 845–877 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:877,搜「slashes overhead by 90%」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 927–965 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:963,搜「1MB of JSON 3x faster」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 967–997 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:989,搜「100 iterations」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 999–1043 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:1039,搜「15ms/frame」)。 ↩ -
出处:「Optimizing WebAssembly Performance」第 1057–1155 段(
text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:1079,搜「wasm-objdump -x」)与text/16-fm-ultimate-webassembly-for-high-performance-apps-b.txt:1151(搜「thresholds」)。 ↩