mem
mem is the heap: raw blocks of memory for buffers you hand to C, to t0
code, or manage yourself. Most code never needs it: strings and the
containers collections generates allocate and free by themselves.
import mem from "mem";The module is @profile(t1) (it reaches the runtime library’s memory
primitives) and pure.
Example
import mem from "mem";
fn checksum(n: i64) -> i64 or mem.MemError {
let buf: *u8 = mem.alloc(n) or return; // zeroed
defer mem.free(buf, n);
for (i in 0..n) { buf[i] = (i % 251) as u8; }
var sum: i64 = 0;
for (i in 0..n) { sum = sum + buf[i] as i64; }
return sum;
}
pub fn main() -> i32 {
let s = checksum(1000) or { return 1; };
return (s % 256) as i32;
}Running out of memory
alloc and realloc return MemError.OutOfMemory when there is no memory;
handle it like any error (or return, or { ... }). When a program has no
better answer than stopping, alloc_or_trap does that, with the line every
allocation the compiler makes uses:
trap: out of memoryand exit code 101, in the VM and natively. The standard library’s own
internals use alloc_or_trap; its functions that already return an error
pass running out of memory on as theirs (gfx.new_surface gives
GfxError.OutOfMemory).
Functions
| Function | Signature | Description |
|---|---|---|
alloc | (size: i64) -> *u8 or MemError | size zeroed bytes, 16-byte aligned. A size of 0 or less is null. |
alloc_or_trap | (size: i64) -> *u8 | alloc, trapping with trap: out of memory instead of returning the error. |
realloc | (p: *u8, old: i64, new: i64) -> *u8 or MemError | A block of new bytes holding the first min(old, new) bytes of p (the rest zeroed); p is freed. A null p is alloc(new); a new of 0 or less frees p and is null. On OutOfMemory, p is left as it was. |
free | (p: *u8, size: i64) | Give back a block alloc or realloc returned. size is a hint the runtime may ignore: pass 0 when you don’t know it. A null p is nothing. |
heap_arena | (size: i64) -> arena.Arena or MemError | An arena.Arena over size bytes of heap. |
free_arena | (a: *arena.Arena) | Free a heap_arena’s memory; the arena is empty afterwards. |
live_allocs | () -> i64 | Heap blocks currently allocated, string buffers included. |
alloc_count | () -> i64 | Heap blocks allocated so far, freed or not, string buffers included. Both counts may be read from any thread. |
copy_raw | (dst: *u8, src: *const u8, n: i64) | Copy n bytes (not overlapping). |
move_raw | (dst: *u8, src: *const u8, n: i64) | Copy n bytes that may overlap. |
set_raw | (dst: *u8, val: u8, n: i64) | Fill n bytes with val. |
The copies and fills are the builtins @move_bytes and @fill_bytes: the
machine’s block move, many gigabytes a second, not a byte loop. A length of
0 or less does nothing.
| zero_raw | (dst: *u8, n: i64) | Zero n bytes. |
| copy, move, set, zero | (dst: *u8, src: *u8, count: u64), … | The same with a u64 count. |
| compare | (a: *u8, b: *u8, count: u64) -> i32 | -1, 0 or 1, by the first differing byte. |
| equal | (a: *u8, b: *u8, count: u64) -> bool | Byte equality. |
With slices, use bytes (copy, fill, compare, …)
instead of the raw forms.
Checking for leaks
live_allocs() counts the heap blocks currently allocated. Compare it before
and after a piece of code: if it grew, something leaked; if it shrank,
something was freed twice.
let before: i64 = mem.live_allocs();
work();
@assert(mem.live_allocs() == before);alloc_count() counts every block allocated so far, freed or not: the
difference across a piece of code is how many allocations it made, which
finds the stage of a program that allocates most.
let start: i64 = mem.alloc_count();
parse(text);
io.println("parse: {mem.alloc_count() - start} allocations");Last modified: