强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

14 - 性能与优化

14 - 性能与优化

性能不是"跑得快"那么简单——启动时间、内存占用、吞吐量、二进制体积都是重要的维度。


14.1 性能维度

维度说明影响场景
执行速度代码执行的吞吐量计算密集型任务
启动时间从加载到可用的时间Serverless/边缘计算
二进制体积.wasm 文件大小Web 加载、OTA 更新
内存占用运行时内存使用IoT/嵌入式设备
编译时间编译到 Wasm 的耗时开发体验

14.2 Wasm vs Native 性能

为什么 Wasm 比 Native 慢?

Native 代码:
源码 → 编译 → 机器码 → 直接执行
                      ↑ 无开销

Wasm 代码:
源码 → 编译 → Wasm 字节码 → 解码/验证 → AOT/JIT → 执行
                  ↑ 额外步骤              ↑ 额外开销

典型性能开销

操作类型Wasm vs Native原因
纯计算0.9-1.0x(接近原生)JIT 直接映射到机器码
内存访问1.0-1.2x线性内存边界检查
函数调用1.1-1.5x间接调用开销
SIMD 运算0.8-1.1x直接映射到向量指令
多线程1.0-1.3xSharedArrayBuffer 开销
I/O 操作1.5-3xWASI 层开销

14.3 编译流水线

V8 引擎的 Wasm 编译策略

.wasm 字节码
    │
    ▼
解码 + 验证 (Decode + Validate)
    │
    ▼
Liftoff 编译器(基线编译)
    │ 快速编译,代码质量一般
    │ 启动后立即可用
    ▼
TurboFan 编译器(优化编译)
    │ 后台编译,代码质量高
    │ 编译完成后替换 Liftoff 代码
    ▼
优化后的机器码

Liftoff vs TurboFan

特性LiftoffTurboFan
编译速度快(< 100ms)慢(秒级)
代码质量一般优秀
启动时机立即后台
适用场景首次执行长时间运行

14.4 AOT 与 JIT

预编译(AOT)

# Wasmtime AOT 编译
wasmtime compile app.wasm -o app.cwasm

# 运行预编译文件(启动更快)
wasmtime run app.cwasm
// 使用 Wasmtime API 进行 AOT
use wasmtime::*;

let engine = Engine::new(&Config::new())?;

// 编译并序列化
let module = Module::from_file(&engine, "app.wasm")?;
module.serialize_to_file("app.cwasm")?;

// 加载预编译模块(跳过解码和验证)
let module = unsafe { Module::deserialize_file(&engine, "app.cwasm")? };

JIT 编译优化

// V8 会自动对热点函数进行 JIT 优化
// 以下方式可以帮助 V8 更好地优化 Wasm:

// 1. 使用 typed 调用(避免 JS ↔ Wasm 类型转换)
const result = instance.exports.f64_add(1.5, 2.5);  // 直接传递 f64

// 2. 避免频繁的 JS ↔ Wasm 切换
// ❌ 慢:频繁切换
for (let i = 0; i < 1000000; i++) {
  instance.exports.increment();
}

// ✅ 快:在 Wasm 内部循环
instance.exports.process_batch(1000000);

14.5 SIMD 优化

WebAssembly SIMD

SIMD(Single Instruction, Multiple Data)允许一条指令同时处理多个数据:

标量计算(一次处理 1 个 f32):
  a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]
  → 4 条指令

SIMD 计算(一次处理 4 个 f32):
  [a[0], a[1], a[2], a[3]] + [b[0], b[1], b[2], b[3]]
  → 1 条指令

Rust SIMD 示例

// Cargo.toml:
// [dependencies]
// packed_simd_2 = "0.3"

use packed_simd_2::*;

#[wasm_bindgen]
pub fn vector_add_simd(a: &[f32], b: &[f32]) -> Vec<f32> {
    assert_eq!(a.len(), b.len());
    let mut result = vec![0.0f32; a.len()];
    
    let chunks = a.len() / 4;
    for i in 0..chunks {
        let va = f32x4::from_slice_unaligned(&a[i*4..]);
        let vb = f32x4::from_slice_unaligned(&b[i*4..]);
        let vr = va + vb;
        vr.write_to_slice_unaligned(&mut result[i*4..]);
    }
    
    // 处理余数
    for i in (chunks*4)..a.len() {
        result[i] = a[i] + b[i];
    }
    
    result
}

