mimalloc icon indicating copy to clipboard operation
mimalloc copied to clipboard

how to use preallocted memory

Open simaocat opened this issue 9 months ago • 6 comments

I pre-allocate a large block of memory using mmap, for example, 1GB, and then use the mi_manage_os_memory function to manage this block of memory. After that, I allocate several memory blocks. However, the results printed by the program show that the addresses of several pointers are not within the allocated address space. Could there be something wrong with my usage? I hope to use mimalloc to manage the pre-allocated large block of memory as a buffer for subsequent allocations and deallocations. ` #include <sys/mman.h> #include <mimalloc.h> #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <string.h>

int main() { const size_t size = 1UL << 30; // 1GB int protection = PROT_READ | PROT_WRITE; int flags = MAP_ANONYMOUS | MAP_SHARED; void* addr = mmap(NULL, size, protection, flags, -1, 0); if (addr == MAP_FAILED) { return 1; }

memset(addr, 0, size);
printf("addr: %p, end:%p\n", addr, addr+size);
bool ok = mi_manage_os_memory(addr, size, true, false, false, -1);
if (!ok) {
    munmap(addr, size);
    return 1;
}
void* p1 = mi_malloc(1 * 1024 * 1024); // 1MB
void* q1 = mi_malloc(1 * 1024 * 1024); // 1MB
void* r1 = mi_malloc(1 * 1024 * 1024); // 1MB
void* s1 = mi_malloc(1 * 1024 * 1024); // 1MB
printf("p=%p, q=%p, r=%p, s=%p\n", p1, q1, r1, s1);
mi_free(p1);
mi_free(q1);
mi_free(r1);
mi_free(s1);

return 0;

}`

possible output: addr: 0x7ff19b690000, end:0x7ff1db690000 p=0x7ff15b6c0080, q=0x7ff15b7d0080, r=0x7ff15b8e0080, s=0x7ff15ba80080

simaocat avatar Mar 11 '25 04:03 simaocat

Problem Diagnosis: The issue occurs because mi_manage_os_memory only informs mimalloc about the available memory block but does not guarantee that allocations will happen within that block. As a result, mi_malloc may still allocate memory elsewhere.

  • Solution Instead of mi_manage_os_memory, use mi_manage_os_memory_ex along with mi_heap_malloc to ensure that allocations are made inside the preallocated block.

✅ Working Example:

#include <sys/mman.h>
#include <mimalloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

int main() {
    const size_t size = 1UL << 30; // 1GB
    int protection = PROT_READ | PROT_WRITE;
    int flags = MAP_ANONYMOUS | MAP_SHARED;
    void* addr = mmap(NULL, size, protection, flags, -1, 0);
    
    if (addr == MAP_FAILED) {
        return 1;
    }

    memset(addr, 0, size);
    printf("addr: %p, end:%p\n", addr, (char*)addr + size);

    // Use mi_manage_os_memory_ex to obtain an arena identifier
    mi_arena_id_t arena_id;
    bool ok = mi_manage_os_memory_ex(addr, size, true, false, false, -1, true, &arena_id);
    if (!ok) {
        munmap(addr, size);
        return 1;
    }

    // Create a heap within the specified arena
    mi_heap_t* my_heap = mi_heap_new_in_arena(arena_id);
    if (!my_heap) {
        munmap(addr, size);
        return 1;
    }

    // Allocate memory using the specific heap
    void* p1 = mi_heap_malloc(my_heap, 1 * 1024 * 1024); // 1MB
    void* q1 = mi_heap_malloc(my_heap, 1 * 1024 * 1024); // 1MB
    void* r1 = mi_heap_malloc(my_heap, 1 * 1024 * 1024); // 1MB
    void* s1 = mi_heap_malloc(my_heap, 1 * 1024 * 1024); // 1MB
    
    printf("p=%p, q=%p, r=%p, s=%p\n", p1, q1, r1, s1);

    // Free memory
    mi_free(p1);
    mi_free(q1);
    mi_free(r1);
    mi_free(s1);

    // Clean up heap and mapped memory
    mi_heap_delete(my_heap);
    munmap(addr, size);

    return 0;
}
  • Why does this work? mi_manage_os_memory_ex returns an arena_id. mi_heap_new_in_arena(arena_id) creates a heap inside that arena. mi_heap_malloc(my_heap, size) ensures that allocations happen inside the preallocated block.

