跳到主要内容

内存管理

WebAssembly 使用线性内存模型,理解内存管理对于开发高效的 WebAssembly 应用至关重要。

线性内存

内存基础

WebAssembly 的内存是一块可增长的连续字节缓冲区,所有数据都存储在这块线性内存中。

内存以页为单位管理,每页大小为 64KB(65536 字节)。

const memory = new WebAssembly.Memory({ 
initial: 1, // 初始 1 页 = 64KB
maximum: 10 // 最大 10 页 = 640KB
});

console.log(memory.buffer.byteLength); // 65536

内存声明

在 WAT 中声明内存:

;; 最小内存声明
(memory 1)

;; 带最大限制
(memory 1 10)

;; 导入内存
(memory (import "env" "memory") 1)

;; 导出内存
(memory (export "memory") 1)

内存增长

const memory = new WebAssembly.Memory({ initial: 1, maximum: 10 });

console.log(memory.buffer.byteLength); // 65536

const previousSize = memory.grow(2); // 增加 2 页
console.log(previousSize); // 1 (之前的大小)
console.log(memory.buffer.byteLength); // 196608 (3页)

在 WAT 中:

(func $grow_memory (param $pages i32) (result i32)
local.get $pages
memory.grow)

内存访问

加载和存储

WebAssembly 提供了多种内存访问指令:

;; 32位整数
i32.load ;; 加载 32 位整数
i32.store ;; 存储 32 位整数

;; 8位整数
i32.load8_s ;; 加载 8 位有符号整数
i32.load8_u ;; 加载 8 位无符号整数
i32.store8 ;; 存储 8 位整数

;; 16位整数
i32.load16_s ;; 加载 16 位有符号整数
i32.load16_u ;; 加载 16 位无符号整数
i32.store16 ;; 存储 16 位整数

;; 64位整数
i64.load ;; 加载 64 位整数
i64.store ;; 存储 64 位整数

;; 浮点数
f32.load ;; 加载 32 位浮点数
f32.store ;; 存储 32 位浮点数
f64.load ;; 加载 64 位浮点数
f64.store ;; 存储 64 位浮点数

偏移量和对齐

;; 默认偏移量 0
i32.load

;; 指定偏移量
i32.load offset=4

;; 指定对齐(2的幂)
i32.load align=4

;; 同时指定偏移和对齐
i32.load offset=8 align=4

内存访问示例

(module
(memory 1)

;; 存储整数数组
(func $store_array (param $ptr i32) (param $index i32) (param $value i32)
local.get $ptr
local.get $index
i32.const 4
i32.mul ;; offset = index * 4
i32.add ;; address = ptr + offset
local.get $value
i32.store)

;; 加载整数数组
(func $load_array (param $ptr i32) (param $index i32) (result i32)
local.get $ptr
local.get $index
i32.const 4
i32.mul
i32.add
i32.load)

(export "store_array" (func $store_array))
(export "load_array" (func $load_array))
(export "memory" (memory 0)))

JavaScript 内存操作

类型化数组视图

const memory = instance.exports.memory;

// 整数视图
const int8View = new Int8Array(memory.buffer);
const uint8View = new Uint8Array(memory.buffer);
const int16View = new Int16Array(memory.buffer);
const uint16View = new Uint16Array(memory.buffer);
const int32View = new Int32Array(memory.buffer);
const uint32View = new Uint32Array(memory.buffer);

// 浮点视图
const float32View = new Float32Array(memory.buffer);
const float64View = new Float64Array(memory.buffer);

// 64位整数视图(需要 BigInt 支持)
const int64View = new BigInt64Array(memory.buffer);
const uint64View = new BigUint64Array(memory.buffer);

使用 DataView

DataView 提供了更灵活的内存访问:

const memory = instance.exports.memory;
const view = new DataView(memory.buffer);

// 读取
const int8 = view.getInt8(offset);
const uint8 = view.getUint8(offset);
const int16 = view.getInt16(offset, true); // true = 小端
const int32 = view.getInt32(offset, true);
const float64 = view.getFloat64(offset, true);

// 写入
view.setInt8(offset, 42);
view.setInt32(offset, 12345, true);
view.setFloat64(offset, 3.14159, true);

内存复制

// 从 JavaScript 复制到 Wasm 内存
function writeToMemory(memory, data, offset) {
const view = new Uint8Array(memory.buffer);
view.set(data, offset);
}

// 从 Wasm 内存复制到 JavaScript
function readFromMemory(memory, offset, length) {
const view = new Uint8Array(memory.buffer, offset, length);
return new Uint8Array(view); // 创建副本
}

Rust 内存管理

使用 wasm-bindgen

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn process_bytes(data: &[u8]) -> Vec<u8> {
data.iter().map(|&b| b.wrapping_add(1)).collect()
}

#[wasm_bindgen]
pub fn process_array(data: Box<[i32]>) -> Box<[i32]> {
data.iter().map(|&x| x * 2).collect()
}

直接内存访问

use wasm_bindgen::prelude::*;
use wasm_bindgen::memory;

