Jan 10, 2020

Memory Model in WebAssembly

For JavaScript to execute, the JavaScript engine should download the resources. The JavaScript engine wait until the resources are downloaded. Once downloaded, the JavaScript engine parses. The parser converts the source code to byte code that JavaScript interpreter runs.

When a function is called multiple times. The baseline compiler (in v8) compiles the code. The compilation happens in the main thread. The compiler spends time for compilation. But the compiled code runs faster than the interpreted code. The compiled code is optimised by the optimising compiler.

When function is called a lot more. The compiler marks the function and tries to optimise further. During this re-optimisation, compiler assumes and produces even more optimised code. This optimisation takes a bit of time but the generated code is much more faster.

The function is executed. Finally, the code is garbage collected.

Alt Text

WebAssembly is fast. 🚀

The JavaScript engine download the WebAssembly module. Once downloaded the WebAssembly module is decoded.

Decoding is faster than parsing.

Once decoded, the WebAssembly module is compiled and optimised. This step is fast because the module has already been compiled and optimised.

The module is finally executed.

Alt Text

Note: there is no separate garbage collection step. The WebAssembly module takes care of allocating and de-allocating the memory.

In the quest of speeding up WebAssembly execution, the browser vendors implemented streaming compilation. Streaming compilation enables JavaScript engines to compile and optimise the module while the WebAssembly module is still downloading. Unlike JavaScript, where the engines should wait until the file is completely downloaded. This speeds up the process.


JavaScript and WebAssembly are two different things at the browser level. Calling WebAssembly from JavaScript or vice versa is slow. (This holds good for calls between any two languages). This is because crossing boundaries has a cost attached to it.

The browser vendors (especially Firefox) are trying to reduce the cost of boundary crossing. In fact, in Firefox the JavaScript to WebAssembly call is much faster than the non-inlined JavaScript to JavaScript calls.

But still proper care should be given to the boundary crossing while designing your application. They can be a major performance bottleneck for the application. In those cases it is important to understand the memory model of the WebAssembly module.

Memory model in WebAssembly

The memory section of the WebAssembly module is a vector of linear memories.

Linear Memory Model

A linear memory model is a memory addressing technique in which the memory is organized in a single contagious address space. It is also known as Flat memory model.

While the linear memory model makes it easier to understand, program, and represent the memory.

They have huge disadvantages like

  • high execution time for rearranging elements
  • wastes a lot of memory area

Alt Text

The memory is a vector of raw bytes of uninterpreted data. They use resizable array buffers to hold the raw bytes of memory. JavaScript and WebAssembly can synchronously read and write into the memory.

We can allocate the memory using WebAssembly.memory() constructor from JavaScript.


Write some code ✍️

Passing from WebAssembly to JavaScript

Let us first see how to pass values through memory from WebAssembly Module (written with Rust) to JavaScript.

Create a new project using cargo.

$ cargo new --lib memory_world

Once the project is successfully created. Open the project in your favourite editor. Let us edit the src/lib.rs with the following contents

#![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{}
}

What is there?

The rust file starts with #![no_std]. The #![no_std] attribute instructs the rust compiler to fallback to core crate instead of std crate. The core crate is platform agnostic. The core crate is a smaller sub set of the std crate. This reduce the binary size dramatically.

The function memory_to_js is annotated with #[no_mangle]. This function does not return any value, because it changes the value in the shared memory.

We define a mutable slice of type u8 and name it as obj. Then we use from_raw_parts_mut to create a u8 using a pointer and length. By default the memory starts at 0 and we just take 1 element.

We are accessing the raw memory so we wrap the calls inside the unsafe block. The generated slice from from_raw_parts_mut is mutable.

Finally we assign 13 in the first index.

unsafe {
    obj = from_raw_parts_mut::<u8>(0 as *mut u8, 1);
}

obj[0] = 13;

We have also defined a panic_handler to capture any panics and ignore it for now (do not do this in your production application).

Note that we are not using wasm_bindgen here.

In JavaScript, we load the WebAssembly module and access the memory straightaway from the module.

First fetch and instantiate the WebAssembly module.

const bytes = await fetch(
  'target/wasm32-unknown-unknown/debug/memory_world.wasm'
)
const response = await bytes.arrayBuffer()
const result = await WebAssembly.instantiate(response, {})

The result object is the WebAssembly object that contains all the imported and exported functions. We call the exported memory_to_js function from the result.exports.

result.exports.memory_to_js()

This calls the WebAssembly module's memory_to_js function and assigns the value in the shared memory.

The shared memory is exported by result.exports.memory.buffer object.

const memObj = new UInt8Array(result.exports.memory.buffer, 0).slice(0, 1)
console.log(memObj[0]) // 13

The memory is accessed via load and store binary instructions. These binary instructions are accessed with the offset and the alignment. The alignment is in base 2 logarithmic representation.

Note: WebAssembly currently provides only 32-bit address ranges. In future, WebAssembly may provide 64-bit address range.


Passing from JavaScript to WebAssembly

We have seen how to share memory between JavaScript and WebAssembly, by creating the memory in Rust. Now it is time to create memory in JavaScript and use it inside Rust.

The memory in the JavaScript land has no way to tell the WebAssembly land what to allocate and when to free them. Being type, WebAssembly expects explicit type information. We need to tell the WebAssembly land how to allocate the memory and then how to free them.

To create the memory via JavaScript, use the WebAssembly.Memory() constructor.

The memory constructor takes in an object to set the defaults. They are

  • initial - The initial size of the memory
  • maximum - The maximum size of the memory (Optional)
  • shared - to denote whether to use the shared memory

The unit for initial and maximum is (WebAssembly) pages. Each page hold upto 64KB.


Write some code ✍️

Initialise the memory,

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

The memory is initialized with WebAssembly.Memory() constructor with an initial value of 10 pages and a maximum value of 100 pages. This translates to 640KB and 6.4MB initial and maximum respectively.

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 },
})

We fetch the WebAssembly Module and instantiate them. But while instantiating we pass in the memory object.

const s = new Set([1, 2, 3])
let jsArr = Uint8Array.from(s)

We create a typedArray (UInt8Array) with values 1, 2, and 3.

const len = jsArr.length
let wasmArrPtr = instance.exports.malloc(len)

WebAssembly modules will not have any clue about the objects size that are created in the memory. WebAssembly needs to allocate memory. We have to manually write the allocation and freeing of memory. In this step, we send the length of the array and allocate that memory. This will give us a pointer to the location of the memory.

let wasmArr = new Uint8Array(instance.exports.memory.buffer, wasmArrPtr, len)

We then create a new typedArray with the buffer (total available memory), memory offset (wasmAttrPtr), and the length of the memory.

wasmArr.set(jsArr)

We finally set the locally created typedArray (jsArr) into the typedArray wasmArrPtr.

const sum = instance.exports.accumulate(wasmArrPtr, len) // -> 7
console.log(sum)

We are sending the pointer (to memory) and length to WebAssembly module. In the WebAssembly module we fetch the value from the memory and use them.

In the Rust, the malloc and accumulate functions are as follows:

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
}

Given the size, the malloc function allocates a block of memory.

#[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
}

The accumulate function takes in the shared array and the size (len). It then recovers the data from the shared memory. Then runs through the data and returns the sum of all the elemnts passed in the data.


Repo


Interested to explore further

WebAssembly Memory using JavaScript API at here

Memory access in the WebAssembly is safer check at here

Check out more about from_raw_parts_mut at here

Check out more about TypedArray here


Up Next