Tested and confirmed working. Hope this helps! 🎯

https://microsoft.github.io/mimalloc/group__extended.html#ga41ce8525d77bbb60f618fa1029994f6e

adelcor avatar Mar 18 '25 14:03 adelcor

Thank you very much for your reply. I tried it and it works fine, but I have the following questions that need to be answered:

  1. When this program exits, it produces a core dump. If I use mi_manage_os_memory_ex to manage pre-allocated memory, do I still need to manually call munmap after mi_heap_delete? According to the core dump location in gdb, the core occurs in the mi_arenas_try_purge function. If comment out munmap, no core dump occurs.

  2. When I try to allocate 32MB of memory using mi_heap_malloc(my_heap, 32 * 1024 * 1024), the return value is NULL. Is there a maximum limit for this kind of allocation? If so, what is the maximum value?

  3. can mi_heap_malloc be used concurrently from different threads?

simaocat avatar Mar 19 '25 05:03 simaocat

Hi! I’ll try to resolve your questions as far as my knowledge permits.

Why The Original Code Is Problematic

In the original code:

mi_manage_os_memory_ex(addr, size, /*commit?*/ true, /*allow_large?*/ false,
                       /*is_pinned?*/ false, /*alignment*/ -1,
                       /*own?*/ true, &arena_id);

// ...

mi_heap_delete(my_heap);
munmap(addr, size);

When using own=true, we tell mimalloc that it "owns" (i.e., will eventually unmap) that memory. If we also manually call munmap on the same region, the result can be a double-unmap situation, which is undefined behavior and can lead to a core dump.

Why It Might Not Crash Every Time:

Undefined behavior does not always produce an immediate crash. Sometimes it seems to work—especially in short-lived programs—but it can fail in subtle or unpredictable ways.

How to Fix It:

We can choose one of two approaches:

Option A: Let mimalloc own the region (own=true). We should not call munmap ourselves. Option B: We own the region (own=false). We must call munmap ourselves when finished.

Option A – Mimalloc Owns The Memory (own=true, No munmap)

#include <sys/mman.h>
#include <mimalloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

int main() {
    const size_t size = 1UL << 30; // 1GB
    int protection = PROT_READ | PROT_WRITE;
    int flags = MAP_ANONYMOUS | MAP_SHARED;
    void* addr = mmap(NULL, size, protection, flags, -1, 0);

    if (addr == MAP_FAILED) {
        fprintf(stderr, "Error: mmap failed\n");
        return 1;
    }

    memset(addr, 0, size);
    printf("addr: %p, end:%p\n", addr, (char*)addr + size);

    // Tell mimalloc it OWNS the region (own=true). 
    // => DO NOT call munmap on this region.
    mi_arena_id_t arena_id;
    bool ok = mi_manage_os_memory_ex(addr, size,
                                     /*commit?*/ true,
                                     /*allow_large?*/ false,
                                     /*is_pinned?*/ false,
                                     /*alignment*/ -1,
                                     /*own?*/ true, 
                                     &arena_id);
    if (!ok) {
        fprintf(stderr, "Error: mi_manage_os_memory_ex failed\n");
        return 1;
    }

    // Create a custom heap in that arena
    mi_heap_t* my_heap = mi_heap_new_in_arena(arena_id);
    if (!my_heap) {
        fprintf(stderr, "Error: could not create heap\n");
        return 1;
    }

    // Allocate memory from our custom heap
    void* p1 = mi_heap_malloc(my_heap, 1 * 1024 * 1024); // 1MB
    void* p2 = mi_heap_malloc(my_heap, 1 * 1024 * 1024); // 1MB
    printf("p1=%p, p2=%p\n", p1, p2);

    // Free the allocated blocks
    mi_free(p1);
    mi_free(p2);

    // Delete the heap - mimalloc will unmap the region eventually
    // No manual munmap call is needed (and would cause a double unmap if we did)
    mi_heap_delete(my_heap);

    return 0;
}

