microsoft / microsoft/mimalloc

Move a heap to a new thread?

Open
#561 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
C
Stars
13.4k
Forks
1.2k
Avg merge
4d 45m
Merged PRs (30d)
13

Description

I'm currently using `mi_heap_new` as a kind of arena allocator

It works very well when allocations only occur on a single thread

What do you suggest doing if the data needs to move to another thread? By "move", I mean arrays potentially need to be re-allocated on a different thread than the one it was created on.

The specific usecase is an HTTP server where most requests are processed by one thread, but if it's a websocket connection or a connection which will need to run some JavaScript, it runs that in a separate thread

In case it helps, here's the code for the arena allocator (zig lets you pass in memory allocators to most functions)

```zig
const mem = @import("std").mem;
const builtin = @import("std").builtin;
const std = @import("std");

const mimalloc = @import("./allocators/mimalloc.zig");
const Environment = @import("./env.zig");
const FeatureFlags = @import("./feature_flags.zig");
const Allocator = mem.Allocator;
const assert = std.debug.assert;

pub const Arena = struct {
heap: *mimalloc.mi_heap_t = undefined,

pub fn backingAllocator(this: Arena) Allocator {
var arena = Arena{ .heap = this.heap.backing() };
return arena.allocator();
}

pub fn allocator(this: Arena) Allocator {
return Allocator{ .ptr = this.heap, .vtable = &c_allocator_vtable };
}

pub fn deinit(this: *Arena) void {
mimalloc.mi_heap_destroy(this.heap);
}

pub fn reset(this: *Arena) void {
this.deinit();
this.* = init() catch unreachable;
}

pub fn init() !Arena {
return Arena{ .heap = mimalloc.mi_heap_new() orelse return error.OutOfMemory };
}

pub fn gc(this: Arena, force: bool) void {
mimalloc.mi_heap_collect(this.heap, force);
}

// Copied from rust
const MI_MAX_ALIGN_SIZE = 16;
inline fn mi_malloc_satisfies_alignment(alignment: usize, size: usize) bool {
return (alignment == @sizeOf(*anyopaque) or
(alignment == MI_MAX_ALIGN_SIZE and size > (MI_MAX_ALIGN_SIZE / 2)));
}

fn alignedAlloc(heap: *mimalloc.mi_heap_t, len: usize, alignment: usize) ?[*]u8 {
if (comptime FeatureFlags.log_allocations) std.debug.print("Malloc: {d}\n", .{len});

// this is the logic that posix_memalign does
var ptr = if (mi_malloc_satisfies_alignment(alignment, len))
mimalloc.mi_heap_malloc(heap, len)
else
mimalloc.mi_heap_malloc_aligned(heap, len, alignment);

return @ptrCast([*]u8, ptr orelse null);
}

fn alloc(
arena: *anyopaque,
len: usize,
alignment: u29,
len_align: u29,
return_address: usize,
) error{OutOfMemory}![]u8 {
_ = return_address;
assert(len > 0);
assert(std.math.isPowerOfTwo(alignment));

var ptr = alignedAlloc(@ptrCast(*mimalloc.mi_heap_t, arena), len, alignment) orelse return error.OutOfMemory;
if (len_align == 0) {
return ptr[0..len];
}

// std.mem.Allocator asserts this, we do it here so we can see the metadata
if (comptime Environment.allow_assert) {
const size = mem.alignBackwardAnyAlign(mimalloc.mi_usable_size(ptr), len_align);

assert(size >= len);
return ptr[0..size];
} else {
return ptr[0..mem.alignBackwardAnyAlign(mimalloc.mi_usable_size(ptr), len_align)];
}
}

fn resize(
_: *anyopaque,
buf: []u8,
buf_align: u29,
new_len: usize,
len_align: u29,
return_address: usize,
) ?usize {
_ = buf_align;
_ = return_address;

if (new_len <= buf.len) {
return mem.alignAllocLen(buf.len, new_len, len_align);
}

const full_len = mimalloc.mi_usable_size(buf.ptr);
if (new_len <= full_len) {
return mem.alignAllocLen(full_len, new_len, len_align);
}

return null;
}

fn free(
_: *anyopaque,
buf: []u8,
buf_align: u29,
return_address: usize,
) void {
_ = buf_align;
_ = return_address;
mimalloc.mi_free(buf.ptr);
}
};

const c_allocator_vtable = Allocator.VTable{
.alloc = Arena.alloc,
.resize = Arena.resize,
.free = Arena.free,
};
```

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reading the mi_heap_new, mi_heap_destroy, mi_heap_collect, and mi_free APIs referenced in the issue, along with the allocator wrapper shown in the body. Determine whether moving allocations between threads is supported and what documented usage should apply to the websocket and JavaScript cases. Done means a clear recommendation or an agreed API change, with any required behavior documented and tested.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, zig
Domain
operating-systems
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.