What is WebAssembly Linear Memory?
In WebAssembly, linear memory is a contiguous block of memory used by a module. It is designed to store data, such as variables, arrays and objects, in binary format. This memory model is managed by the WebAssembly runtime and is accessible to both WebAssembly and JavaScript.
Key Features:
- Flat Memory Model: Memory is represented as a single array of bytes.
- Resizable: Memory can grow (in pages of 64 KiB each) as needed but cannot shrink.
- Shared Memory: It can be shared between WebAssembly and JavaScript for seamless data exchange.
Declaring and Exporting Memory
Memory Declaration in WebAssembly Text Format (WAT)
Memory is declared in the .wat (WebAssembly text format) file and can be exported for use in JavaScript.
Example:
(module
(memory (export "memory") 1 2) ;; Declare memory with initial size of 1 page and maximum size of 2 pages
)
- Initial Size: The memory starts with 1 page (64 KiB).
- Maximum Size: The memory can grow up to 2 pages (128 KiB).
Importing Memory in WebAssembly
Memory can also be imported if you want JavaScript to define it:
(module
(import "env" "memory" (memory 1 2)) ;; Import memory defined in JavaScript
)
Accessing Memory in JavaScript
WebAssembly exports memory as an ArrayBuffer, which allows JavaScript to create typed arrays for interacting with binary data.
Example: Accessing Exported Memory
(async () => {
const response = await fetch('module.wasm');
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes);
const memory = new Uint8Array(instance.exports.memory.buffer); // Access memory as Uint8Array
// Write data to memory
memory[0] = 42;
console.log(memory[0]); // Output: 42
})();
Static vs. Dynamic Memory Allocation
Memory in WebAssembly can be allocated either statically or dynamically.
1. Static Memory Allocation
Static memory allocation occurs during the compilation of the WebAssembly module. Variables and arrays are assigned specific memory locations.
Example: Statically Allocated Data
(module
(memory (export "memory") 1) ;; Export memory
(data (i32.const 0) "Hello, WebAssembly!") ;; Write string at memory offset 0
)
In JavaScript:
(async () => {
const { instance } = await WebAssembly.instantiateStreaming(fetch('module.wasm'));
const memory = new Uint8Array(instance.exports.memory.buffer);
// Read the statically allocated string
let text = '';
for (let i = 0; i < 18; i++) {
text += String.fromCharCode(memory[i]);
}
console.log(text); // Output: "Hello, WebAssembly!"
})();
2. Dynamic Memory Allocation
Dynamic memory allocation allows programs to allocate and deallocate memory at runtime. WebAssembly does not have built-in support for dynamic memory management like malloc and free in C, but developers often use custom allocators.
Implementing Dynamic Memory Allocation
A basic custom allocator can manage memory allocation in WebAssembly. Below is a simplified example.
WebAssembly Module (WAT):
(module
(memory (export "memory") 1) ;; Export 1 page of memory
(global $heap_base (mut i32) (i32.const 1024)) ;; Heap starts at 1024 bytes
(func $malloc (param $size i32) (result i32)
(local $addr i32)
(global.get $heap_base)
(local.set $addr)
(global.get $heap_base)
(local.get $size)
(i32.add)
(global.set $heap_base)
(local.get $addr)
)
(export "malloc" (func $malloc))
)
JavaScript Code:
(async () => {
const response = await fetch('module.wasm');
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes);
const memory = new Uint8Array(instance.exports.memory.buffer);
const malloc = instance.exports.malloc;
// Allocate 10 bytes of memory
const address = malloc(10);
console.log("Allocated memory starts at address:", address);
// Write and read data in the allocated memory
memory[address] = 255; // Write data
console.log("Data at allocated address:", memory[address]); // Output: 255
})();
Explanation:
- Heap Management: The heap starts at a specific memory address (heap_base) and grows dynamically.
- malloc Function: Allocates a block of memory and updates the heap base.
Example: Growing Memory in JavaScript
(async () => {
const response = await fetch('module.wasm');
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes);
const memory = instance.exports.memory;
console.log("Initial Memory Size:", memory.buffer.byteLength); // Output: 65536 (1 page)
memory.grow(1); // Add 1 page (64 KiB)
console.log("New Memory Size:", memory.buffer.byteLength); // Output: 131072 (2 pages)
})();
Best Practices for Memory Allocation in WebAssembly
- Use Typed Arrays:
- Use Uint8Array, Int32Array, or other typed arrays for efficient interaction with memory.
- Pre-allocate Memory:
- Reserve sufficient memory during initialization to reduce the need for growth at runtime.
- Minimize Memory Access Overhead:
- Batch memory read/write operations whenever possible to reduce performance overhead.
- Custom Allocators:
- Implement custom allocators like malloc for managing complex memory requirements.
- Bounds Checking:
- Always check memory bounds when accessing memory to avoid out-of-bounds errors.