Why It Works: own=true makes mimalloc responsible for eventually unmapping that region. We never call munmap ourselves, so there is no double free/unmap.

Option B – We Own The Memory (own=false, Manual munmap)

#include <sys/mman.h>
#include <mimalloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

int main() {
    const size_t size = 1UL << 30; // 1GB
    int protection = PROT_READ | PROT_WRITE;
    int flags = MAP_ANONYMOUS | MAP_SHARED;
    void* addr = mmap(NULL, size, protection, flags, -1, 0);

    if (addr == MAP_FAILED) {
        fprintf(stderr, "Error: mmap failed\n");
        return 1;
    }

    memset(addr, 0, size);
    printf("addr: %p, end:%p\n", addr, (char*)addr + size);

    // Tell mimalloc we (the user) own the region (own=false).
    // => We are responsible for calling munmap.
    mi_arena_id_t arena_id;
    bool ok = mi_manage_os_memory_ex(addr, size,
                                     /*commit?*/ true,
                                     /*allow_large?*/ false,
                                     /*is_pinned?*/ false,
                                     /*alignment*/ -1,
                                     /*own?*/ false, 
                                     &arena_id);
    if (!ok) {
        fprintf(stderr, "Error: mi_manage_os_memory_ex failed\n");
        munmap(addr, size);
        return 1;
    }

    // Create a custom heap in that arena
    mi_heap_t* my_heap = mi_heap_new_in_arena(arena_id);
    if (!my_heap) {
        fprintf(stderr, "Error: could not create heap\n");
        munmap(addr, size);
        return 1;
    }

    // Allocate memory from our custom heap
    void* p1 = mi_heap_malloc(my_heap, 1 * 1024 * 1024); // 1MB
    void* p2 = mi_heap_malloc(my_heap, 1 * 1024 * 1024); // 1MB
    printf("p1=%p, p2=%p\n", p1, p2);

    // Free the allocated blocks
    mi_free(p1);
    mi_free(p2);

    // Delete the heap
    mi_heap_delete(my_heap);

    // Now we can safely unmap, since mimalloc doesn't "own" the region
    munmap(addr, size);

    return 0;
}

Why It Works:

own=false tells mimalloc not to unmap that region. After deleting the heap, you safely call munmap. No double unmap can happen because mimalloc never tries to unmap it in this scenario.

Mimalloc does not impose a strict size limit like “32MB” for a single allocation. The only inherent limit is:

The total memory available in the arena (if you are using a custom arena via mi_manage_os_memory_ex). Overall system memory constraints (commit limits, OS limits, etc.). Therefore, if mi_heap_malloc(my_heap, 32 * 1024 * 1024) returns NULL, it is almost certainly because mimalloc could not fulfill that request from the memory region you provided.

Common Causes for a NULL Return:

Insufficient Free Space in the Pre‐Allocated Region If you already used up or fragmented most of the 1GB region, the contiguous free space might be too small for 32MB.

commit=false and No More Commit Quota

If you are calling mi_manage_os_memory_ex(addr, size, /commit?/false, ...), then mimalloc might attempt to commit pages on demand. If your OS denies that commit (for example, overcommit is disabled, or there is insufficient swap), the allocation can fail. Make sure you either pass commit=true or that you have enough commit allowance left in your system. Large Page Issues (if allow_large=true)

If you are trying to use large pages (for example, with allow_large=true), sometimes the OS may reject allocating large pages if none are available. This can cause a failure on large requests. Misconfiguration or Partial Overlaps