#[wasm_bindgen]
pub fn read_memory(offset: usize, length: usize) -> Vec<u8> {
let mem = memory();
let view = js_sys::Uint8Array::new(&mem);
view.slice(offset as u32, (offset + length) as u32).to_vec()
}

#[wasm_bindgen]
pub fn write_memory(offset: usize, data: &[u8]) {
let mem = memory();
let view = js_sys::Uint8Array::new(&mem);
view.set(&js_sys::Uint8Array::from(data), offset as u32);
}

使用 wee_alloc

wee_alloc 是一个轻量级内存分配器,可以减小 Wasm 文件体积:

#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

C/C++ 内存管理

malloc 和 free

#include <stdlib.h>
#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int* create_array(int size) {
return (int*)malloc(size * sizeof(int));
}

EMSCRIPTEN_KEEPALIVE
void free_array(int* arr) {
free(arr);
}

EMSCRIPTEN_KEEPALIVE
void set_element(int* arr, int index, int value) {
arr[index] = value;
}

EMSCRIPTEN_KEEPALIVE
int get_element(int* arr, int index) {
return arr[index];
}

JavaScript 端:

const size = 100;
const ptr = Module._create_array(size);

for (let i = 0; i < size; i++) {
Module._set_element(ptr, i, i * 2);
}

const value = Module._get_element(ptr, 50);
console.log(value); // 100

Module._free_array(ptr);

使用 HEAP 视图

// 直接访问内存
const ptr = Module._create_array(100);

// 使用 HEAP32 视图
const offset = ptr / 4; // Int32 是 4 字节
for (let i = 0; i < 100; i++) {
Module.HEAP32[offset + i] = i * 2;
}

Module._free_array(ptr);

内存布局

典型内存布局

+------------------+ 低地址
| 代码段 |
+------------------+
| 全局数据 |
+------------------+
| 堆(向上增长)|
| ↓ |
+------------------+
| |
| 空闲空间 |
| |
+------------------+
| ↑ |
| 栈(向下增长)|
+------------------+ 高地址

栈和堆

(module
;; 全局变量在静态区
(global $counter (mut i32) (i32.const 0))

(memory 1)

(func $stack_example (result i32)
;; 局部变量在栈上
(local $temp i32)
i32.const 42
local.set $temp
local.get $temp)

;; 堆操作需要手动管理
(func $heap_example (result i32)
;; 使用线性内存作为堆
i32.const 0
i32.load))

内存安全

边界检查

WebAssembly 会自动进行边界检查,越界访问会抛出异常:

try {
const view = new Int32Array(memory.buffer, 0, 100);
view[100000] = 42; // 越界访问
} catch (e) {
console.error('Memory access out of bounds');
}

内存增长后的注意事项

const memory = new WebAssembly.Memory({ initial: 1 });

// 保存视图引用
let view = new Int32Array(memory.buffer);

// 增长内存
memory.grow(1);

// 旧视图失效!需要重新创建
view = new Int32Array(memory.buffer);

避免内存泄漏

// 不好的做法
function badExample() {
const ptr = Module._malloc(1024);
// 忘记释放...
}

// 好的做法
function goodExample() {
const ptr = Module._malloc(1024);
try {
// 使用内存
} finally {
Module._free(ptr);
}
}

共享内存

SharedArrayBuffer

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

console.log(memory.buffer instanceof SharedArrayBuffer); // true

多线程使用

// 主线程
const memory = new WebAssembly.Memory({
initial: 1,
maximum: 10,
shared: true
});

const worker = new Worker('worker.js');
worker.postMessage({ memory });

// worker.js
self.onmessage = function(e) {
const { memory } = e.data;
const view = new Int32Array(memory.buffer);

// 原子操作
Atomics.add(view, 0, 1);
};

内存优化技巧

减少内存分配

// 不好的做法:频繁分配
#[wasm_bindgen]
pub fn process_items(items: Vec<i32>) -> Vec<i32> {
items.iter().map(|&x| x * 2).collect()
}

// 好的做法:复用内存
#[wasm_bindgen]
pub struct Processor {
buffer: Vec<i32>,
}

#[wasm_bindgen]
impl Processor {
pub fn new() -> Self {
Processor { buffer: Vec::new() }
}

pub fn process(&mut self, items: &[i32]) -> &[i32] {
self.buffer.clear();
self.buffer.extend(items.iter().map(|&x| x * 2));
&self.buffer
}
}

批量操作

// 不好的做法:多次调用
for (let i = 0; i < 1000; i++) {
instance.exports.process_item(i);
}

// 好的做法:批量处理
const data = new Int32Array(memory.buffer, 0, 1000);
for (let i = 0; i < 1000; i++) {
data[i] = i;
}
instance.exports.process_batch(0, 1000);

预分配内存

#[wasm_bindgen]
pub struct Buffer {
data: Vec<u8>,
}

#[wasm_bindgen]
impl Buffer {
#[wasm_bindgen(constructor)]
pub fn new(capacity: usize) -> Self {
let mut data = Vec::with_capacity(capacity);
data.resize(capacity, 0);
Buffer { data }
}
}

下一步

理解内存管理后,你可以继续学习: