WebAssembly 中的内存模型
WebAssembly 中的内存模型
要执行 JavaScript 代码,JavaScript 引擎需要下载资源。JavaScript 引擎会等待资源下载完成。下载完成后,JavaScript 引擎会进行解析。解析器会将源代码转换为 JavaScript 解释器可以执行的字节码。
当一个函数被多次调用时,编译器baseline compiler(在 v8 中)会编译代码。编译过程在主线程中进行。编译器需要花费一些时间进行编译。但是,编译后的代码比解释执行的代码运行速度更快。编译后的代码会经过优化optimising compiler。
当函数被频繁调用时,编译器会标记该函数并尝试进一步优化。在此过程中re-optimisation,编译器会假设并生成更优化的代码。这种优化需要一些时间,但生成的代码速度更快。
函数执行完毕。最后,代码被垃圾回收。
WebAssembly速度很快。🚀
JavaScript引擎下载WebAssembly模块。下载完成后,WebAssembly模块会被解码。
解码比解析快。
WebAssembly 模块解码完成后,会进行编译和优化。由于该模块已经过编译和优化,因此这一步骤速度很快。
模块最终执行完毕。
注意:没有单独的垃圾回收步骤。WebAssembly 模块负责内存的分配和释放。
点击此处查看我关于 Rust 和 WebAssembly 的书。
为了加快 WebAssembly 的执行速度,浏览器厂商实现了流式编译。流式编译使得 JavaScript 引擎能够在 WebAssembly 模块仍在下载的同时进行编译和优化。这与 JavaScript 不同,JavaScript 引擎必须等到文件完全下载后才能进行编译。这大大加快了处理速度。
在浏览器层面,JavaScript 和 WebAssembly 是两种不同的技术。从 JavaScript 调用 WebAssembly 或反之亦然,速度都很慢。(任何两种语言之间的调用都存在这个问题)。这是因为跨越语言边界需要付出额外的代价。
浏览器厂商(尤其是 Firefox)正努力降低跨边界调用的成本。事实上,在 Firefox 中,JavaScript 到 WebAssembly 的调用速度远快于非内联的 JavaScript 到 JavaScript 的调用。
但是,在设计应用程序时,仍然需要妥善处理边界跨越问题。边界跨越可能会成为应用程序性能的主要瓶颈。在这种情况下,理解 WebAssembly 模块的内存模型至关重要。
WebAssembly 中的内存模型
memory sectionWebAssembly 模块的元素是一个线性存储器向量。
线性记忆模型
线性内存模型是一种内存寻址技术,它将内存组织在一个单一的、连续的地址空间中。它也被称为扁平内存模型。
线性内存模型使得理解、编程和表示内存变得更加容易。
它们存在巨大的劣势,例如
- 元素重排执行时间长
- 浪费大量内存空间
内存是一个包含未经解释的原始字节数据的向量。它们使用可调整大小的数组缓冲区来保存这些原始字节数据。JavaScript 和 WebAssembly 可以同步地对内存进行读写操作。
我们可以使用 JavaScript 的构造函数来分配内存WebAssembly.memory()。
写点代码✍️
从 WebAssembly 过渡到 JavaScript
我们先来看看如何将值从 WebAssembly 模块(用 Rust 编写)通过内存传递到 JavaScript。
使用以下方式创建一个新项目cargo。
$ cargo new --lib memory_world
项目创建成功后,请在您喜欢的编辑器中打开项目。接下来,我们将src/lib.rs使用以下内容进行编辑。
#![no_std]
use core::panic::PanicInfo;
use core::slice::from_raw_parts_mut;
#[no_mangle]
fn memory_to_js() {
let obj: &mut [u8];
unsafe {
obj = from_raw_parts_mut::<u8>(0 as *mut u8, 1);
}
obj[0] = 13;
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> !{
loop{}
}
将此添加到Cargo.toml:
[lib]
crate-type = ["cdylib"]
那里有什么?
Rust 文件以 `.` 开头#![no_std]。该#![no_std]属性指示 Rust 编译器回退到 core crate 而不是 std crate。core crate 与平台无关,并且是 std crate 的一个较小子集。这可以显著减小二进制文件的大小。
该函数memory_to_js带有注解#[no_mangle]。此函数不返回任何值,因为它会改变共享内存中的值。
我们定义一个可变切片,u8并将其命名为 ` Slice` obj。然后我们使用`Slice` 通过指针和长度from_raw_parts_mut创建一个 `Slice` u8。默认情况下,内存从 `Slice` 开始0,我们只取1其中的元素。
我们正在访问原始内存,因此我们将调用封装在unsafe代码块中。生成的切片from_raw_parts_mut是可变的。
最后,我们13在第一个索引处进行赋值。
unsafe {
obj = from_raw_parts_mut::<u8>(0 as *mut u8, 1);
}
obj[0] = 13;
我们还定义了一个panic_handler用于捕获任何 panic 并暂时忽略它的功能(不要在生产应用程序中这样做)。
请注意,我们这里没有使用
wasm_bindgen。
在 JavaScript 中,我们加载 WebAssembly 模块,并直接从该模块访问内存。
首先,获取并实例化 WebAssembly 模块。
const bytes = await fetch("target/wasm32-unknown-unknown/debug/memory_world.wasm");
const response = await bytes.arrayBuffer();
const result = await WebAssembly.instantiate(response, {});
结果对象是包含所有导入和导出函数的 WebAssembly 对象。我们memory_to_js从中调用导出的函数result.exports。
result.exports.memory_to_js();
这会调用 WebAssembly 模块的memory_to_js函数,并将值赋给共享内存。
共享内存按对象导出result.exports.memory.buffer。
const memObj = new UInt8Array(result.exports.memory.buffer, 0).slice(0, 1);
console.log(memObj[0]); // 13
内存访问通过load二进制指令进行。这些二进制指令使用 `_`和 `_`store进行访问。`_`采用以 2 为底的对数表示。offsetalignmentalignment
注意:WebAssembly 目前仅提供
32-bit地址范围。未来,WebAssembly 可能会提供64-bit地址范围。
从 JavaScript 过渡到 WebAssembly
我们已经了解了如何在 JavaScript 和 WebAssembly 之间共享内存,方法是在 Rust 中创建内存。现在,是时候在 JavaScript 中创建内存并在 Rust 中使用它了。
JavaScript 的内存管理机制无法直接告诉 WebAssembly 需要分配什么内存以及何时释放。由于 WebAssembly 是一种类型系统,它需要显式的类型信息。因此,我们需要告诉 WebAssembly 如何分配内存以及如何释放内存。
要通过 JavaScript 创建内存,请使用WebAssembly.Memory()构造函数。
内存构造函数接受一个对象来设置默认值。它们是:
- 初始大小 - 内存的初始大小
- 最大值 - 内存的最大容量(可选)
- 共享 - 表示是否使用共享内存
初始和最大页数单位均为(WebAssembly)页。每页最多可容纳 64KB。
写点代码✍️
初始化内存,
const memory = new WebAssembly.Memory({initial: 10, maximum: 100});
内存通过WebAssembly.Memory()构造函数初始化,初始值为 640KB 10 pages,最大值为 6.4MB 100 pages。这分别相当于初始值 640KB 和最大值 6.4MB。
const bytes = await fetch("target/wasm32-unknown-unknown/debug/memory_world.wasm");
const response = await bytes.arrayBuffer();
const instance = await WebAssembly.instantiate(response, { js: { mem: memory } });
我们获取 WebAssembly 模块并实例化它们。但在实例化时,我们会传入内存对象。
const s = new Set([1, 2, 3]);
let jsArr = Uint8Array.from(s);
我们创建一个typedArray括号UInt8Array,其值为 1、2 和 3。
const len = jsArr.length;
let wasmArrPtr = instance.exports.malloc(length);
WebAssembly 模块无法感知内存中创建的对象的大小。WebAssembly 需要分配内存。我们必须手动编写内存分配和释放的代码。在此步骤中,我们传递数组的长度并分配相应的内存。这将为我们提供指向内存位置的指针。
let wasmArr = new Uint8Array(instance.exports.memory.buffer, wasmArrPtr, len);
然后,我们创建一个新的 typedArray,其中包含缓冲区(总可用内存)、内存偏移量(wasmAttrPtr)和内存长度。
wasmArr.set(jsArr);
最后,我们将本地创建的 typedArray ( jsArr) 设置到 typedArray 中wasmArrPtr。
const sum = instance.exports.accumulate(wasmArrPtr, len); // -> 7
console.log(sum);
我们将数据发送pointer到内存和lengthWebAssembly 模块。在 WebAssembly 模块中,我们从内存中获取值并使用它。
在 Rust 中,` mallocand`accumulate函数如下:
use std::alloc::{alloc, dealloc, Layout};
use std::mem;
#[no_mangle]
fn malloc(size: usize) -> *mut u8 {
let align = std::mem::align_of::<usize>();
if let Ok(layout) = Layout::from_size_align(size, align) {
unsafe {
if layout.size() > 0 {
let ptr = alloc(layout);
if !ptr.is_null() {
return ptr
}
} else {
return align as *mut u8
}
}
}
std::process::abort
}
根据指定的大小,malloc 函数会分配一块内存。
#[no_mangle]
fn accumulate(data: *mut u8, len: usize) -> i32 {
let y = unsafe { std::slice::from_raw_parts(data as *const u8, len) };
let mut sum = 0;
for i in 0..len {
sum = sum + y[i];
}
sum as i32
}
该accumulate函数接收共享数组及其大小作为参数len。然后,它data从共享内存中恢复数据。接着,它遍历数组data并返回所有传入元素的总和。
如果你喜欢这篇文章,那么你或许也会喜欢我写的关于 Rust 和 WebAssembly 的书。点击这里查看。
👇仓库👇
sendilkumarn / rustwasm-memory-model
使用 Rust 在 WebAssembly 和 JavaScript 之间共享内存
有兴趣进一步了解。
如何使用 JavaScript API 实现 WebAssembly 内存,请点击此处。
WebAssembly 中的内存访问安全性在此处进行检查。
from_raw_parts_mut点击这里查看更多信息。
点击此处了解更多关于 TypedArray 的信息
🐦 Twitter // 💻 GitHub // ✍️ 博客// 🔶 Hacker News
如果你喜欢这篇文章,请点赞或留言。❤️
文章来源:https://dev.to/sendilkumarn/rust-and-web assembly-for-the-masses-memory-model-1jhd