Check that the entire addr region is actually valid (e.g., you didn’t unmap part of it). If you set own=true but also unmap part of the area, mimalloc may see that region as unavailable. Alignment or Overhead

By default, mimalloc can handle big allocations just fine, but if you used certain special alignment parameters (alignment in mi_manage_os_memory_ex), you might be inadvertently restricting how mimalloc can subdivide the memory.

How to Diagnose:

Verify commit=true If you want immediate access to all 1GB, ensure commit=true so the entire region is committed.

Check Free Memory

If you’re doing many smaller allocations before the 32MB request, confirm you haven’t used or fragmented most of the 1GB. A fresh 1GB region with minimal prior allocations should easily accommodate 32MB. Use Debug/Stats in Mimalloc

Mimalloc has debug and statistics options. Try running your program with the environment variable MIMALLOC_VERBOSE=1 or MIMALLOC_STATS=1 to get insight into memory usage and see if it shows an out‐of‐memory or commit failure. Confirm You Didn’t Manually munmap Double‐check you haven’t inadvertently unmapped part of the region or reused it for something else.

Yes, mi_heap_malloc is thread‐safe – multiple threads can simultaneously allocate and free from the same heap without corrupting mimalloc’s internal data structures. However, you can experience more contention and lock overhead when multiple threads share a single heap.

To maximize performance in multi‐threaded applications, the usual recommendation is:

Either let each thread use mimalloc’s default thread‐local heap (i.e., just use the standard mi_malloc/mi_free in each thread). Or if using custom heaps, create one custom heap per thread. This reduces lock contention since each thread performs most allocations from its own heap. You can still share memory between threads, but the creation and destruction of heaps, as well as large allocations, are less serialized.

Therefore, while you can safely call mi_heap_malloc(my_heap, ...) concurrently on a shared heap, it is typically more efficient to give each thread its own heap if you expect a lot of concurrent allocations.

adelcor avatar Mar 19 '25 11:03 adelcor

Hi @adelcor -- thanks for giving extensive responses :-) However, please do not respond if you are not 100% sure as it may cause confusion for others looking later at issues. In particular, mi_heap_malloc is not thread-safe and mimalloc mi_heap_t is always tied to a particular thread. mi_malloc/mi_free is always thread safe, and arena's are shared between threads as well. If you want to always allocate from a single arena, you need to create a heap tied to such arena for each thread! (and that works fine, memory will even be shared between those heaps etc).

In the future we will have multi-threaded heaps as well but it is not there now.

  1. When I try to allocate 32MB of memory using mi_heap_malloc(my_heap, 32 * 1024 * 1024), the return value is NULL. Is there a maximum limit for this kind of allocation? If so, what is the maximum value?

Ah, that is a current limitation of per-arena allocation as generally arena's are only used for up-to 32 MiB allocations -- anything larger is allocated from the OS. Normally that is fine but if you want to specifically allocate always within one arena you will hit this limitation as mi_heap_malloc will return NULL (as it can only allocate from that arena, and the max is 32MiB).

We can lift this limit to allow larger blocks inside one arena but we did not do so yet. These api's are still in flux and feedback is welcome to improve them :-). See also the latest dev3 that has mi_manage_memory that can use custom (de)commit functions and reload entire arena's from disk.

daanx avatar Mar 19 '25 20:03 daanx

Thanks for the clarification @daanx !

I misunderstood the thread safety of mi_heap_malloc. That makes sense. I appreciate the explanation!

I’ll keep that in mind for future contributions. Thanks again for taking the time to explain.

adelcor avatar Mar 20 '25 08:03 adelcor

@daanx Hi, I also hit upon the issue where mi_heap_malloc returns NULL for larger size, although for me it's just 1MB (with v1.9.3). I'm wondering if I can just go ahead and change this to a larger value for my particular need of allocating from an arena. If so, any pointer to the code is appreciated! If not, could you please elaborate on why there's this limit, the downsides of raising it etc.? Thanks!

monkey-sheng avatar Apr 03 '25 23:04 monkey-sheng