SIMD 性能提升

操作标量SIMD提升
向量加法 (f32 x 4)4 指令1 指令~4x
矩阵乘法 (4x4)64 指令16 指令~4x
像素处理 (RGBA)4 指令/像素1 指令/像素~4x
SHA-256基线SIMD 加速~2-3x

检测 SIMD 支持

function hasSIMD() {
  try {
    const bytes = new Uint8Array([
      0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,
      10,10,1,8,0,65,0,253,15,253,98,11
    ]);
    return WebAssembly.validate(bytes);
  } catch (e) {
    return false;
  }
}

// 渐进增强:支持 SIMD 则加载优化版本
const wasmUrl = hasSIMD() ? 'app-simd.wasm' : 'app.wasm';

14.6 二进制体积优化

优化策略对照

策略效果工具
编译优化 -Oz30-50% 体积减小编译器
LTO (链接时优化)10-30% 体积减小Rust/Cargo
wasm-opt 后优化5-20% 体积减小Binaryen
剥离符号5-15% 体积减小wasm-strip
Brotli 压缩70-85% 传输体积减小Web 服务器
代码分割按需加载手动/工具

Rust 优化配置

# Cargo.toml
[profile.release]
opt-level = "z"       # 最小体积
lto = true             # 链接时优化
codegen-units = 1      # 单编译单元
strip = true           # 剥离符号
panic = "abort"        # panic 时 abort(不展开栈)

[profile.release.build-override]
opt-level = 0          # 构建脚本不优化(加快编译)

C/C++ 优化选项

# Emscripten 优化
emcc input.c -o output.js \
  -Oz \
  -flto \
  -s FILESYSTEM=0 \
  -s DISABLE_EXCEPTION_CATCHING=1 \
  -s MINIMAL_RUNTIME \
  --closure 1

# 后处理
wasm-opt -Oz output.wasm -o output.wasm

体积分析工具

# twiggy (Rust)
cargo install twiggy
twiggy top -n 20 app.wasm
twiggy dominators app.wasm

# bloaty (C/C++)
bloaty app.wasm -d sections
bloaty app.wasm -d compileunits

# wasm-tools
wasm-tools dump app.wasm  # 查看段信息

14.7 启动时间优化

启动时间分解

总启动时间 = 获取时间 + 解码时间 + 验证时间 + 编译时间 + 实例化时间

                    网络 I/O        CPU 处理
                   ┌──────────┐  ┌─────────────────────────┐
获取 ──────────────►│ .wasm    │  │ 解码 | 验证 | 编译 | 实例化│
(fetch/缓存)       │ 下载完成  │  └─────────────────────────┘
                   └──────────┘
                   ↑ 可以用流式编译重叠这部分

优化策略

// 1. 流式编译(边下载边编译)
const { instance } = await WebAssembly.instantiateStreaming(
  fetch('app.wasm'), imports
);

// 2. 编译缓存到 IndexedDB
async function compileWithCache(url) {
  const cacheKey = `wasm:${url}`;
  const db = await openCache();
  
  // 尝试从缓存加载
  const cached = await db.get(cacheKey);
  if (cached) {
    return WebAssembly.instantiate(cached, imports);  // 跳过编译
  }
  
  // 编译并缓存
  const module = await WebAssembly.compileStreaming(fetch(url));
  await db.put(cacheKey, module);
  return WebAssembly.instantiate(module, imports);
}

// 3. AOT 预编译(服务端或构建时)
// 在部署前完成编译,运行时直接实例化

// 4. 预编译 + 冻结
// V8 支持将编译后的代码序列化到磁盘
// Chrome: chrome://flags/#v8-cache-options

启动时间对比

方案首次加载缓存加载说明
instantiate(buffer)200ms150ms同步,阻塞主线程
compile + instantiate180ms100ms异步
instantiateStreaming120ms80ms流式编译
IndexedDB 缓存120ms20ms跳过编译
AOT 预编译15ms15ms仅需实例化

14.8 内存管理优化

内存分配策略

// 简单的 Arena 分配器
struct Arena {
    buffer: Vec<u8>,
    offset: usize,
}

impl Arena {
    fn new(size: usize) -> Self {
        Arena {
            buffer: vec![0u8; size],
            offset: 0,
        }
    }
    
    fn alloc(&mut self, size: usize, align: usize) -> *mut u8 {
        let aligned = (self.offset + align - 1) & !(align - 1);
        if aligned + size > self.buffer.len() {
            return std::ptr::null_mut();  // 内存不足
        }
        self.offset = aligned + size;
        unsafe { self.buffer.as_mut_ptr().add(aligned) }
    }
    
    fn reset(&mut self) {
        self.offset = 0;  // O(1) 重置
    }
}

避免内存碎片

频繁的 malloc/free 会导致碎片:
[已用][空闲][已用][空闲][已用][空闲]  → 内存利用率低

Arena 分配器可以避免碎片:
[分配][分配][分配][分配][......空闲......]  → 紧凑排列

然后一次性 reset:
[空闲][空闲][空闲][空闲][空闲][空闲]  → 立即可用

内存增长策略

// 预分配足够内存,避免频繁 grow
// ❌ 频繁增长
let mut data = Vec::new();
for i in 0..1000000 {
    data.push(i);  // 可能多次 grow
}

// ✅ 预分配
let mut data = Vec::with_capacity(1000000);
for i in 0..1000000 {
    data.push(i);  // 不会 grow
}

14.9 线程与并行

SharedArrayBuffer + Web Workers

// 创建共享内存
const memory = new WebAssembly.Memory({
  initial: 1,
  maximum: 16,
  shared: true
});

// 多个 Worker 操作同一块内存
const workers = Array.from({ length: 4 }, () => new Worker('worker.js'));

// 分配不同区域给不同 Worker
const chunkSize = 64 * 1024;  // 64KB 每块
for (let i = 0; i < workers.length; i++) {
  workers[i].postMessage({
    memory,
    offset: i * chunkSize,
    length: chunkSize
  });
}
// Wasm 内部使用原子操作
use std::sync::atomic::{AtomicI32, Ordering};

static COUNTER: AtomicI32 = AtomicI32::new(0);

#[wasm_bindgen]
pub fn increment_counter() -> i32 {
    COUNTER.fetch_add(1, Ordering::SeqCst)
}

#[wasm_bindgen]
pub fn get_counter() -> i32 {
    COUNTER.load(Ordering::SeqCst)
}

14.10 性能分析工具

Chrome DevTools Performance 面板

1. 打开 DevTools → Performance
2. 点击 Record
3. 执行 Wasm 操作
4. 查看火焰图中的 Wasm 函数

浏览器内测量

// 精确计时
performance.mark('wasm-start');
instance.exports.heavy_compute();
performance.mark('wasm-end');
performance.measure('wasm', 'wasm-start', 'wasm-end');

const measures = performance.getEntriesByName('wasm');
console.log(`Wasm: ${measures[0].duration}ms`);

Wasmtime 性能分析

# 使用 perf 工具
perf record -g wasmtime app.wasm
perf report

# 使用 Wasmtime 内置计时
wasmtime --profile=perfmap app.wasm

14.11 注意事项

⚠️ 不要过早优化:先确保正确性,再优化性能。使用 profiling 工具找到真正的瓶颈。

⚠️ 平台差异:不同浏览器的 Wasm 引擎(V8/SpiderMonkey/JavaScriptCore)优化策略不同,需要在目标平台上测试。

⚠️ i64 性能:i64 操作在某些平台上(尤其是 32 位环境)可能比 i32 慢。仅在需要时使用 i64。

⚠️ SIMD 可用性:虽然 SIMD 支持率已很高,但仍需要提供非 SIMD 的回退方案。


14.12 扩展阅读


下一章15 - 最佳实践 — 将所学知识整合为可落地的工程实践。