diff --git a/Doc/license.rst b/Doc/license.rst index 41e5ce4e3b668d8..f0fc6393eb6b5e2 100644 --- a/Doc/license.rst +++ b/Doc/license.rst @@ -1034,7 +1034,7 @@ mimalloc MIT License:: - Copyright (c) 2018-2021 Microsoft Corporation, Daan Leijen + Copyright (c) 2018-2025 Microsoft Corporation, Daan Leijen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Include/internal/mimalloc/mimalloc-new-delete.h b/Include/internal/mimalloc/mimalloc-new-delete.h new file mode 100644 index 000000000000000..00aa0b306e37478 --- /dev/null +++ b/Include/internal/mimalloc/mimalloc-new-delete.h @@ -0,0 +1,66 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2020 Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MIMALLOC_NEW_DELETE_H +#define MIMALLOC_NEW_DELETE_H + +// ---------------------------------------------------------------------------- +// This header provides convenient overrides for the new and +// delete operations in C++. +// +// This header should be included in only one source file! +// +// On Windows, or when linking dynamically with mimalloc, these +// can be more performant than the standard new-delete operations. +// See +// --------------------------------------------------------------------------- +#if defined(__cplusplus) + #include + #include "mimalloc.h" + + #if defined(_MSC_VER) && defined(_Ret_notnull_) && defined(_Post_writable_byte_size_) + // stay consistent with VCRT definitions + #define mi_decl_new(n) mi_decl_nodiscard mi_decl_restrict _Ret_notnull_ _Post_writable_byte_size_(n) + #define mi_decl_new_nothrow(n) mi_decl_nodiscard mi_decl_restrict _Ret_maybenull_ _Success_(return != NULL) _Post_writable_byte_size_(n) + #else + #define mi_decl_new(n) mi_decl_nodiscard mi_decl_restrict + #define mi_decl_new_nothrow(n) mi_decl_nodiscard mi_decl_restrict + #endif + + void operator delete(void* p) noexcept { mi_free(p); }; + void operator delete[](void* p) noexcept { mi_free(p); }; + + void operator delete (void* p, const std::nothrow_t&) noexcept { mi_free(p); } + void operator delete[](void* p, const std::nothrow_t&) noexcept { mi_free(p); } + + mi_decl_new(n) void* operator new(std::size_t n) noexcept(false) { return mi_new(n); } + mi_decl_new(n) void* operator new[](std::size_t n) noexcept(false) { return mi_new(n); } + + mi_decl_new_nothrow(n) void* operator new (std::size_t n, const std::nothrow_t& tag) noexcept { (void)(tag); return mi_new_nothrow(n); } + mi_decl_new_nothrow(n) void* operator new[](std::size_t n, const std::nothrow_t& tag) noexcept { (void)(tag); return mi_new_nothrow(n); } + + #if (__cplusplus >= 201402L || _MSC_VER >= 1916) + void operator delete (void* p, std::size_t n) noexcept { mi_free_size(p,n); }; + void operator delete[](void* p, std::size_t n) noexcept { mi_free_size(p,n); }; + #endif + + #if (__cplusplus > 201402L && defined(__cpp_aligned_new)) + void operator delete (void* p, std::align_val_t al) noexcept { mi_free_aligned(p, static_cast(al)); } + void operator delete[](void* p, std::align_val_t al) noexcept { mi_free_aligned(p, static_cast(al)); } + void operator delete (void* p, std::size_t n, std::align_val_t al) noexcept { mi_free_size_aligned(p, n, static_cast(al)); }; + void operator delete[](void* p, std::size_t n, std::align_val_t al) noexcept { mi_free_size_aligned(p, n, static_cast(al)); }; + void operator delete (void* p, std::align_val_t al, const std::nothrow_t&) noexcept { mi_free_aligned(p, static_cast(al)); } + void operator delete[](void* p, std::align_val_t al, const std::nothrow_t&) noexcept { mi_free_aligned(p, static_cast(al)); } + + void* operator new (std::size_t n, std::align_val_t al) noexcept(false) { return mi_new_aligned(n, static_cast(al)); } + void* operator new[](std::size_t n, std::align_val_t al) noexcept(false) { return mi_new_aligned(n, static_cast(al)); } + void* operator new (std::size_t n, std::align_val_t al, const std::nothrow_t&) noexcept { return mi_new_aligned_nothrow(n, static_cast(al)); } + void* operator new[](std::size_t n, std::align_val_t al, const std::nothrow_t&) noexcept { return mi_new_aligned_nothrow(n, static_cast(al)); } + #endif +#endif + +#endif // MIMALLOC_NEW_DELETE_H diff --git a/Include/internal/mimalloc/mimalloc-override.h b/Include/internal/mimalloc/mimalloc-override.h new file mode 100644 index 000000000000000..c34093fe00dde6b --- /dev/null +++ b/Include/internal/mimalloc/mimalloc-override.h @@ -0,0 +1,68 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2020 Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MIMALLOC_OVERRIDE_H +#define MIMALLOC_OVERRIDE_H + +/* ---------------------------------------------------------------------------- +This header can be used to statically redirect malloc/free and new/delete +to the mimalloc variants. This can be useful if one can include this file on +each source file in a project (but be careful when using external code to +not accidentally mix pointers from different allocators). +-----------------------------------------------------------------------------*/ + +#include "mimalloc.h" + +// Standard C allocation +#define malloc(n) mi_malloc(n) +#define calloc(c,n) mi_calloc(c,n) +#define realloc(p,n) mi_realloc(p,n) +#define free(p) mi_free(p) + +#define strdup(s) mi_strdup(s) +#define strndup(s,n) mi_strndup(s,n) +#define realpath(f,n) mi_realpath(f,n) + +// Microsoft extensions +#define _expand(p,n) mi_expand(p,n) +#define _msize(p) mi_usable_size(p) +#define _recalloc(p,c,n) mi_recalloc(p,c,n) + +#define _strdup(s) mi_strdup(s) +#define _strndup(s,n) mi_strndup(s,n) +#define _wcsdup(s) mi_wcsdup(s) +#define _mbsdup(s) mi_mbsdup(s) +#define _dupenv_s(buf,n,nm) mi_dupenv_s(buf,n,nm) +#define _wdupenv_s(buf,n,nm) mi_wdupenv_s(buf,n,nm) + +// Various Posix and Unix variants +#define reallocf(p,n) mi_reallocf(p,n) +#define malloc_size(p) mi_usable_size(p) +#define malloc_usable_size(p) mi_usable_size(p) +#define malloc_good_size(n) mi_malloc_good_size(n) +#define cfree(p) mi_free(p) + +#define valloc(n) mi_valloc(n) +#define pvalloc(n) mi_pvalloc(n) +#define reallocarray(p,c,n) mi_reallocarray(p,c,n) +#define reallocarr(ptrp,c,n) mi_reallocarr(ptrp,c,n) +#define memalign(a,n) mi_memalign(a,n) +#define aligned_alloc(a,n) mi_aligned_alloc(a,n) +#define posix_memalign(p,a,n) mi_posix_memalign(p,a,n) +#define _posix_memalign(p,a,n) mi_posix_memalign(p,a,n) + +// Microsoft aligned variants +#define _aligned_malloc(n,a) mi_malloc_aligned(n,a) +#define _aligned_realloc(p,n,a) mi_realloc_aligned(p,n,a) +#define _aligned_recalloc(p,c,n,a) mi_aligned_recalloc(p,c,n,a) +#define _aligned_msize(p,a,o) mi_usable_size(p) +#define _aligned_free(p) mi_free(p) +#define _aligned_offset_malloc(n,a,o) mi_malloc_aligned_at(n,a,o) +#define _aligned_offset_realloc(p,n,a,o) mi_realloc_aligned_at(p,n,a,o) +#define _aligned_offset_recalloc(p,c,n,a,o) mi_recalloc_aligned_at(p,c,n,a,o) + +#endif // MIMALLOC_OVERRIDE_H diff --git a/Include/internal/mimalloc/mimalloc-profile.h b/Include/internal/mimalloc/mimalloc-profile.h new file mode 100644 index 000000000000000..eea3a946afb984c --- /dev/null +++ b/Include/internal/mimalloc/mimalloc-profile.h @@ -0,0 +1,97 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2024-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MIMALLOC_PROFILE_H +#define MIMALLOC_PROFILE_H + +#include "mimalloc.h" +#include // bool +#include // uint64_t + +//--------------------------------------------------------------------------- +// Initial support for profiling hooks; still experimental +//--------------------------------------------------------------------------- + +#define MI_PROFILE_SAMPLE_DATA_MAX_SIZE (1024) + +// User defined profiler; see `test_profile.c` for an example. +typedef struct mi_profiler_s mi_profiler_t; + +// Profiler data is stored together with each sampled allocation (unless the `on_free` field in the profiler is NULL.) +typedef struct mi_profiler_sample_data_s { + size_t user_data_size; // size of the user_data (should be the `mi_profiler_t.sample_data_size`) + void* user_data[1]; // default, but can be less or more (up to 1KiB), depending on `sample_data_size` +} mi_profiler_sample_data_t; + +// Profiling callback invoked on each sampled allocation. +// If `profiler_data!=NULL` (i.e. when `on_free` is not NULL), then `profiler_data->requested_size == requested_size`. +typedef size_t (mi_cdecl mi_profiler_on_alloc_fun)(mi_profiler_t* profiler, mi_profiler_sample_data_t* profiler_data, void* ptr, size_t requested_size, size_t bytes_sample_rate, uint64_t bytes_since_last_sample, mi_heap_t* heap); + +// Profiling callback invoked on each sampled in-place re-allocation. +typedef size_t (mi_cdecl mi_profiler_on_realloc_inplace_fun)(mi_profiler_t* profiler, mi_profiler_sample_data_t* profiler_data, void* ptr, size_t old_size, mi_heap_t* heap); + +// Profiling callback invoked on a previously sampled allocation. +typedef void (mi_cdecl mi_profiler_on_free_fun)(mi_profiler_t* profiler, mi_profiler_sample_data_t* profiler_data, void* ptr, mi_heap_t* heap); + +// Profiling callback invoked on mi_profiler_snapshot() +typedef void (mi_cdecl mi_profiler_on_snapshot_fun)(mi_profiler_t* profiler); + +// A profiler +// All fields are considered immutable such that they can be copied and accessed concurrently. All fields can be NULL/0. +struct mi_profiler_s { + void* reserved; // opaque; reserved by mimalloc + size_t sample_data_size; // size of required profiler data for each sampled allocation (or zero for no data) + size_t initial_sample_rate;// initial sample rate in bytes (set to at least 1 or higher) (can be adjusted by `on_alloc`) + mi_profiler_on_alloc_fun* on_alloc; // called on a sampled allocation (may be called concurrently) + mi_profiler_on_free_fun* on_free; // called on when previous sampled allocation is freed (may be called concurrently) + mi_profiler_on_realloc_inplace_fun* on_realloc_inplace; // (as yet unused) called on in-place reallocation of a previous sampled allocation (may be called concurrently) + mi_profiler_on_snapshot_fun* on_snapshot; // called on mi_profiler_snapshot() (only one thread at a time) + // ... more user fields allowed +}; + +// Exported definitions +#ifdef __cplusplus +extern "C" { +#endif + +// attach a profiler to a particular heap only. +mi_decl_export bool mi_heap_profile(mi_heap_t* heap, mi_profiler_t* profiler); + +// disable profiling for a particular heap; useful for a heap that the profiler uses itself for metadata. +mi_decl_export void mi_heap_profile_disable(mi_heap_t* heap); + +// attach a profiler to any (current and future) heaps in a sub-process (unless those heaps disabled profiling) +mi_decl_export bool mi_subproc_profile(mi_subproc_id_t subproc_id, mi_profiler_t* profiler); + +// attach a profiler to any heaps in the main sub-process (use NULL to detach) +mi_decl_export bool mi_profile(mi_profiler_t* profiler); + +// start sampling +mi_decl_export bool mi_profiler_start(mi_profiler_t* profiler); + +// end sampling +mi_decl_export bool mi_profiler_stop(mi_profiler_t* profiler); + +// snapshot the current profile (may be called concurrently but only one thread will actually call the snapshot callback) +mi_decl_export void mi_profiler_snapshot(mi_profiler_t* profiler); + +// builtin pprof compatible profiler +// the builtin profiler can also be activated using environment variables (see [`pprof.c`](/src/profile/pprof.c)) +// `base_file_name` selects the text snapshot format if it ends in `.heap` or `.text` (the extension is +// stripped); any other extension, or none, uses the default (protobuf) snapshot format. +// `alloc_interval_size`, if >0, automatically snapshots every that many allocated bytes. +// `inuse_interval_size`, if >0, automatically snapshots every time the in-use bytes grow by that many bytes. +// `time_interval_secs`, if >0, automatically snapshots every that many seconds (checked from allocation events). +mi_decl_export mi_profiler_t* mi_pprof_profiler_new(size_t initial_threshold, const char* base_file_name, size_t alloc_interval_size, size_t inuse_interval_size, size_t time_interval_secs); +mi_decl_export void mi_pprof_profiler_delete(mi_profiler_t* profiler); + + +#ifdef __cplusplus +} +#endif + +#endif // MIMALLOC_PROFILE_H diff --git a/Include/internal/mimalloc/mimalloc-stats.h b/Include/internal/mimalloc/mimalloc-stats.h new file mode 100644 index 000000000000000..85198984aed127e --- /dev/null +++ b/Include/internal/mimalloc/mimalloc-stats.h @@ -0,0 +1,188 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2024-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MIMALLOC_STATS_H +#define MIMALLOC_STATS_H + +#include "mimalloc.h" +#include // memset +#include // int64_t + +#define MI_STAT_VERSION 6 // increased on every backward incompatible change + +// alignment for atomic fields +#if defined(_MSC_VER) +#define mi_decl_align(a) __declspec(align(a)) +#elif defined(__GNUC__) +#define mi_decl_align(a) __attribute__((aligned(a))) +#elif __cplusplus >= 201103L +#define mi_decl_align(a) alignas(a) +#else +#define mi_decl_align(a) _Alignas(a) +#endif + + +// count allocation over time +typedef struct mi_stat_count_s { + int64_t total; // total allocated + int64_t peak; // peak allocation + int64_t current; // current allocation +} mi_stat_count_t; + +// counters only increase +typedef struct mi_stat_counter_s { + int64_t total; // total count +} mi_stat_counter_t; + +#define MI_STAT_FIELDS() \ + MI_STAT_COUNT(pages) /* count of mimalloc pages */ \ + MI_STAT_COUNT(reserved) /* reserved memory bytes */ \ + MI_STAT_COUNT(committed) /* committed bytes */ \ + MI_STAT_COUNTER(reset) /* reset bytes */ \ + MI_STAT_COUNTER(purged) /* purged bytes */ \ + MI_STAT_COUNT(page_committed) /* committed memory inside pages */ \ + MI_STAT_COUNT(pages_abandoned) /* abandoned pages count */ \ + MI_STAT_COUNT(threads) /* number of threads */ \ + MI_STAT_COUNT(allocated) /* allocated bytes */ \ + MI_STAT_COUNTER(malloc_count) /* total number of malloc calls */ \ + MI_STAT_COUNTER(free_count) /* total number of free calls */ \ + MI_STAT_COUNTER(freed) /* total free'd bytes (note: total allocated is `allocated.total`) */ \ + MI_STAT_COUNTER(malloc_requested) /* malloc requested bytes */ \ + \ + MI_STAT_COUNTER(mmap_calls) \ + MI_STAT_COUNTER(commit_calls) \ + MI_STAT_COUNTER(reset_calls) \ + MI_STAT_COUNTER(purge_calls) \ + MI_STAT_COUNTER(arena_count) /* number of memory arena's */ \ + MI_STAT_COUNTER(malloc_guarded_count) /* number of allocations with guard pages */ \ + \ + /* internal statistics */ \ + MI_STAT_COUNTER(arena_rollback_count) \ + MI_STAT_COUNTER(arena_purges) \ + MI_STAT_COUNTER(pages_extended) /* number of page extensions */ \ + MI_STAT_COUNTER(pages_retire) /* number of pages that are retired */ \ + MI_STAT_COUNTER(page_searches) /* total pages searched for a fresh page */ \ + MI_STAT_COUNTER(page_searches_count) /* searched count for a fresh page */ \ + /* only on v1 and v2 */ \ + MI_STAT_COUNT(segments) \ + MI_STAT_COUNT(segments_abandoned) \ + MI_STAT_COUNT(segments_cache) \ + MI_STAT_COUNT(_segments_reserved) \ + /* only on v3 */ \ + MI_STAT_COUNT(heaps) \ + MI_STAT_COUNT(theaps) \ + MI_STAT_COUNT(pages_os_abandoned) /* pages in the os abandoned list (bad) */ \ + MI_STAT_COUNT(pages_os_allocated) /* pages allocated outside arenas (bad) */ \ + MI_STAT_COUNTER(pages_reclaim_on_alloc) \ + MI_STAT_COUNTER(pages_reclaim_on_free) \ + MI_STAT_COUNTER(pages_reabandon_full) \ + MI_STAT_COUNTER(pages_unabandon_busy_wait) \ + MI_STAT_COUNTER(pages_unown_cas_retry) \ + MI_STAT_COUNTER(heaps_delete_wait) \ + MI_STAT_COUNTER(pages_stat_updates) /* calls to successful page_stat_update */ \ + MI_STAT_COUNTER(pages_stat_update_count) /* total free/allocs */ \ + MI_STAT_COUNTER(profile_samples) /* total sampled profiled allocations */ \ + /* deprecated */ \ + MI_STAT_COUNT(malloc_normal) /* allocated bytes <= MI_LARGE_OBJ_SIZE_MAX */ \ + MI_STAT_COUNT(malloc_huge) /* allocated bytes in huge pages */ \ + MI_STAT_COUNTER(malloc_normal_count) /* number of blocks <= MI_LARGE_OBJ_SIZE_MAX */ \ + MI_STAT_COUNTER(malloc_huge_count) /* number of huge bloks */ \ + + +// Size bins for chunks +typedef enum mi_chunkbin_e { + MI_CBIN_SMALL, // slice_count == 1 + MI_CBIN_OTHER, // slice_count: any other from the other bins, and 1 <= slice_count <= MI_BCHUNK_BITS + MI_CBIN_MEDIUM, // slice_count == 8 + MI_CBIN_LARGE, // slice_count == MI_SIZE_BITS (only used if MI_ENABLE_LARGE_PAGES is 1) + MI_CBIN_HUGE, // slice_count > MI_BCHUNK_BITS + MI_CBIN_NONE, // no bin assigned yet (the chunk is completely free) + MI_CBIN_COUNT +} mi_chunkbin_t; + + +// Define the statistics structure +#define MI_BIN_HUGE (73U) // see types.h +#define MI_STAT_COUNT(stat) mi_stat_count_t stat; +#define MI_STAT_COUNTER(stat) mi_stat_counter_t stat; + +typedef struct mi_stats_s +{ + size_t size; // size of the mi_stats_t structure + size_t version; + + mi_decl_align(8) MI_STAT_FIELDS() + + // future extension + mi_stat_count_t _stat_reserved[4]; + mi_stat_counter_t _stat_counter_reserved[4]; + + // size segregated statistics + mi_stat_count_t malloc_bins[MI_BIN_HUGE+1]; // allocation per size bin + mi_stat_count_t page_bins[MI_BIN_HUGE+1]; // pages allocated per size bin + mi_stat_count_t chunk_bins[MI_CBIN_COUNT]; // chunks per page sizes +} mi_stats_t; + +#undef MI_STAT_COUNT +#undef MI_STAT_COUNTER + +// Initialization +static inline void mi_stats_header_init(mi_stats_t* stats) { + stats->size = sizeof(*stats); + stats->version = MI_STAT_VERSION; +} +static inline void mi_stats_init(mi_stats_t* stats) { + memset(stats,0,sizeof(*stats)); + mi_stats_header_init(stats); +} + +// Always use this to declare and initialize a `mi_stats_t` variable such +// that the version and size can be checked at runtime. +#define mi_stats_t_decl(name) mi_stats_t name; mi_stats_init(&name); + +// Exported definitions +#ifdef __cplusplus +extern "C" { +#endif + +// stats from a heap +mi_decl_export bool mi_heap_stats_get(mi_heap_t* heap, mi_stats_t* stats) mi_attr_noexcept; +mi_decl_export char* mi_heap_stats_get_json(mi_heap_t* heap, size_t buf_size, char* buf) mi_attr_noexcept; // use mi_free to free the result if the input buf == NULL +mi_decl_export void mi_heap_stats_print_out(mi_heap_t* heap, mi_output_fun* out, void* arg) mi_attr_noexcept; + +// stats from a subprocess and its heaps aggregated +mi_decl_export bool mi_subproc_stats_get(mi_subproc_id_t subproc_id, mi_stats_t* stats) mi_attr_noexcept; +mi_decl_export char* mi_subproc_stats_get_json(mi_subproc_id_t subproc_id, size_t buf_size, char* buf) mi_attr_noexcept; // use mi_free to free the result if the input buf == NULL +mi_decl_export void mi_subproc_stats_print_out(mi_subproc_id_t subproc_id, mi_output_fun* out, void* arg) mi_attr_noexcept; +// print subprocess and all its heap stats segregated +mi_decl_export void mi_subproc_heap_stats_print_out(mi_subproc_id_t subproc_id, mi_output_fun* out, void* arg) mi_attr_noexcept; + +// stats aggregated for the current subprocess and all its heaps. +mi_decl_export bool mi_stats_get(mi_stats_t* stats) mi_attr_noexcept; +mi_decl_export char* mi_stats_get_json(size_t buf_size, char* buf) mi_attr_noexcept; // use mi_free to free the result if the input buf == NULL +mi_decl_export void mi_stats_print_out(mi_output_fun* out, void* arg) mi_attr_noexcept; + +// add the stats of the heap to the subprocess and clear the heap stats +mi_decl_export void mi_heap_stats_merge_to_subproc(mi_heap_t* heap); + +// stats from the subprocess without aggregating its heaps +mi_decl_export bool mi_subproc_stats_get_exclusive(mi_subproc_id_t subproc_id, mi_stats_t* stats) mi_attr_noexcept; + +mi_decl_export char* mi_stats_as_json(mi_stats_t* stats, size_t buf_size, char* buf) mi_attr_noexcept; // use mi_free to free the result if the input buf == NULL +mi_decl_export size_t mi_stats_get_bin_size(size_t bin) mi_attr_noexcept; + +// get theap stats (only thread safe on theaps belonging to the calling thread) +mi_decl_export bool mi_theap_stats_get(mi_theap_t* theap, mi_stats_t* stats) mi_attr_noexcept; + +// add the theap stats to the stats of the parent heap and clear the theap stats +mi_decl_export void mi_theap_stats_merge_to_heap(mi_theap_t* theap) mi_attr_noexcept; + +#ifdef __cplusplus +} +#endif + +#endif // MIMALLOC_STATS_H diff --git a/Include/internal/mimalloc/mimalloc.h b/Include/internal/mimalloc/mimalloc.h index 821129e7690b1b2..9d4609e7b61a9ac 100644 --- a/Include/internal/mimalloc/mimalloc.h +++ b/Include/internal/mimalloc/mimalloc.h @@ -1,5 +1,5 @@ /* ---------------------------------------------------------------------------- -Copyright (c) 2018-2023, Microsoft Research, Daan Leijen +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen This is free software; you can redistribute it and/or modify it under the terms of the MIT license. A copy of the license can be found in the file "LICENSE" at the root of this distribution. @@ -8,7 +8,7 @@ terms of the MIT license. A copy of the license can be found in the file #ifndef MIMALLOC_H #define MIMALLOC_H -#define MI_MALLOC_VERSION 212 // major + 2 digits minor +#define MI_MALLOC_VERSION 30600 // major + 2 digits minor + 2 digits patch // ------------------------------------------------------ // Compiler specific attributes @@ -95,9 +95,9 @@ terms of the MIT license. A copy of the license can be found in the file // Includes // ------------------------------------------------------ -#include // size_t +#include // size_t, wchar_t #include // bool -#include // INTPTR_MAX +#include // inline assertions #ifdef __cplusplus extern "C" { @@ -115,13 +115,13 @@ mi_decl_export void* mi_expand(void* p, size_t newsize) mi_decl_export void mi_free(void* p) mi_attr_noexcept; mi_decl_nodiscard mi_decl_export mi_decl_restrict char* mi_strdup(const char* s) mi_attr_noexcept mi_attr_malloc; mi_decl_nodiscard mi_decl_export mi_decl_restrict char* mi_strndup(const char* s, size_t n) mi_attr_noexcept mi_attr_malloc; -mi_decl_nodiscard mi_decl_export mi_decl_restrict char* mi_realpath(const char* fname, char* resolved_name) mi_attr_noexcept mi_attr_malloc; +mi_decl_nodiscard mi_decl_export mi_decl_restrict char* mi_realpath(const char* fname, char* resolved_name) mi_attr_noexcept; // ------------------------------------------------------ -// Extended functionality +// Extended allocation functions // ------------------------------------------------------ -#define MI_SMALL_WSIZE_MAX (128) -#define MI_SMALL_SIZE_MAX (MI_SMALL_WSIZE_MAX*sizeof(void*)) +#define MI_SMALL_WSIZE_MAX 128 +#define MI_SMALL_SIZE_MAX (MI_SMALL_WSIZE_MAX*sizeof(size_t)) // mimalloc considers `sizeof(size_t)` as the machine word size mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_malloc_small(size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1); mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_zalloc_small(size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1); @@ -134,9 +134,59 @@ mi_decl_nodiscard mi_decl_export void* mi_reallocf(void* p, size_t newsize) mi_decl_nodiscard mi_decl_export size_t mi_usable_size(const void* p) mi_attr_noexcept; mi_decl_nodiscard mi_decl_export size_t mi_good_size(size_t size) mi_attr_noexcept; +// `mi_free_size` can be more efficient (as it calls mi_free_small internally). +mi_decl_export void mi_free_size(void* p, size_t size) mi_attr_noexcept; + +// `mi_free_small` is for special applications like language runtimes. +// it should only be used to free objects from `mi_*alloc_small` and is potentially a tiny bit faster than `mi_free` +mi_decl_export void mi_free_small(void* p) mi_attr_noexcept; + +// As `mi_free_small` but `p` should not be a NULL pointer. +mi_decl_export void mi_free_small_nonnull(void* p) mi_attr_noexcept; + +// For runtime's: return the current owner thread-id of the page containing an allocation. +mi_decl_nodiscard mi_decl_export size_t mi_thread_id_of(const void* p) mi_attr_noexcept; + + +// ------------------------------------------------------------------------------------- +// Aligned allocation +// Note that `alignment` always follows `size` for consistency with unaligned +// allocation, but unfortunately this differs from `posix_memalign` and `aligned_alloc`. +// ------------------------------------------------------------------------------------- + +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_malloc_aligned(size_t size, size_t alignment) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1) mi_attr_alloc_align(2); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_malloc_aligned_at(size_t size, size_t alignment, size_t offset) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_zalloc_aligned(size_t size, size_t alignment) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1) mi_attr_alloc_align(2); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_zalloc_aligned_at(size_t size, size_t alignment, size_t offset) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_calloc_aligned(size_t count, size_t size, size_t alignment) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size2(1, 2) mi_attr_alloc_align(3); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_calloc_aligned_at(size_t count, size_t size, size_t alignment, size_t offset) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size2(1, 2); +mi_decl_nodiscard mi_decl_export void* mi_realloc_aligned(void* p, size_t newsize, size_t alignment) mi_attr_noexcept mi_attr_alloc_size(2) mi_attr_alloc_align(3); +mi_decl_nodiscard mi_decl_export void* mi_realloc_aligned_at(void* p, size_t newsize, size_t alignment, size_t offset) mi_attr_noexcept mi_attr_alloc_size(2); + + +// ------------------------------------------------------ +// Typed allocation, the type is always the first parameter +// ------------------------------------------------------ + +#define mi_malloc_tp(tp) ((tp*)mi_malloc_csize(sizeof(tp))) +#define mi_zalloc_tp(tp) ((tp*)mi_zalloc_csize(sizeof(tp))) +#define mi_calloc_tp(tp,n) ((tp*)mi_calloc(n,sizeof(tp))) +#define mi_mallocn_tp(tp,n) ((tp*)mi_mallocn(n,sizeof(tp))) +#define mi_reallocn_tp(tp,p,n) ((tp*)mi_reallocn(p,n,sizeof(tp))) +#define mi_recalloc_tp(tp,p,n) ((tp*)mi_recalloc(p,n,sizeof(tp))) +#define mi_free_tp(tp,p) (mi_free_csize(p,sizeof(tp))) + +#define mi_heap_malloc_tp(tp,hp) ((tp*)mi_heap_malloc(hp,sizeof(tp))) +#define mi_heap_zalloc_tp(tp,hp) ((tp*)mi_heap_zalloc(hp,sizeof(tp))) +#define mi_heap_calloc_tp(tp,hp,n) ((tp*)mi_heap_calloc(hp,n,sizeof(tp))) +#define mi_heap_mallocn_tp(tp,hp,n) ((tp*)mi_heap_mallocn(hp,n,sizeof(tp))) +#define mi_heap_reallocn_tp(tp,hp,p,n) ((tp*)mi_heap_reallocn(hp,p,n,sizeof(tp))) +#define mi_heap_recalloc_tp(tp,hp,p,n) ((tp*)mi_heap_recalloc(hp,p,n,sizeof(tp))) + // ------------------------------------------------------ // Internals +// See also `mimalloc-stats.h` for statistics // ------------------------------------------------------ typedef void (mi_cdecl mi_deferred_free_fun)(bool force, unsigned long long heartbeat, void* arg); @@ -148,66 +198,77 @@ mi_decl_export void mi_register_output(mi_output_fun* out, void* arg) mi_attr_no typedef void (mi_cdecl mi_error_fun)(int err, void* arg); mi_decl_export void mi_register_error(mi_error_fun* fun, void* arg); -mi_decl_export void mi_collect(bool force) mi_attr_noexcept; -mi_decl_export int mi_version(void) mi_attr_noexcept; -mi_decl_export void mi_stats_reset(void) mi_attr_noexcept; -mi_decl_export void mi_stats_merge(void) mi_attr_noexcept; -mi_decl_export void mi_stats_print(void* out) mi_attr_noexcept; // backward compatibility: `out` is ignored and should be NULL -mi_decl_export void mi_stats_print_out(mi_output_fun* out, void* arg) mi_attr_noexcept; +mi_decl_export void mi_collect(bool force) mi_attr_noexcept; +mi_decl_export int mi_version(void); +mi_decl_export void mi_options_print(void) mi_attr_noexcept; +mi_decl_export void mi_process_info_print(void) mi_attr_noexcept; +mi_decl_export void mi_options_print_out(mi_output_fun* out, void* arg) mi_attr_noexcept; +mi_decl_export void mi_process_info_print_out(mi_output_fun* out, void* arg) mi_attr_noexcept; +mi_decl_export void mi_process_info(size_t* elapsed_msecs, size_t* user_msecs, size_t* system_msecs, + size_t* current_rss, size_t* peak_rss, + size_t* current_commit, size_t* peak_commit, size_t* page_faults) mi_attr_noexcept; + + +// Generally do not use the following as these are usually called automatically mi_decl_export void mi_process_init(void) mi_attr_noexcept; +mi_decl_export void mi_cdecl mi_process_done(void) mi_attr_noexcept; mi_decl_export void mi_thread_init(void) mi_attr_noexcept; mi_decl_export void mi_thread_done(void) mi_attr_noexcept; -mi_decl_export void mi_thread_stats_print_out(mi_output_fun* out, void* arg) mi_attr_noexcept; +mi_decl_export void mi_thread_set_in_threadpool(void) mi_attr_noexcept; // communicate that a thread is in a threadpool -mi_decl_export void mi_process_info(size_t* elapsed_msecs, size_t* user_msecs, size_t* system_msecs, - size_t* current_rss, size_t* peak_rss, - size_t* current_commit, size_t* peak_commit, size_t* page_faults) mi_attr_noexcept; -// ------------------------------------------------------------------------------------- -// Aligned allocation -// Note that `alignment` always follows `size` for consistency with unaligned -// allocation, but unfortunately this differs from `posix_memalign` and `aligned_alloc`. -// ------------------------------------------------------------------------------------- +// ----------------------------------------------------------------- +// Return allocated block size (if the return value is not NULL) +// ----------------------------------------------------------------- -mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_malloc_aligned(size_t size, size_t alignment) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1) mi_attr_alloc_align(2); -mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_malloc_aligned_at(size_t size, size_t alignment, size_t offset) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1); -mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_zalloc_aligned(size_t size, size_t alignment) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1) mi_attr_alloc_align(2); -mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_zalloc_aligned_at(size_t size, size_t alignment, size_t offset) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1); -mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_calloc_aligned(size_t count, size_t size, size_t alignment) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size2(1,2) mi_attr_alloc_align(3); -mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_calloc_aligned_at(size_t count, size_t size, size_t alignment, size_t offset) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size2(1,2); -mi_decl_nodiscard mi_decl_export void* mi_realloc_aligned(void* p, size_t newsize, size_t alignment) mi_attr_noexcept mi_attr_alloc_size(2) mi_attr_alloc_align(3); -mi_decl_nodiscard mi_decl_export void* mi_realloc_aligned_at(void* p, size_t newsize, size_t alignment, size_t offset) mi_attr_noexcept mi_attr_alloc_size(2); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_umalloc(size_t size, size_t* block_size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_ucalloc(size_t count, size_t size, size_t* block_size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size2(1,2); +mi_decl_nodiscard mi_decl_export void* mi_urealloc(void* p, size_t newsize, size_t* block_size_pre, size_t* block_size_post) mi_attr_noexcept mi_attr_alloc_size(2); +mi_decl_export void mi_ufree(void* p, size_t* block_size) mi_attr_noexcept; + +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_umalloc_aligned(size_t size, size_t alignment, size_t* block_size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1) mi_attr_alloc_align(2); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_uzalloc_aligned(size_t size, size_t alignment, size_t* block_size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1) mi_attr_alloc_align(2); + +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_umalloc_small(size_t size, size_t* block_size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_uzalloc_small(size_t size, size_t* block_size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1); // ------------------------------------------------------------------------------------- -// Heaps: first-class, but can only allocate from the same thread that created it. +// Heaps: first-class. Can allocate from any thread (and be free'd from any thread) +// Heaps keep allocations in separate pages from each other (but share the arena's and free'd pages) // ------------------------------------------------------------------------------------- struct mi_heap_s; typedef struct mi_heap_s mi_heap_t; mi_decl_nodiscard mi_decl_export mi_heap_t* mi_heap_new(void); -mi_decl_export void mi_heap_delete(mi_heap_t* heap); -mi_decl_export void mi_heap_destroy(mi_heap_t* heap); -mi_decl_export mi_heap_t* mi_heap_set_default(mi_heap_t* heap); -mi_decl_export mi_heap_t* mi_heap_get_default(void); -mi_decl_export mi_heap_t* mi_heap_get_backing(void); -mi_decl_export void mi_heap_collect(mi_heap_t* heap, bool force) mi_attr_noexcept; - -mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_heap_malloc(mi_heap_t* heap, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2); -mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_heap_zalloc(mi_heap_t* heap, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2); -mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_heap_calloc(mi_heap_t* heap, size_t count, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size2(2, 3); + +mi_decl_export void mi_heap_delete(mi_heap_t* heap); // move live blocks to the main heap +mi_decl_export void mi_heap_destroy(mi_heap_t* heap); // free all live blocks +mi_decl_export void mi_heap_set_numa_affinity(mi_heap_t* heap, int numa_node); +mi_decl_export void mi_heap_set_debug_fill_preserve(mi_heap_t* heap, size_t preserve); // in debug mode, do not fill the first `preserve` bytes of blocks on allocation or free +mi_decl_export void mi_heap_collect(mi_heap_t* heap, bool force); + +mi_decl_nodiscard mi_decl_export mi_heap_t* mi_heap_main(void); +mi_decl_nodiscard mi_decl_export mi_heap_t* mi_heap_of(const void* p); +mi_decl_nodiscard mi_decl_export bool mi_heap_contains(const mi_heap_t* heap, const void* p); +mi_decl_nodiscard mi_decl_export bool mi_any_heap_contains(const void* p); + +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_heap_malloc(mi_heap_t* theap, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_heap_zalloc(mi_heap_t* heap, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_heap_calloc(mi_heap_t* heap, size_t count, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size2(2, 3); mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_heap_mallocn(mi_heap_t* heap, size_t count, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size2(2, 3); mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_heap_malloc_small(mi_heap_t* heap, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_heap_zalloc_small(mi_heap_t* heap, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2); mi_decl_nodiscard mi_decl_export void* mi_heap_realloc(mi_heap_t* heap, void* p, size_t newsize) mi_attr_noexcept mi_attr_alloc_size(3); -mi_decl_nodiscard mi_decl_export void* mi_heap_reallocn(mi_heap_t* heap, void* p, size_t count, size_t size) mi_attr_noexcept mi_attr_alloc_size2(3,4); -mi_decl_nodiscard mi_decl_export void* mi_heap_reallocf(mi_heap_t* heap, void* p, size_t newsize) mi_attr_noexcept mi_attr_alloc_size(3); +mi_decl_nodiscard mi_decl_export void* mi_heap_reallocn(mi_heap_t* heap, void* p, size_t count, size_t size) mi_attr_noexcept mi_attr_alloc_size2(3, 4); +mi_decl_nodiscard mi_decl_export void* mi_heap_reallocf(mi_heap_t* theap, void* p, size_t newsize) mi_attr_noexcept mi_attr_alloc_size(3); mi_decl_nodiscard mi_decl_export mi_decl_restrict char* mi_heap_strdup(mi_heap_t* heap, const char* s) mi_attr_noexcept mi_attr_malloc; mi_decl_nodiscard mi_decl_export mi_decl_restrict char* mi_heap_strndup(mi_heap_t* heap, const char* s, size_t n) mi_attr_noexcept mi_attr_malloc; -mi_decl_nodiscard mi_decl_export mi_decl_restrict char* mi_heap_realpath(mi_heap_t* heap, const char* fname, char* resolved_name) mi_attr_noexcept mi_attr_malloc; +mi_decl_nodiscard mi_decl_export mi_decl_restrict char* mi_heap_realpath(mi_heap_t* heap, const char* fname, char* resolved_name) mi_attr_noexcept; mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_heap_malloc_aligned(mi_heap_t* heap, size_t size, size_t alignment) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2) mi_attr_alloc_align(3); mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_heap_malloc_aligned_at(mi_heap_t* heap, size_t size, size_t alignment, size_t offset) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2); @@ -235,81 +296,163 @@ mi_decl_nodiscard mi_decl_export void* mi_recalloc_aligned(void* p, size_t newco mi_decl_nodiscard mi_decl_export void* mi_recalloc_aligned_at(void* p, size_t newcount, size_t size, size_t alignment, size_t offset) mi_attr_noexcept mi_attr_alloc_size2(2,3); mi_decl_nodiscard mi_decl_export void* mi_heap_rezalloc(mi_heap_t* heap, void* p, size_t newsize) mi_attr_noexcept mi_attr_alloc_size(3); -mi_decl_nodiscard mi_decl_export void* mi_heap_recalloc(mi_heap_t* heap, void* p, size_t newcount, size_t size) mi_attr_noexcept mi_attr_alloc_size2(3,4); +mi_decl_nodiscard mi_decl_export void* mi_heap_recalloc(mi_heap_t* heap, void* p, size_t newcount, size_t size) mi_attr_noexcept mi_attr_alloc_size2(3, 4); mi_decl_nodiscard mi_decl_export void* mi_heap_rezalloc_aligned(mi_heap_t* heap, void* p, size_t newsize, size_t alignment) mi_attr_noexcept mi_attr_alloc_size(3) mi_attr_alloc_align(4); mi_decl_nodiscard mi_decl_export void* mi_heap_rezalloc_aligned_at(mi_heap_t* heap, void* p, size_t newsize, size_t alignment, size_t offset) mi_attr_noexcept mi_attr_alloc_size(3); -mi_decl_nodiscard mi_decl_export void* mi_heap_recalloc_aligned(mi_heap_t* heap, void* p, size_t newcount, size_t size, size_t alignment) mi_attr_noexcept mi_attr_alloc_size2(3,4) mi_attr_alloc_align(5); -mi_decl_nodiscard mi_decl_export void* mi_heap_recalloc_aligned_at(mi_heap_t* heap, void* p, size_t newcount, size_t size, size_t alignment, size_t offset) mi_attr_noexcept mi_attr_alloc_size2(3,4); +mi_decl_nodiscard mi_decl_export void* mi_heap_recalloc_aligned(mi_heap_t* heap, void* p, size_t newcount, size_t size, size_t alignment) mi_attr_noexcept mi_attr_alloc_size2(3, 4) mi_attr_alloc_align(5); +mi_decl_nodiscard mi_decl_export void* mi_heap_recalloc_aligned_at(mi_heap_t* heap, void* p, size_t newcount, size_t size, size_t alignment, size_t offset) mi_attr_noexcept mi_attr_alloc_size2(3, 4); + // ------------------------------------------------------ -// Analysis +// Visiting pages and individual blocks in a heap. // ------------------------------------------------------ -mi_decl_export bool mi_heap_contains_block(mi_heap_t* heap, const void* p); -mi_decl_export bool mi_heap_check_owned(mi_heap_t* heap, const void* p); -mi_decl_export bool mi_check_owned(const void* p); - // An area of heap space contains blocks of a single size. typedef struct mi_heap_area_s { - void* blocks; // start of the area containing heap blocks + void* blocks; // start of the area containing theap blocks size_t reserved; // bytes reserved for this area (virtual) size_t committed; // current available bytes for this area size_t used; // number of allocated blocks size_t block_size; // size in bytes of each block size_t full_block_size; // size in bytes of a full block including padding and metadata. + void* reserved1; // internal } mi_heap_area_t; typedef bool (mi_cdecl mi_block_visit_fun)(const mi_heap_t* heap, const mi_heap_area_t* area, void* block, size_t block_size, void* arg); -mi_decl_export bool mi_heap_visit_blocks(const mi_heap_t* heap, bool visit_all_blocks, mi_block_visit_fun* visitor, void* arg); +// Heap visitors assume no concurrent heap access (except thread exit). +mi_decl_export bool mi_heap_visit_blocks(mi_heap_t* heap, bool visit_blocks, mi_block_visit_fun* visitor, void* arg); +mi_decl_export bool mi_heap_visit_abandoned_blocks(mi_heap_t* heap, bool visit_blocks, mi_block_visit_fun* visitor, void* arg); + + +// ------------------------------------------------------ +// Arena memory management +// Arena's are larger memory area's provided by the OS or user +// ------------------------------------------------------ -// Experimental -mi_decl_nodiscard mi_decl_export bool mi_is_in_heap_region(const void* p) mi_attr_noexcept; mi_decl_nodiscard mi_decl_export bool mi_is_redirected(void) mi_attr_noexcept; -mi_decl_export int mi_reserve_huge_os_pages_interleave(size_t pages, size_t numa_nodes, size_t timeout_msecs) mi_attr_noexcept; -mi_decl_export int mi_reserve_huge_os_pages_at(size_t pages, int numa_node, size_t timeout_msecs) mi_attr_noexcept; +mi_decl_export int mi_reserve_huge_os_pages_interleave(size_t pages, size_t numa_nodes, size_t timeout_msecs) mi_attr_noexcept; +mi_decl_export int mi_reserve_huge_os_pages_at(size_t pages, int numa_node, size_t timeout_msecs) mi_attr_noexcept; -mi_decl_export int mi_reserve_os_memory(size_t size, bool commit, bool allow_large) mi_attr_noexcept; -mi_decl_export bool mi_manage_os_memory(void* start, size_t size, bool is_committed, bool is_large, bool is_zero, int numa_node) mi_attr_noexcept; +mi_decl_export int mi_reserve_os_memory(size_t size, bool commit, bool allow_large) mi_attr_noexcept; +mi_decl_export bool mi_manage_os_memory(void* start, size_t size, bool is_committed, bool is_pinned /* cannot decommit/reset? */, bool is_zero, int numa_node) mi_attr_noexcept; -mi_decl_export void mi_debug_show_arenas(void) mi_attr_noexcept; +mi_decl_export void mi_debug_show_arenas(void) mi_attr_noexcept; +mi_decl_export void mi_arenas_print(void) mi_attr_noexcept; +mi_decl_export void mi_arenas_purge(void) mi_attr_noexcept; +mi_decl_export size_t mi_arena_min_alignment(void); +mi_decl_export size_t mi_arena_min_size(void); +mi_decl_export size_t mi_arena_max_object_size(void); -// Experimental: heaps associated with specific memory arena's -typedef int mi_arena_id_t; -mi_decl_export void* mi_arena_area(mi_arena_id_t arena_id, size_t* size); -mi_decl_export int mi_reserve_huge_os_pages_at_ex(size_t pages, int numa_node, size_t timeout_msecs, bool exclusive, mi_arena_id_t* arena_id) mi_attr_noexcept; -mi_decl_export int mi_reserve_os_memory_ex(size_t size, bool commit, bool allow_large, bool exclusive, mi_arena_id_t* arena_id) mi_attr_noexcept; -mi_decl_export bool mi_manage_os_memory_ex(void* start, size_t size, bool is_committed, bool is_large, bool is_zero, int numa_node, bool exclusive, mi_arena_id_t* arena_id) mi_attr_noexcept; +typedef void* mi_arena_id_t; +mi_decl_export void* mi_arena_area(mi_arena_id_t arena_id, size_t* size); +mi_decl_export int mi_reserve_huge_os_pages_at_ex(size_t pages, int numa_node, size_t timeout_msecs, bool exclusive, mi_arena_id_t* arena_id) mi_attr_noexcept; +mi_decl_export int mi_reserve_os_memory_ex(size_t size, bool commit, bool allow_large, bool exclusive, mi_arena_id_t* arena_id) mi_attr_noexcept; +mi_decl_export bool mi_manage_os_memory_ex(void* start, size_t size, bool is_committed, bool is_pinned, bool is_zero, int numa_node, bool exclusive, mi_arena_id_t* arena_id) mi_attr_noexcept; +mi_decl_export bool mi_arena_contains(mi_arena_id_t arena_id, const void* p); -#if MI_MALLOC_VERSION >= 182 // Create a heap that only allocates in the specified arena mi_decl_nodiscard mi_decl_export mi_heap_t* mi_heap_new_in_arena(mi_arena_id_t arena_id); -#endif -// deprecated -mi_decl_export int mi_reserve_huge_os_pages(size_t pages, double max_secs, size_t* pages_reserved) mi_attr_noexcept; + +// ------------------------------------------------------ +// Subprocesses +// Advanced: allow sub-processes whose memory arena's stay fully separated (and no reclamation between them). +// Used for example for separate interpreters in one process. +// ------------------------------------------------------ + +typedef struct { void* _mi_subproc_id; } mi_subproc_id_t; // abstract type +mi_decl_export mi_subproc_id_t mi_subproc_main(void); +mi_decl_export mi_subproc_id_t mi_subproc_current(void); +mi_decl_export mi_subproc_id_t mi_subproc_new(void); +mi_decl_export void mi_subproc_destroy(mi_subproc_id_t subproc); +mi_decl_export void mi_subproc_add_current_thread(mi_subproc_id_t subproc); // this should be called right after a thread is created (and no allocation has taken place yet) + +typedef bool (mi_cdecl mi_heap_visit_fun)(mi_heap_t* heap, void* arg); +mi_decl_export bool mi_subproc_visit_heaps(mi_subproc_id_t subproc, mi_heap_visit_fun* visitor, void* arg); + + +// ------------------------------------------------------------------------------------- +// A "theap" is a thread-local heap. This API is only provided for special circumstances like runtimes +// that already have a thread-local context and can store the theap there for (slightly) faster allocations. +// This also allows to set a default theap for the current thread so that `malloc` etc. allocate from +// that theap (instead of the main (t)heap). +// Theaps are first-class, but can only allocate from the same thread that created it. +// Allocation through a `theap` may be a tiny bit faster than using plain malloc +// (as we don't need to lookup the thread local variable). +// ------------------------------------------------------------------------------------- + +struct mi_theap_s; +typedef struct mi_theap_s mi_theap_t; + +mi_decl_export mi_theap_t* mi_heap_theap(mi_heap_t* heap); +mi_decl_export mi_theap_t* mi_theap_set_default(mi_theap_t* theap); +mi_decl_export mi_theap_t* mi_theap_get_default(void); +mi_decl_export void mi_theap_collect(mi_theap_t* theap, bool force) mi_attr_noexcept; + +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_theap_malloc(mi_theap_t* theap, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_theap_zalloc(mi_theap_t* theap, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_theap_calloc(mi_theap_t* theap, size_t count, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size2(2, 3); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_theap_malloc_small(mi_theap_t* theap, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_theap_zalloc_small(mi_theap_t* theap, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_theap_malloc_aligned(mi_theap_t* theap, size_t size, size_t alignment) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2) mi_attr_alloc_align(3); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_theap_zalloc_aligned(mi_theap_t* theap, size_t size, size_t alignment) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2) mi_attr_alloc_align(3); +mi_decl_nodiscard mi_decl_export void* mi_theap_realloc(mi_theap_t* theap, void* p, size_t newsize) mi_attr_noexcept mi_attr_alloc_size(3); +mi_decl_nodiscard mi_decl_export void* mi_theap_rezalloc(mi_theap_t* theap, void* p, size_t newsize) mi_attr_noexcept mi_attr_alloc_size(3); + +// ------------------------------------------------------ +// Experimental +// ------------------------------------------------------ + +// Experimental: objects followed by a guard page. +// Setting the sample rate on a specific theap can be used to test parts of the program more +// specifically (in combination with `mi_theap_set_default`). +// A sample rate of 0 disables guarded objects, while 1 uses a guard page for every object. +// A seed of 0 uses a random start point. Only objects within the size bound are eligable for guard pages. +mi_decl_export void mi_theap_guarded_set_sample_rate(mi_theap_t* theap, size_t sample_rate, size_t seed); +mi_decl_export void mi_theap_guarded_set_size_bound(mi_theap_t* theap, size_t min, size_t max); + +// very experimental +typedef bool (mi_cdecl mi_commit_fun_t)(bool commit, void* start, size_t size, bool* is_zero, void* user_arg); +mi_decl_export bool mi_manage_memory(void* start, size_t size, bool is_committed, bool is_pinned, bool is_zero, int numa_node, bool exclusive, + mi_commit_fun_t* commit_fun, void* commit_fun_arg, mi_arena_id_t* arena_id) mi_attr_noexcept; + +//mi_decl_export bool mi_arena_unload(mi_arena_id_t arena_id, void** base, size_t* accessed_size, size_t* size); +//mi_decl_export bool mi_arena_reload(void* start, size_t size, mi_commit_fun_t* commit_fun, void* commit_fun_arg, mi_arena_id_t* arena_id); +//mi_decl_export bool mi_theap_reload(mi_theap_t* theap, mi_arena_id_t arena); +//mi_decl_export void mi_theap_unload(mi_theap_t* theap); + +// unsafe: assumes the page belonging to `p` is only accessed by the calling thread. +mi_decl_export bool mi_unsafe_heap_page_is_under_utilized(mi_heap_t* heap, void* p, size_t perc_threshold) mi_attr_noexcept; + +// Owner-thread only: abandon pages and delete the theap (not its heap or live blocks). +mi_decl_export void mi_theap_delete(mi_theap_t* theap) mi_attr_noexcept; + +// Abandon all theap pages, preserving live blocks and the theap for reuse. The owner must be the current thread or quiescent outside mimalloc (excluding concurrent heap/theap deletion). +mi_decl_export void mi_theap_quiescent_abandon(mi_theap_t* theap) mi_attr_noexcept; // ------------------------------------------------------ -// Convenience +// Deprecated // ------------------------------------------------------ -#define mi_malloc_tp(tp) ((tp*)mi_malloc(sizeof(tp))) -#define mi_zalloc_tp(tp) ((tp*)mi_zalloc(sizeof(tp))) -#define mi_calloc_tp(tp,n) ((tp*)mi_calloc(n,sizeof(tp))) -#define mi_mallocn_tp(tp,n) ((tp*)mi_mallocn(n,sizeof(tp))) -#define mi_reallocn_tp(p,tp,n) ((tp*)mi_reallocn(p,n,sizeof(tp))) -#define mi_recalloc_tp(p,tp,n) ((tp*)mi_recalloc(p,n,sizeof(tp))) +mi_decl_export bool mi_check_owned(const void* p); + +mi_decl_export void mi_thread_stats_print_out(mi_output_fun* out, void* arg) mi_attr_noexcept; +mi_decl_nodiscard mi_decl_export bool mi_is_in_heap_region(const void* p) mi_attr_noexcept; +mi_decl_export bool mi_theap_visit_blocks(const mi_theap_t* theap, bool visit_blocks, mi_block_visit_fun* visitor, void* arg); + +mi_decl_export int mi_reserve_huge_os_pages(size_t pages, double max_secs, size_t* pages_reserved) mi_attr_noexcept; +mi_decl_export void mi_collect_reduce(size_t target_thread_owned) mi_attr_noexcept; + +mi_decl_export void mi_stats_reset(void) mi_attr_noexcept; +mi_decl_export void mi_stats_merge(void) mi_attr_noexcept; +mi_decl_export void mi_stats_print(void* out) mi_attr_noexcept; // backward compatibility: `out` is ignored and should be NULL -#define mi_heap_malloc_tp(hp,tp) ((tp*)mi_heap_malloc(hp,sizeof(tp))) -#define mi_heap_zalloc_tp(hp,tp) ((tp*)mi_heap_zalloc(hp,sizeof(tp))) -#define mi_heap_calloc_tp(hp,tp,n) ((tp*)mi_heap_calloc(hp,n,sizeof(tp))) -#define mi_heap_mallocn_tp(hp,tp,n) ((tp*)mi_heap_mallocn(hp,n,sizeof(tp))) -#define mi_heap_reallocn_tp(hp,p,tp,n) ((tp*)mi_heap_reallocn(hp,p,n,sizeof(tp))) -#define mi_heap_recalloc_tp(hp,p,tp,n) ((tp*)mi_heap_recalloc(hp,p,n,sizeof(tp))) +mi_decl_export void mi_stats_print_out(mi_output_fun* out, void* arg) mi_attr_noexcept; // not deprecated but declared in `mimalloc-stats.h` now. // ------------------------------------------------------ @@ -318,40 +461,69 @@ mi_decl_export int mi_reserve_huge_os_pages(size_t pages, double max_secs, size typedef enum mi_option_e { // stable options - mi_option_show_errors, // print error messages - mi_option_show_stats, // print statistics on termination - mi_option_verbose, // print verbose messages - // the following options are experimental (see src/options.h) - mi_option_eager_commit, // eager commit segments? (after `eager_commit_delay` segments) (=1) - mi_option_arena_eager_commit, // eager commit arenas? Use 2 to enable just on overcommit systems (=2) - mi_option_purge_decommits, // should a memory purge decommit (or only reset) (=1) - mi_option_allow_large_os_pages, // allow large (2MiB) OS pages, implies eager commit - mi_option_reserve_huge_os_pages, // reserve N huge OS pages (1GiB/page) at startup - mi_option_reserve_huge_os_pages_at, // reserve huge OS pages at a specific NUMA node - mi_option_reserve_os_memory, // reserve specified amount of OS memory in an arena at startup + mi_option_show_errors, // print error messages + mi_option_show_stats, // print statistics on termination + mi_option_verbose, // print verbose messages + // advanced options + mi_option_deprecated_eager_commit, + mi_option_arena_eager_commit, // eager commit arenas? Use 2 to enable just on overcommit systems (=2) + mi_option_purge_decommits, // should a memory purge decommit? (=1). Set to 0 to use memory reset on a purge (instead of decommit) + mi_option_allow_large_os_pages, // allow use of large (2 or 4 MiB) OS pages, implies eager commit. + mi_option_reserve_huge_os_pages, // reserve N huge OS pages (1GiB pages) at startup + mi_option_reserve_huge_os_pages_at, // reserve huge OS pages at a specific NUMA node + mi_option_reserve_os_memory, // reserve specified amount of OS memory in an arena at startup (internally, this value is in KiB; use `mi_option_get_size`) mi_option_deprecated_segment_cache, mi_option_deprecated_page_reset, - mi_option_abandoned_page_purge, // immediately purge delayed purges on thread termination + mi_option_deprecated_abandoned_page_purge, mi_option_deprecated_segment_reset, - mi_option_eager_commit_delay, - mi_option_purge_delay, // memory purging is delayed by N milli seconds; use 0 for immediate purging or -1 for no purging at all. - mi_option_use_numa_nodes, // 0 = use all available numa nodes, otherwise use at most N nodes. - mi_option_limit_os_alloc, // 1 = do not use OS memory for allocation (but only programmatically reserved arenas) - mi_option_os_tag, // tag used for OS logging (macOS only for now) - mi_option_max_errors, // issue at most N error messages - mi_option_max_warnings, // issue at most N warning messages - mi_option_max_segment_reclaim, - mi_option_destroy_on_exit, // if set, release all memory on exit; sometimes used for dynamic unloading but can be unsafe. - mi_option_arena_reserve, // initial memory size in KiB for arena reservation (1GiB on 64-bit) - mi_option_arena_purge_mult, - mi_option_purge_extend_delay, + mi_option_deprecated_eager_commit_delay, + mi_option_purge_delay, // memory purging is delayed by N milli seconds; use 0 for immediate purging or -1 for no purging at all. (=10) + mi_option_use_numa_nodes, // 0 = use all available numa nodes, otherwise use at most N nodes. + mi_option_disallow_os_alloc, // 1 = do not use OS memory for allocation (but only programmatically reserved arenas) + mi_option_os_tag, // tag used for OS logging (macOS only for now) (=100) + mi_option_max_errors, // issue at most N error messages + mi_option_max_warnings, // issue at most N warning messages + mi_option_deprecated_max_segment_reclaim, // max. percentage of the abandoned segments can be reclaimed per try (=10%) + mi_option_destroy_on_exit, // if set, release all memory on exit; sometimes used for dynamic unloading but can be unsafe + mi_option_arena_reserve, // initial memory size for arena reservation (= 1 GiB on 64-bit) (internally, this value is in KiB; use `mi_option_get_size`) + mi_option_arena_purge_mult, // multiplier for `purge_delay` for the purging delay for arenas (=10) + mi_option_deprecated_purge_extend_delay, + mi_option_disallow_arena_alloc, // 1 = do not use arena's for allocation (except if using specific arena id's) + mi_option_retry_on_oom, // retry on out-of-memory for N milli seconds (=400), set to 0 to disable retries. (only on windows) + mi_option_deprecated_visit_abandoned, // allow visiting theap blocks from abandoned threads (=0) + mi_option_guarded_min, // only used when building with MI_GUARDED: minimal rounded object size for guarded objects (=0) + mi_option_guarded_max, // only used when building with MI_GUARDED: maximal rounded object size for guarded objects (=0) + mi_option_guarded_precise, // disregard minimal alignment requirement to always place guarded blocks exactly in front of a guard page (=0) + mi_option_guarded_sample_rate, // 1 out of N allocations in the min/max range will be guarded (=1000) + mi_option_guarded_sample_seed, // can be set to allow for a (more) deterministic re-execution when a guard page is triggered (=0) + mi_option_generic_collect, // collect theaps every N (=10000) generic allocation calls + mi_option_page_reclaim_on_free, // heap-creation default for reclaim on free: -1 disallow, 0 originating theap only, 1 allow across theaps + mi_option_page_full_retain, // retain N full (small) pages per size class (=2) + mi_option_page_max_candidates, // max candidate pages to consider for allocation (=4) + mi_option_max_vabits, // max user space virtual address bits to consider (=48) + mi_option_pagemap_commit, // commit the full pagemap (to always catch invalid pointer uses) (=0) + mi_option_page_commit_on_demand, // commit page memory on-demand + mi_option_page_max_reclaim, // don't reclaim pages of the same originating theap if we already own N pages (in that size class) (=-1 (unlimited)) + mi_option_page_cross_thread_max_reclaim, // don't reclaim pages across threads if we already own N pages (in that size class) (=32) + mi_option_allow_thp, // allow transparent huge pages? (=1) (on Android =0 by default). Set to 0 to disable THP for the process. + mi_option_arena_purge_min_size, // set minimal purge size (in KiB) (=0). By default set to either 64 or 2048 if THP is enabled. + mi_option_arena_max_object_size, // set maximal object size that can be allocated in an arena (in KiB) (=2GiB on 64-bit). + mi_option_arena_is_numa_local, // experimental: associate local numa node with an initial arena allocation + mi_option_stats_merge_on_collect, // on each theap collection, the theap stats are merged automatically with the parent heap + mi_option_profile_alloc_interval, // number of allocated bytes between automatic dumps of the `MIMALLOC_PROFILE` heap profiler (internally, this value is in KiB; use `mi_option_get_size`) (=0, disabled) + mi_option_profile_inuse_interval, // number of in-use bytes grown between automatic dumps of the `MIMALLOC_PROFILE` heap profiler (internally, this value is in KiB; use `mi_option_get_size`) (=0, disabled) + mi_option_profile_time_interval, // number of seconds between automatic dumps of the `MIMALLOC_PROFILE` heap profiler (=0, disabled) + mi_option_profile_sample_rate, // sample rate (in bytes) for the `MIMALLOC_PROFILE` heap profiler (internally, this value is in KiB; use `mi_option_get_size`) (=16 KiB) + mi_option_stats_merge_threshold, // on a page stats update, merge theap stats into the parent heap if N bytes were freed/allocated (=0, disabled) + mi_option_arena_purge_immediate_size, // ranges above this size are purged immediately _mi_option_last, // legacy option names mi_option_large_os_pages = mi_option_allow_large_os_pages, mi_option_eager_region_commit = mi_option_arena_eager_commit, mi_option_reset_decommits = mi_option_purge_decommits, mi_option_reset_delay = mi_option_purge_delay, - mi_option_abandoned_page_reset = mi_option_abandoned_page_purge + mi_option_limit_os_alloc = mi_option_disallow_os_alloc, + mi_option_minimal_purge_size = mi_option_arena_purge_min_size } mi_option_t; @@ -371,37 +543,42 @@ mi_decl_export void mi_option_set_default(mi_option_t option, long value); // ------------------------------------------------------------------------------------------------------- // "mi" prefixed implementations of various posix, Unix, Windows, and C++ allocation functions. // (This can be convenient when providing overrides of these functions as done in `mimalloc-override.h`.) -// note: we use `mi_cfree` as "checked free" and it checks if the pointer is in our heap before free-ing. +// note: we use `mi_cfree` as "checked free" and it checks if the pointer is in our theap before free-ing. // ------------------------------------------------------------------------------------------------------- -mi_decl_export void mi_cfree(void* p) mi_attr_noexcept; +mi_decl_export bool mi_cfree(void* p) mi_attr_noexcept; mi_decl_export void* mi__expand(void* p, size_t newsize) mi_attr_noexcept; mi_decl_nodiscard mi_decl_export size_t mi_malloc_size(const void* p) mi_attr_noexcept; mi_decl_nodiscard mi_decl_export size_t mi_malloc_good_size(size_t size) mi_attr_noexcept; mi_decl_nodiscard mi_decl_export size_t mi_malloc_usable_size(const void *p) mi_attr_noexcept; -mi_decl_export int mi_posix_memalign(void** p, size_t alignment, size_t size) mi_attr_noexcept; +mi_decl_export int mi_posix_memalign(void** p, size_t alignment, size_t size); // mi_attr_noexcept; mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_memalign(size_t alignment, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2) mi_attr_alloc_align(1); mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_valloc(size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1); mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_pvalloc(size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1); mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_aligned_alloc(size_t alignment, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2) mi_attr_alloc_align(1); mi_decl_nodiscard mi_decl_export void* mi_reallocarray(void* p, size_t count, size_t size) mi_attr_noexcept mi_attr_alloc_size2(2,3); -mi_decl_nodiscard mi_decl_export int mi_reallocarr(void* p, size_t count, size_t size) mi_attr_noexcept; +mi_decl_nodiscard mi_decl_export int mi_reallocarr(void* ptrp, size_t count, size_t size) mi_attr_noexcept; mi_decl_nodiscard mi_decl_export void* mi_aligned_recalloc(void* p, size_t newcount, size_t size, size_t alignment) mi_attr_noexcept; mi_decl_nodiscard mi_decl_export void* mi_aligned_offset_recalloc(void* p, size_t newcount, size_t size, size_t alignment, size_t offset) mi_attr_noexcept; -mi_decl_nodiscard mi_decl_export mi_decl_restrict unsigned short* mi_wcsdup(const unsigned short* s) mi_attr_noexcept mi_attr_malloc; -mi_decl_nodiscard mi_decl_export mi_decl_restrict unsigned char* mi_mbsdup(const unsigned char* s) mi_attr_noexcept mi_attr_malloc; -mi_decl_export int mi_dupenv_s(char** buf, size_t* size, const char* name) mi_attr_noexcept; -mi_decl_export int mi_wdupenv_s(unsigned short** buf, size_t* size, const unsigned short* name) mi_attr_noexcept; - -mi_decl_export void mi_free_size(void* p, size_t size) mi_attr_noexcept; mi_decl_export void mi_free_size_aligned(void* p, size_t size, size_t alignment) mi_attr_noexcept; mi_decl_export void mi_free_aligned(void* p, size_t alignment) mi_attr_noexcept; +mi_decl_export int mi_dupenv_s(char** buf, size_t* size, const char* name) mi_attr_noexcept; + +// wide characters +mi_decl_export int mi_wdupenv_s(wchar_t** buf, size_t* size, const wchar_t* name) mi_attr_noexcept; +mi_decl_nodiscard mi_decl_export mi_decl_restrict wchar_t* mi_wcsdup(const wchar_t* s) mi_attr_noexcept mi_attr_malloc; +mi_decl_nodiscard mi_decl_export mi_decl_restrict unsigned char* mi_mbsdup(const unsigned char* s) mi_attr_noexcept mi_attr_malloc; + +// -------------------------------------------------------- +// C++ wrappers +// The `mi_new` wrappers implement C++ semantics on out-of-memory +// instead of directly returning `NULL`. (and call `std::get_new_handler` +// and potentially raise a `std::bad_alloc` exception). +// -------------------------------------------------------- -// The `mi_new` wrappers implement C++ semantics on out-of-memory instead of directly returning `NULL`. -// (and call `std::get_new_handler` and potentially raise a `std::bad_alloc` exception). mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_new(size_t size) mi_attr_malloc mi_attr_alloc_size(1); mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_new_aligned(size_t size, size_t alignment) mi_attr_malloc mi_attr_alloc_size(1) mi_attr_alloc_align(2); mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_new_nothrow(size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(1); @@ -410,9 +587,73 @@ mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_new_n(size_t count, s mi_decl_nodiscard mi_decl_export void* mi_new_realloc(void* p, size_t newsize) mi_attr_alloc_size(2); mi_decl_nodiscard mi_decl_export void* mi_new_reallocn(void* p, size_t newcount, size_t size) mi_attr_alloc_size2(2, 3); -mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_heap_alloc_new(mi_heap_t* heap, size_t size) mi_attr_malloc mi_attr_alloc_size(2); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_heap_alloc_new(mi_heap_t* heap, size_t size) mi_attr_malloc mi_attr_alloc_size(2); mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_heap_alloc_new_n(mi_heap_t* heap, size_t count, size_t size) mi_attr_malloc mi_attr_alloc_size2(2, 3); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_theap_alloc_new(mi_theap_t* theap, size_t size) mi_attr_malloc mi_attr_alloc_size(2); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_theap_alloc_new_n(mi_theap_t* theap, size_t count, size_t size) mi_attr_malloc mi_attr_alloc_size2(2, 3); +mi_decl_nodiscard mi_decl_export mi_decl_restrict void* mi_theap_alloc_new_nothrow(mi_theap_t* theap, size_t size) mi_attr_noexcept mi_attr_malloc mi_attr_alloc_size(2); + + +// -------------------------------------------------------------------------- +// Inlined constant size allocations. +// These are meant for runtime systems, or overrides where we need the best +// performance for small, constant-size allocations. +// These are only better than the regular functions if the size or alignment +// are indeed constant at the call site. +// -------------------------------------------------------------------------- + +// Internal machine word size allocation. `wsize` is the allocation size in machine words (`sizeof(size_t)`) +mi_decl_nodiscard mi_decl_restrict void* mi_wzalloc_small(size_t wsize) mi_attr_noexcept mi_attr_malloc; +mi_decl_nodiscard mi_decl_restrict void* mi_wmalloc_small(size_t wsize) mi_attr_noexcept mi_attr_malloc; +mi_decl_nodiscard mi_decl_restrict void* mi_theap_wmalloc_small(mi_theap_t* theap, size_t wsize) mi_attr_noexcept mi_attr_malloc; +mi_decl_nodiscard mi_decl_restrict void* mi_theap_wzalloc_small(mi_theap_t* theap, size_t wsize) mi_attr_noexcept mi_attr_malloc; + +static inline size_t mi_wsize_from_size(size_t size) mi_attr_noexcept { + return (size + sizeof(size_t) - 1) / sizeof(size_t); +} + +static inline mi_decl_restrict void* mi_malloc_csize(size_t size) mi_attr_noexcept { + if (size <= MI_SMALL_SIZE_MAX) { return mi_wmalloc_small(mi_wsize_from_size(size)); } else { return mi_malloc(size); } +} +static inline mi_decl_restrict void* mi_zalloc_csize(size_t size) mi_attr_noexcept { + if (size <= MI_SMALL_SIZE_MAX) { return mi_wzalloc_small(mi_wsize_from_size(size)); } else { return mi_zalloc(size); } +} +static inline mi_decl_restrict void* mi_theap_malloc_csize(mi_theap_t* theap, size_t size) mi_attr_noexcept { + assert(theap!=NULL); + if (size <= MI_SMALL_SIZE_MAX) { return mi_theap_wmalloc_small(theap,mi_wsize_from_size(size)); } else { return mi_theap_malloc(theap,size); } +} +static inline mi_decl_restrict void* mi_theap_zalloc_csize(mi_theap_t* theap, size_t size) mi_attr_noexcept { + assert(theap!=NULL); + if (size <= MI_SMALL_SIZE_MAX) { return mi_theap_wzalloc_small(theap,mi_wsize_from_size(size)); } else { return mi_theap_zalloc(theap,size); } +} + +static inline void mi_free_csize(void* p, size_t size) mi_attr_noexcept { + if (size <= MI_SMALL_SIZE_MAX) { mi_free_small(p); } else { mi_free(p); } +} +static inline void mi_free_csize_nonnull(void* p, size_t size) mi_attr_noexcept { + assert(p!=NULL); + if (size <= MI_SMALL_SIZE_MAX) { mi_free_small_nonnull(p); } else { mi_free(p); } +} +static inline void mi_free_csize_aligned(void* p, size_t size, size_t aligned) mi_attr_noexcept { + if (aligned <= size && size <= MI_SMALL_SIZE_MAX) { mi_free_small(p); } else { mi_free(p); } +} +static inline void mi_free_csize_aligned_nonnull(void* p, size_t size, size_t aligned) mi_attr_noexcept { + assert(p!=NULL); + if (aligned <= size && size <= MI_SMALL_SIZE_MAX) { mi_free_small_nonnull(p); } else { mi_free(p); } +} + +// As `mi_free_small` but `p` must be allocated on a page owned by the current thread (be very careful!) +mi_decl_export void mi_free_small_local(void* p) mi_attr_noexcept; + +// As `mi_free_small_local` but `p` cannot be NULL as well. +mi_decl_export void mi_free_small_local_nonnull(void* p) mi_attr_noexcept; + + +// ------------------------------------------------------ +// C++ standard library allocator interface. +// ------------------------------------------------------ + #ifdef __cplusplus } #endif @@ -488,13 +729,13 @@ template bool operator!=(const mi_stl_allocator& , const #include // std::shared_ptr -// Common base class for STL allocators in a specific heap +// Common base class for STL allocators in a specific theap template struct _mi_heap_stl_allocator_common : public _mi_stl_allocator_common { using typename _mi_stl_allocator_common::size_type; using typename _mi_stl_allocator_common::value_type; using typename _mi_stl_allocator_common::pointer; - _mi_heap_stl_allocator_common(mi_heap_t* hp) : heap(hp) { } /* will not delete nor destroy the passed in heap */ + _mi_heap_stl_allocator_common(mi_heap_t* hp) : heap(hp, [](mi_heap_t*) {}) {} /* will not delete nor destroy the passed in heap */ #if (__cplusplus >= 201703L) // C++17 mi_decl_nodiscard T* allocate(size_type count) { return static_cast(mi_heap_alloc_new_n(this->heap.get(), count, sizeof(T))); } diff --git a/Include/internal/mimalloc/mimalloc/alloc/alloc.h b/Include/internal/mimalloc/mimalloc/alloc/alloc.h new file mode 100644 index 000000000000000..314c5150dc8b25c --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/alloc/alloc.h @@ -0,0 +1,37 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_ALLOC_H +#define MI_INTERNAL_ALLOC_H + +#include "../types.h" +#include "../util/libc.h" +#include "../util/options.h" + +mi_decl_restrict void* _mi_page_malloc_zero(mi_theap_t* theap, mi_page_t* page, size_t size, bool zero) mi_attr_noexcept; // called from `_mi_theap_malloc_aligned` +mi_decl_restrict void* _mi_theap_malloc_zero(mi_theap_t* theap, size_t size, bool zero, size_t huge_alignment, mi_page_t** ppage) mi_attr_noexcept; // called from `_mi_theap_malloc_aligned` +void* _mi_theap_realloc_zero(mi_theap_t* theap, void* p, size_t newsize, bool zero) mi_attr_noexcept; + +// Safe multiply `count*size` into `total`; return `true` on overflow. +static inline bool mi_count_size_overflow(size_t count, size_t size, size_t* total) { + if (count==1) { // quick check for the case where count is one (common for C++ allocators) + *total = size; + return false; + } + else if mi_likely(!mi_mul_overflow(count, size, total)) { + return false; + } + else { + #if MI_DEBUG > 0 + _mi_error_message(EOVERFLOW, "allocation request is too large (%zu * %zu bytes)\n", count, size); + #endif + *total = SIZE_MAX; + return true; + } +} + +#endif diff --git a/Include/internal/mimalloc/mimalloc/alloc/free.h b/Include/internal/mimalloc/mimalloc/alloc/free.h new file mode 100644 index 000000000000000..2f06ea861ee658c --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/alloc/free.h @@ -0,0 +1,18 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_FREE_H +#define MI_INTERNAL_FREE_H + +#include "../types.h" + +mi_block_t* _mi_page_ptr_unalign(const mi_page_t* page, const void* p); +void _mi_padding_shrink(const mi_page_t* page, const mi_block_t* block, const size_t min_size); +void _mi_free_subproc_safe(void* p) mi_attr_noexcept; +size_t _mi_page_usable_size(const mi_page_t* page, const void* p) mi_attr_noexcept; + +#endif diff --git a/Include/internal/mimalloc/mimalloc/alloc/page.h b/Include/internal/mimalloc/mimalloc/alloc/page.h new file mode 100644 index 000000000000000..a44cdc5c92bdc87 --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/alloc/page.h @@ -0,0 +1,605 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_PAGE_H +#define MI_INTERNAL_PAGE_H + +#include "../types.h" +#include "../prim/os.h" +#include "../heap/heap.h" +#include "../heap/page-map.h" +#include "../track.h" + +mi_decl_restrict void* _mi_malloc_generic(mi_theap_t* theap, size_t size, size_t zero_huge_alignment, mi_page_t** ppage) mi_attr_noexcept mi_attr_malloc; +mi_decl_restrict void* _mi_malloc_generic_no_sample(mi_theap_t* theap, size_t size, bool zero, mi_page_t** ppage) mi_attr_noexcept mi_attr_malloc; + +void _mi_page_retire(mi_page_t* page) mi_attr_noexcept; // free the page if there are no other pages with many free blocks +void _mi_page_unfull(mi_page_t* page); +bool _mi_page_free(mi_page_t* page, mi_page_queue_t* pq); // false if page release is delayed +void _mi_page_abandon(mi_page_t* page, mi_page_queue_t* pq); // abandon the page, to be picked up by another thread... +void _mi_deferred_free(mi_theap_t* theap, bool force); +bool _mi_page_free_collect(mi_page_t* page, bool force); // returns `true` if cross-thread free'd blocks were collected +mi_block_t* _mi_page_free_collect_partly(mi_page_t* page, mi_block_t* head); +mi_decl_nodiscard bool _mi_page_init(mi_theap_t* theap, mi_page_t* page); +void _mi_page_update_stats(mi_page_t* page); + +void _mi_theap_collect_retired(mi_theap_t* theap, bool force); +void _mi_theap_page_reclaim(mi_theap_t* theap, mi_page_t* page); +#if MI_DEBUG>1 +bool _mi_page_is_valid(mi_page_t* page); +#endif + +// in page-queue.c +bool _mi_page_queue_is_valid(mi_theap_t* theap, const mi_page_queue_t* pq); +size_t _mi_page_stats_bin(const mi_page_t* page); // for stats +size_t _mi_bin_size(size_t bin); // for stats +size_t _mi_bin(size_t size); // for stats + + +static inline mi_page_queue_t* mi_page_queue(const mi_theap_t* theap, size_t size) { + mi_page_queue_t* const pq = &((mi_theap_t*)theap)->pages[_mi_bin(size)]; + if (size <= MI_LARGE_MAX_OBJ_SIZE) { mi_assert_internal(pq->block_size <= MI_LARGE_MAX_OBJ_SIZE); } + return pq; +} + +// Get the block size of a page +static inline size_t mi_page_block_size(const mi_page_t* page) { + mi_assert_internal(page->block_size > 0); + return page->block_size; +} + +// Page start +static inline uint8_t* mi_page_start(const mi_page_t* page) { + // multiplication must be done in `size_t` + return (uint8_t*)page + page->page_offset; +} + +static inline size_t mi_page_size(const mi_page_t* page) { + return mi_page_block_size(page) * page->reserved; +} + +static inline uint8_t* mi_page_area(const mi_page_t* page, size_t* size) { + if (size) { *size = mi_page_size(page); } + return mi_page_start(page); +} + +static inline size_t mi_page_info_size(void) { + return mi_align_up(sizeof(mi_page_t), MI_MAX_ALIGN_SIZE); +} + +// Can the first block in a page with blocks of size `bsize` have a cache offset? (see `MI_PAGE_CACHE_OFFSET`) +static inline bool mi_block_size_has_cache_offset(size_t bsize) { + return (MI_PAGE_CACHE_OFFSET != 0 && bsize > MI_SMALL_MAX_OBJ_SIZE); +} + +// Is a (non-small) block of size `bsize` allocated in a medium page? +// Blocks of 1/8th of a medium page (64 KiB) leave no space for a cache offset and use a large page instead. +static inline bool mi_block_size_is_medium(size_t bsize) { + #if MI_ENABLE_LARGE_PAGES + if (mi_block_size_has_cache_offset(bsize) && bsize == MI_MEDIUM_PAGE_SIZE/8) return false; + #endif + return (bsize <= MI_MEDIUM_MAX_OBJ_SIZE); +} + +// The alignment that every block of size `bsize` is guaranteed to have in any page (or 0 if `bsize==0`). +// This is guaranteed by the block start (see `arena/arena-page.c:mi_page_meta_in_front_block_start` and `mi_page_meta_select`), +// and used by `mi_malloc_aligned` to avoid over-allocation for naturally aligned blocks. +static inline size_t mi_natural_alignment_of_size(size_t bsize) { + const size_t lowbit = bsize & (~bsize + 1); // largest power of 2 that divides `bsize` + if (mi_is_power_of_two(bsize) && bsize <= MI_PAGE_MAX_START_BLOCK_ALIGN2) return bsize; // power-of-two blocks up to 4KiB + if (bsize <= MI_SMALL_SIZE_MAX) return lowbit; // small blocks start at a multiple of their size + if ((bsize % MI_PAGE_OSPAGE_BLOCK_ALIGN2) == 0) { // multiples of 4KiB are 4KiB aligned (or cache offset unit aligned) + return (mi_block_size_has_cache_offset(bsize) ? MI_PAGE_CACHE_OFFSET_UNIT : MI_PAGE_OSPAGE_BLOCK_ALIGN2); + } + return (lowbit < MI_MAX_ALIGN_SIZE ? lowbit : MI_MAX_ALIGN_SIZE); // otherwise blocks start at least `MI_MAX_ALIGN_SIZE` aligned +} + +static inline bool mi_page_contains_address(const mi_page_t* page, const void* p) { + size_t psize; + uint8_t* start = mi_page_area(page, &psize); + return (start <= (uint8_t*)p && (uint8_t*)p < start + psize); +} + +static inline bool mi_page_is_in_arena(const mi_page_t* page) { + return (page->memid.memkind == MI_MEM_ARENA); +} + +static inline bool mi_page_is_singleton(const mi_page_t* page) { + return (page->reserved == 1); +} + +// Get the usable block size of a page without fixed padding. +// This may still include internal padding due to alignment and rounding up size classes. +static inline size_t mi_page_usable_block_size(const mi_page_t* page) { + return mi_page_block_size(page) - MI_PADDING_SIZE; +} + +static inline bool mi_page_meta_is_separated(const mi_page_t* page) { + #if MI_PAGE_META_IS_ALIGNED + #if MI_PAGE_META_SMALL_IS_ALIGNED + return (page != mi_align_down_ptr(mi_page_start(page), MI_ARENA_SLICE_ALIGN)); + #else + MI_UNUSED_RELEASE(page); + mi_assert_internal(page != mi_align_down_ptr(mi_page_start(page), MI_ARENA_SLICE_ALIGN)); + return true; + #endif + #elif MI_PAGE_META_IS_SEPARATED + // usually separated but can still be in front for direct OS allocations (due to size or alignment) or due to MI_PAGE_META_SMALL_IS_ALIGNED + return (page->memid.memkind == MI_MEM_ARENA && page != mi_align_down_ptr(mi_page_start(page), MI_ARENA_SLICE_ALIGN)); + #else + MI_UNUSED(page); + return false; + #endif +} + +static inline uint8_t* mi_page_slice_start(const mi_page_t* page) { + if (mi_page_meta_is_separated(page)) { + // page meta info is at a separate location (at `arena->pages`) + return (uint8_t*)mi_align_down_ptr(mi_page_start(page), MI_ARENA_SLICE_ALIGN); + } + else { + // page meta info is at the start of the page slices + return (uint8_t*)page; + } +} + +// This gives the offset relative to the start slice of a page. +static inline size_t mi_page_slice_offset_of(const mi_page_t* page, size_t offset_relative_to_page_start) { + return (mi_page_start(page) - mi_page_slice_start(page)) + offset_relative_to_page_start; +} + +// How much of the page is committed relative to the slice start? (or 0 if fully committed already) +static inline size_t mi_page_slice_committed(const mi_page_t* page) { + return ((size_t)page->slice_pcommitted * _mi_os_page_size()); +} + +// Currently committed part of a page +static inline size_t mi_page_committed(const mi_page_t* page) { + const size_t slice_committed = mi_page_slice_committed(page); + return (slice_committed == 0 ? mi_page_size(page) : slice_committed - mi_page_slice_offset_of(page,0)); +} + +static inline size_t mi_page_used(const mi_page_t* page) { + mi_assert_internal(page != NULL); + return mi_xused_used_count(page->xused); +} + +static inline void mi_page_used_reset(mi_page_t* page) { + page->xused = mi_xused_used_reset(page->xused); +} + +static inline size_t mi_page_alloc_count(const mi_page_t* page) { + return mi_xused_alloc_count(page->xused); +} + +static inline size_t mi_page_last_used(const mi_page_t* page) { + #if MI_SIZE_SIZE >= 8 + return (page->xused.used_alloc >> 32) & 0xFFFF; + #else + return page->xlast_used; + #endif +} + +static inline size_t mi_page_last_alloc(const mi_page_t* page) { + #if MI_SIZE_SIZE >= 8 + return (page->xused.used_alloc >> 48) & 0xFFFF; + #else + return page->xlast_alloc; + #endif +} + + +// are all blocks in a page freed? +// note: needs up-to-date used count, (as the `xthread_free` list may not be empty). see `_mi_page_collect_free`. +static inline bool mi_page_all_free(const mi_page_t* page) { + mi_assert_internal(page != NULL); + return (mi_page_used(page)==0); +} + +// are there immediately available blocks, i.e. blocks available on the free list. +static inline bool mi_page_immediate_available(const mi_page_t* page) { + mi_assert_internal(page != NULL); + return (page->free != NULL); +} + + +// is the page not yet used up to its reserved space? +static inline bool mi_page_is_expandable(const mi_page_t* page) { + mi_assert_internal(page != NULL); + mi_assert_internal(page->capacity <= page->reserved); + return (page->capacity < page->reserved); +} + + +static inline bool mi_page_is_full(const mi_page_t* page) { + const bool full = (page->reserved == mi_page_used(page)); + mi_assert_internal(!full || page->free == NULL); + return full; +} + +// is more than (1 - 1/n)'th of a page in use? +static inline bool mi_page_is_used_at_frac(const mi_page_t* page, size_t n) { + if (page==NULL) return true; + const size_t frac = page->reserved / n; + return (page->reserved - mi_page_used(page) <= frac); +} + +// is more than 7/8th of a page in use? +static inline bool mi_page_is_mostly_used(const mi_page_t* page) { + return mi_page_is_used_at_frac(page, 8); +} + +static inline bool mi_page_is_small(const mi_page_t* page) { + return (page->block_size <= MI_SMALL_MAX_OBJ_SIZE && !mi_page_is_singleton(page)); +} + +static inline bool mi_page_is_huge(const mi_page_t* page) { + return (mi_page_is_singleton(page) && + (page->block_size > MI_LARGE_MAX_OBJ_SIZE || + (mi_memkind_is_os(page->memid.memkind) && page->memid.mem.os.base < (void*)page))); +} + +static inline size_t mi_page_min_commit_size(void) { + const size_t psize = _mi_os_page_size(); + return (MI_PAGE_MIN_COMMIT_SIZE >= psize ? MI_PAGE_MIN_COMMIT_SIZE : psize); +} + +//----------------------------------------------------------- +// Page thread id and flags +//----------------------------------------------------------- + +// Thread id of thread that owns this page (with flags in the bottom 2 bits) +static inline mi_threadid_t mi_page_xthread_id(const mi_page_t* page) { + return mi_atomic_load_relaxed(&((mi_page_t*)page)->xthread_id); +} + +// Plain thread id of the thread that owns this page +static inline mi_threadid_t mi_page_thread_id(const mi_page_t* page) { + return (mi_page_xthread_id(page) & ~MI_PAGE_FLAG_MASK); +} + +static inline mi_page_flags_t mi_page_flags(const mi_page_t* page) { + return (mi_page_xthread_id(page) & MI_PAGE_FLAG_MASK); +} + +static inline bool mi_page_flags_set(mi_page_t* page, bool set, mi_page_flags_t newflag) { + mi_page_flags_t old; + if (set) { old = mi_atomic_or_relaxed(&page->xthread_id, newflag); } + else { old = mi_atomic_and_relaxed(&page->xthread_id, ~newflag); } + return ((old & newflag) == newflag); +} + +static inline bool mi_page_is_in_full(const mi_page_t* page) { + return ((mi_page_flags(page) & MI_PAGE_IN_FULL_QUEUE) != 0); +} + +static inline void mi_page_set_in_full(mi_page_t* page, bool in_full) { + const bool was_in_full = mi_page_flags_set(page, in_full, MI_PAGE_IN_FULL_QUEUE); + if (was_in_full != in_full) { + // optimize: maintain pages_full_size to avoid visiting the full queue (issue #1220) + mi_theap_t* const theap = page->theap; + mi_assert_internal(theap!=NULL); + if (theap != NULL) { + mi_assert_internal(page->capacity==page->reserved); + const size_t size = page->reserved * mi_page_block_size(page); + if (in_full) { theap->pages_full_size += size; } + else { mi_assert_internal(size <= theap->pages_full_size); theap->pages_full_size -= size; } + } + } +} + +static inline bool mi_page_has_interior_pointers(const mi_page_t* page) { + return ((mi_page_flags(page) & MI_PAGE_HAS_INTERIOR_POINTERS) != 0); +} + +static inline void mi_page_set_has_interior_pointers(mi_page_t* page, bool has_aligned) { + mi_page_flags_set(page, has_aligned, MI_PAGE_HAS_INTERIOR_POINTERS); +} + +static inline void mi_page_set_theap(mi_page_t* page, mi_theap_t* theap) { + // mi_assert_internal(!mi_page_is_in_full(page)); // can happen when destroying pages on theap_destroy + page->theap = theap; + const mi_threadid_t tid = (theap == NULL ? MI_THREADID_ABANDONED : theap->tld->thread_id); + mi_assert_internal((tid & MI_PAGE_FLAG_MASK) == 0); + + // we need to use an atomic cas since a concurrent thread may still set the MI_PAGE_HAS_INTERIOR_POINTERS flag (see `alloc_aligned.c`). + mi_threadid_t xtid_old = mi_page_xthread_id(page); + mi_threadid_t xtid; + do { + xtid = tid | (xtid_old & MI_PAGE_FLAG_MASK); + } while (!mi_atomic_cas_weak_release(&page->xthread_id, &xtid_old, xtid)); +} + +static inline bool mi_page_is_abandoned(const mi_page_t* page) { + // note: the xtheap field of an abandoned theap is set to the subproc (for fast reclaim-on-free) + return (mi_page_thread_id(page) <= MI_THREADID_ABANDONED_MAPPED); +} + +static inline bool mi_page_is_abandoned_mapped(const mi_page_t* page) { + return (mi_page_thread_id(page) == MI_THREADID_ABANDONED_MAPPED); +} + +static inline void mi_page_set_abandoned_mapped(mi_page_t* page) { + mi_assert_internal(mi_page_is_abandoned(page)); + mi_atomic_or_relaxed(&page->xthread_id, (mi_threadid_t)MI_THREADID_ABANDONED_MAPPED); +} + +static inline void mi_page_clear_abandoned_mapped(mi_page_t* page) { + mi_assert_internal(mi_page_is_abandoned_mapped(page)); + mi_atomic_and_relaxed(&page->xthread_id, (mi_threadid_t)MI_PAGE_FLAG_MASK); +} + + +static inline mi_theap_t* mi_page_theap(const mi_page_t* page) { + mi_assert_internal(!mi_page_is_abandoned(page)); + mi_assert_internal(page->theap != NULL && !_mi_is_empty_theap(page->theap)); + return page->theap; +} + +static inline mi_tld_t* mi_page_tld(const mi_page_t* page) { + mi_assert_internal(!mi_page_is_abandoned(page)); + mi_assert_internal(page->theap != NULL); + return page->theap->tld; +} + + +static inline mi_heap_t* mi_page_heap(const mi_page_t* page) { + mi_heap_t* heap = page->heap; + mi_assert_internal(heap != NULL); + return heap; +} + +static inline mi_subproc_t* mi_page_subproc(const mi_page_t* page) { + mi_heap_t* const heap = mi_page_heap(page); + return heap->subproc; +} + +//----------------------------------------------------------- +// Thread free list and ownership +//----------------------------------------------------------- + +// Thread free flag helpers +static inline mi_block_t* mi_tf_block(mi_thread_free_t tf) { + return (mi_block_t*)(tf & ~1); +} + +static inline bool mi_tf_is_owned(mi_thread_free_t tf) { + return ((tf & 1) == 1); +} + +static inline mi_thread_free_t mi_tf_create(mi_block_t* block, bool owned) { + const uintptr_t base = (uintptr_t)block | (owned ? 1 : 0); + return (mi_thread_free_t)base; +} + +static inline mi_thread_free_t mi_tf_set_owned(mi_thread_free_t tf, bool owned) { + return mi_tf_create(mi_tf_block(tf), owned); +} + +// Thread free access +static inline mi_block_t* mi_page_thread_free(const mi_page_t* page) { + return mi_tf_block(mi_atomic_load_relaxed(&((mi_page_t*)page)->xthread_free)); +} + +// are there any available blocks? +static inline bool mi_page_has_any_available(const mi_page_t* page) { + mi_assert_internal(page != NULL && page->reserved > 0); + return (mi_page_used(page) < page->reserved || (mi_page_thread_free(page) != NULL)); +} + +// Owned? +static inline bool mi_page_is_owned(const mi_page_t* page) { + return mi_tf_is_owned(mi_atomic_load_relaxed(&((mi_page_t*)page)->xthread_free)); +} + +// get ownership; returns true if the page was not owned before. +static inline bool mi_page_claim_ownership(mi_page_t* page) { + const uintptr_t old = mi_atomic_or_acq_rel(&page->xthread_free, (uintptr_t)1); + return ((old&1)==0); +} + +/* ----------------------------------------------------------- + Delayed page release +----------------------------------------------------------- */ + +static inline void mi_page_qsbr_unlink(mi_page_t* page) { +#if MI_QSBR + if (!page->qsbr_in_queue) { + mi_assert_internal(page->qsbr_next == NULL && page->qsbr_prev == NULL); + return; + } + mi_theap_t* theap = mi_page_theap(page); + if (page->qsbr_prev != NULL) { page->qsbr_prev->qsbr_next = page->qsbr_next; } + else { + mi_assert_internal(theap->qsbr_first == page); + theap->qsbr_first = page->qsbr_next; + } + if (page->qsbr_next != NULL) { page->qsbr_next->qsbr_prev = page->qsbr_prev; } + else { + mi_assert_internal(theap->qsbr_last == page); + theap->qsbr_last = page->qsbr_prev; + } + page->qsbr_next = page->qsbr_prev = NULL; + page->qsbr_in_queue = false; +#else + MI_UNUSED(page); +#endif +} + +static inline void mi_page_qsbr_enqueue(mi_page_t* page, mi_theap_t* theap) { +#if MI_QSBR + mi_assert_internal(mi_page_theap(page) == theap); + mi_assert_internal(mi_page_all_free(page)); + mi_assert_internal(mi_heap_qsbr_is_enabled(mi_page_heap(page))); + mi_assert_internal(!mi_page_qsbr_is_clear(&page->qsbr)); + if (page->qsbr_in_queue) return; + mi_assert_internal(page->qsbr_next == NULL && page->qsbr_prev == NULL); + page->qsbr_prev = theap->qsbr_last; + if (theap->qsbr_last != NULL) { theap->qsbr_last->qsbr_next = page; } + else { theap->qsbr_first = page; } + theap->qsbr_last = page; + page->qsbr_in_queue = true; +#else + MI_UNUSED(page); + MI_UNUSED(theap); +#endif +} + +static inline void _mi_page_qsbr_clear(mi_page_t* page) { +#if MI_QSBR + mi_page_qsbr_unlink(page); + mi_page_qsbr_clear(&page->qsbr); +#else + MI_UNUSED(page); +#endif +} + +static inline bool _mi_page_qsbr_is_clear(const mi_page_t* page) { +#if MI_QSBR + return (!page->qsbr_in_queue && page->qsbr_next == NULL && page->qsbr_prev == NULL && + mi_page_qsbr_is_clear(&page->qsbr)); +#else + MI_UNUSED(page); + return true; +#endif +} + +static inline bool _mi_page_qsbr_should_delay(mi_page_t* page) { +#if MI_QSBR + mi_assert_internal(mi_page_is_owned(page) && mi_page_all_free(page)); + mi_heap_t* heap = mi_page_heap(page); + if (!mi_heap_qsbr_is_enabled(heap)) return false; + const bool delayed = mi_page_qsbr_should_delay(&page->qsbr, &heap->qsbr, page->capacity * mi_page_block_size(page)); + mi_assert_internal(!delayed || !mi_page_qsbr_is_clear(&page->qsbr)); + return delayed; +#else + MI_UNUSED(page); + return false; +#endif +} + +/* ------------------------------------------------------------------- +Encoding/Decoding the free list next pointers + +This is to protect against buffer overflow exploits where the +free list is mutated. Many hardened allocators xor the next pointer `p` +with a secret key `k1`, as `p^k1`. This prevents overwriting with known +values but might be still too weak: if the attacker can guess +the pointer `p` this can reveal `k1` (since `p^k1^p == k1`). +Moreover, if multiple blocks can be read as well, the attacker can +xor both as `(p1^k1) ^ (p2^k1) == p1^p2` which may reveal a lot +about the pointers (and subsequently `k1`). + +Instead mimalloc uses an extra key `k2` and encodes as `((p^k2)<<next, keys); + #else + MI_UNUSED(keys); MI_UNUSED(null); + next = (mi_block_t*)block->next; + #endif + mi_track_mem_noaccess(block,sizeof(mi_block_t)); + return next; +} + +static inline void mi_block_set_nextx(const void* null, mi_block_t* block, const mi_block_t* next, const uintptr_t* keys) { + mi_track_mem_undefined(block,sizeof(mi_block_t)); + #if MI_ENCODE_FREELIST + block->next = mi_ptr_encode(null, next, keys); + #else + MI_UNUSED(keys); MI_UNUSED(null); + block->next = (mi_encoded_t)next; + #endif + mi_track_mem_noaccess(block,sizeof(mi_block_t)); +} + +static inline mi_block_t* mi_block_next(const mi_page_t* page, const mi_block_t* block) { + #if MI_ENCODE_FREELIST + mi_block_t* next = mi_block_nextx(page,block,page->keys); + // check for free list corruption: is `next` at least in the same page? + // todo: check if `next` is `page->block_size` aligned? + if mi_unlikely(next!=NULL && !mi_page_contains_address(page,next)) { + return _mi_block_next_is_corrupted(page,block,next); // returns NULL + } + return next; + #else + MI_UNUSED(page); + return mi_block_nextx(page,block,NULL); + #endif +} + +static inline void mi_block_set_next(const mi_page_t* page, mi_block_t* block, const mi_block_t* next) { + #if MI_ENCODE_FREELIST + mi_block_set_nextx(page,block,next, page->keys); + #else + MI_UNUSED(page); + mi_block_set_nextx(page,block,next,NULL); + #endif +} + +#endif diff --git a/Include/internal/mimalloc/mimalloc/arena/arena-alloc.h b/Include/internal/mimalloc/mimalloc/arena/arena-alloc.h new file mode 100644 index 000000000000000..710f29cc902a11f --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/arena/arena-alloc.h @@ -0,0 +1,18 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_ARENA_ALLOC_H +#define MI_INTERNAL_ARENA_ALLOC_H + +#include "../types.h" + +void* _mi_arenas_alloc(mi_heap_t* heap, size_t size, bool commit, bool allow_pinned, mi_arena_t* req_arena, size_t tseq, int numa_node, mi_memid_t* memid); +void* _mi_arenas_alloc_aligned(mi_heap_t* heap, size_t size, size_t alignment, size_t align_offset, bool commit, bool allow_pinned, mi_arena_t* req_arena, size_t tseq, int numa_node, mi_memid_t* memid); +void _mi_arenas_free(mi_subproc_t* subproc, void* p, size_t size, mi_memid_t memid); +void _mi_arenas_collect(bool force_purge, bool visit_all, mi_tld_t* tld); + +#endif diff --git a/Include/internal/mimalloc/mimalloc/arena/arena-page.h b/Include/internal/mimalloc/mimalloc/arena/arena-page.h new file mode 100644 index 000000000000000..40715566b4653c2 --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/arena/arena-page.h @@ -0,0 +1,25 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_ARENA_PAGE_H +#define MI_INTERNAL_ARENA_PAGE_H + +#include "../types.h" + +mi_page_t* _mi_arenas_page_alloc(mi_theap_t* theap, size_t block_size, size_t page_alignment); +void _mi_arenas_page_free(mi_page_t* page, mi_theap_t* current_theapx /* can be NULL */); +void _mi_arenas_page_abandon(mi_page_t* page, mi_theap_t* current_theap); +void _mi_arenas_page_unabandon(mi_page_t* page, mi_theap_t* current_theapx /* can be NULL */); +bool _mi_arenas_page_try_reabandon_to_mapped(mi_page_t* page); +void _mi_heap_move_pages(mi_heap_t* heap_from, mi_heap_t* heap_to); +void _mi_heap_destroy_pages(mi_heap_t* heap_from); +#if MI_QSBR +// The caller must stop all allocation, freeing, and theap lifecycle operations on this heap. +void _mi_heap_collect_abandoned(mi_heap_t* heap); +#endif + +#endif diff --git a/Include/internal/mimalloc/mimalloc/arena/arena.h b/Include/internal/mimalloc/mimalloc/arena/arena.h new file mode 100644 index 000000000000000..18d486a2eb42d1b --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/arena/arena.h @@ -0,0 +1,23 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_ARENA_H +#define MI_INTERNAL_ARENA_H + +#include "../types.h" + +mi_arena_id_t _mi_arena_id_none(void); +mi_arena_t* _mi_arena_from_id(mi_arena_id_t id); +bool _mi_arena_memid_is_suitable(mi_memid_t memid, mi_arena_t* request_arena); + +void _mi_arenas_unsafe_destroy_all(mi_subproc_t* subproc); + +mi_heap_t* _mi_arena_heap_main(const mi_arena_t* arena); +size_t _mi_slice_count_of_size(size_t size); +size_t _mi_size_of_slices(size_t bcount); + +#endif diff --git a/Include/internal/mimalloc/mimalloc/atomic.h b/Include/internal/mimalloc/mimalloc/atomic.h index 65fa477d6437826..2cbe2486d3d6d1c 100644 --- a/Include/internal/mimalloc/mimalloc/atomic.h +++ b/Include/internal/mimalloc/mimalloc/atomic.h @@ -1,12 +1,24 @@ /* ---------------------------------------------------------------------------- -Copyright (c) 2018-2023 Microsoft Research, Daan Leijen +Copyright (c) 2018-2024 Microsoft Research, Daan Leijen This is free software; you can redistribute it and/or modify it under the terms of the MIT license. A copy of the license can be found in the file "LICENSE" at the root of this distribution. -----------------------------------------------------------------------------*/ #pragma once -#ifndef MIMALLOC_ATOMIC_H -#define MIMALLOC_ATOMIC_H +#ifndef MI_ATOMIC_H +#define MI_ATOMIC_H + +// include windows.h or pthreads.h +#if defined(_WIN32) || defined(__CYGWIN__) // we use windows locks on cygwin, but otherwise treat it as unix +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif +#if MI_TLS_MODEL_PTHREADS || (!defined(_WIN32) && !defined(__wasi__) && (!defined(__EMSCRIPTEN__) || defined(__EMSCRIPTEN_PTHREADS__))) +#define MI_USE_PTHREADS 1 +#include +#endif // -------------------------------------------------------------------------------------------- // Atomics @@ -20,33 +32,33 @@ terms of the MIT license. A copy of the license can be found in the file #if defined(__cplusplus) // Use C++ atomics #include -#define _Atomic(tp) std::atomic -#define mi_atomic(name) std::atomic_##name -#define mi_memory_order(name) std::memory_order_##name -#if (__cplusplus >= 202002L) // c++20, see issue #571 - #define MI_ATOMIC_VAR_INIT(x) x +#define _Atomic(tp) std::atomic +#define mi_atomic(name) std::atomic_##name +#define mi_memory_order(name) std::memory_order_##name +#if (__cplusplus >= 202002L) // c++20, see issue #571 + #define MI_ATOMIC_VAR_INIT(x) x #elif !defined(ATOMIC_VAR_INIT) - #define MI_ATOMIC_VAR_INIT(x) x + #define MI_ATOMIC_VAR_INIT(x) x #else - #define MI_ATOMIC_VAR_INIT(x) ATOMIC_VAR_INIT(x) + #define MI_ATOMIC_VAR_INIT(x) ATOMIC_VAR_INIT(x) #endif -#elif defined(_MSC_VER) +#elif defined(_MSC_VER) && !defined(__clang__) // Use MSVC C wrapper for C11 atomics -#define _Atomic(tp) tp -#define MI_ATOMIC_VAR_INIT(x) x -#define mi_atomic(name) mi_atomic_##name -#define mi_memory_order(name) mi_memory_order_##name +#define _Atomic(tp) tp +#define MI_ATOMIC_VAR_INIT(x) x +#define mi_atomic(name) mi_atomic_##name +#define mi_memory_order(name) mi_memory_order_##name #else // Use C11 atomics #include -#define mi_atomic(name) atomic_##name -#define mi_memory_order(name) memory_order_##name +#define mi_atomic(name) atomic_##name +#define mi_memory_order(name) memory_order_##name #if (__STDC_VERSION__ >= 201710L) // c17, see issue #735 - #define MI_ATOMIC_VAR_INIT(x) x + #define MI_ATOMIC_VAR_INIT(x) x #elif !defined(ATOMIC_VAR_INIT) - #define MI_ATOMIC_VAR_INIT(x) x + #define MI_ATOMIC_VAR_INIT(x) x #else - #define MI_ATOMIC_VAR_INIT(x) ATOMIC_VAR_INIT(x) + #define MI_ATOMIC_VAR_INIT(x) ATOMIC_VAR_INIT(x) #endif #endif @@ -61,31 +73,40 @@ terms of the MIT license. A copy of the license can be found in the file #define mi_atomic_load_relaxed(p) mi_atomic(load_explicit)(p,mi_memory_order(relaxed)) #define mi_atomic_store_release(p,x) mi_atomic(store_explicit)(p,x,mi_memory_order(release)) #define mi_atomic_store_relaxed(p,x) mi_atomic(store_explicit)(p,x,mi_memory_order(relaxed)) +#define mi_atomic_exchange_relaxed(p,x) mi_atomic(exchange_explicit)(p,x,mi_memory_order(relaxed)) #define mi_atomic_exchange_release(p,x) mi_atomic(exchange_explicit)(p,x,mi_memory_order(release)) #define mi_atomic_exchange_acq_rel(p,x) mi_atomic(exchange_explicit)(p,x,mi_memory_order(acq_rel)) + +#define mi_atomic_cas_weak_relaxed(p,exp,des) mi_atomic_cas_weak(p,exp,des,mi_memory_order(relaxed),mi_memory_order(relaxed)) #define mi_atomic_cas_weak_release(p,exp,des) mi_atomic_cas_weak(p,exp,des,mi_memory_order(release),mi_memory_order(relaxed)) #define mi_atomic_cas_weak_acq_rel(p,exp,des) mi_atomic_cas_weak(p,exp,des,mi_memory_order(acq_rel),mi_memory_order(acquire)) +#define mi_atomic_cas_strong_relaxed(p,exp,des) mi_atomic_cas_strong(p,exp,des,mi_memory_order(relaxed),mi_memory_order(relaxed)) #define mi_atomic_cas_strong_release(p,exp,des) mi_atomic_cas_strong(p,exp,des,mi_memory_order(release),mi_memory_order(relaxed)) #define mi_atomic_cas_strong_acq_rel(p,exp,des) mi_atomic_cas_strong(p,exp,des,mi_memory_order(acq_rel),mi_memory_order(acquire)) #define mi_atomic_add_relaxed(p,x) mi_atomic(fetch_add_explicit)(p,x,mi_memory_order(relaxed)) -#define mi_atomic_sub_relaxed(p,x) mi_atomic(fetch_sub_explicit)(p,x,mi_memory_order(relaxed)) #define mi_atomic_add_acq_rel(p,x) mi_atomic(fetch_add_explicit)(p,x,mi_memory_order(acq_rel)) +#define mi_atomic_sub_relaxed(p,x) mi_atomic(fetch_sub_explicit)(p,x,mi_memory_order(relaxed)) #define mi_atomic_sub_acq_rel(p,x) mi_atomic(fetch_sub_explicit)(p,x,mi_memory_order(acq_rel)) +#define mi_atomic_and_relaxed(p,x) mi_atomic(fetch_and_explicit)(p,x,mi_memory_order(relaxed)) #define mi_atomic_and_acq_rel(p,x) mi_atomic(fetch_and_explicit)(p,x,mi_memory_order(acq_rel)) +#define mi_atomic_or_relaxed(p,x) mi_atomic(fetch_or_explicit)(p,x,mi_memory_order(relaxed)) #define mi_atomic_or_acq_rel(p,x) mi_atomic(fetch_or_explicit)(p,x,mi_memory_order(acq_rel)) -#define mi_atomic_increment_relaxed(p) mi_atomic_add_relaxed(p,(uintptr_t)1) -#define mi_atomic_decrement_relaxed(p) mi_atomic_sub_relaxed(p,(uintptr_t)1) -#define mi_atomic_increment_acq_rel(p) mi_atomic_add_acq_rel(p,(uintptr_t)1) -#define mi_atomic_decrement_acq_rel(p) mi_atomic_sub_acq_rel(p,(uintptr_t)1) +#define mi_atomic_increment_relaxed(p) mi_atomic_add_relaxed(p,(size_t)1) +#define mi_atomic_decrement_relaxed(p) mi_atomic_sub_relaxed(p,(size_t)1) +#define mi_atomic_increment_acq_rel(p) mi_atomic_add_acq_rel(p,(size_t)1) +#define mi_atomic_decrement_acq_rel(p) mi_atomic_sub_acq_rel(p,(size_t)1) -static inline void mi_atomic_yield(void); -static inline intptr_t mi_atomic_addi(_Atomic(intptr_t)*p, intptr_t add); -static inline intptr_t mi_atomic_subi(_Atomic(intptr_t)*p, intptr_t sub); +static inline mi_ssize_t mi_atomic_addi_acq_rel(_Atomic(mi_ssize_t)*p, mi_ssize_t add); +static inline mi_ssize_t mi_atomic_subi_acq_rel(_Atomic(mi_ssize_t)*p, mi_ssize_t sub); +static inline mi_ssize_t mi_atomic_addi_relaxed(_Atomic(mi_ssize_t)*p, mi_ssize_t add); +static inline mi_ssize_t mi_atomic_subi_relaxed(_Atomic(mi_ssize_t)*p, mi_ssize_t sub); +static inline void mi_atomic_storess_relaxed(_Atomic(mi_ssize_t)* p, mi_ssize_t x); +static inline mi_ssize_t mi_atomic_loads_relaxed(_Atomic(mi_ssize_t)* p); -#if defined(__cplusplus) || !defined(_MSC_VER) +#if defined(__cplusplus) || !defined(_MSC_VER) || defined(__clang__) // In C++/C11 atomics we have polymorphic atomics so can use the typed `ptr` variants (where `tp` is the type of atomic value) // We use these macros so we can provide a typed wrapper in MSVC in C compilation mode as well @@ -99,6 +120,8 @@ static inline intptr_t mi_atomic_subi(_Atomic(intptr_t)*p, intptr_t sub); #define mi_atomic_cas_ptr_weak_release(tp,p,exp,des) mi_atomic_cas_weak_release(p,exp,(tp*)des) #define mi_atomic_cas_ptr_weak_acq_rel(tp,p,exp,des) mi_atomic_cas_weak_acq_rel(p,exp,(tp*)des) #define mi_atomic_cas_ptr_strong_release(tp,p,exp,des) mi_atomic_cas_strong_release(p,exp,(tp*)des) +#define mi_atomic_cas_ptr_strong_acq_rel(tp,p,exp,des) mi_atomic_cas_strong_acq_rel(p,exp,(tp*)des) +#define mi_atomic_exchange_ptr_relaxed(tp,p,x) mi_atomic_exchange_relaxed(p,(tp*)x) #define mi_atomic_exchange_ptr_release(tp,p,x) mi_atomic_exchange_release(p,(tp*)x) #define mi_atomic_exchange_ptr_acq_rel(tp,p,x) mi_atomic_exchange_acq_rel(p,(tp*)x) #else @@ -107,15 +130,23 @@ static inline intptr_t mi_atomic_subi(_Atomic(intptr_t)*p, intptr_t sub); #define mi_atomic_cas_ptr_weak_release(tp,p,exp,des) mi_atomic_cas_weak_release(p,exp,des) #define mi_atomic_cas_ptr_weak_acq_rel(tp,p,exp,des) mi_atomic_cas_weak_acq_rel(p,exp,des) #define mi_atomic_cas_ptr_strong_release(tp,p,exp,des) mi_atomic_cas_strong_release(p,exp,des) +#define mi_atomic_cas_ptr_strong_acq_rel(tp,p,exp,des) mi_atomic_cas_strong_acq_rel(p,exp,des) +#define mi_atomic_exchange_ptr_relaxed(tp,p,x) mi_atomic_exchange_relaxed(p,x) #define mi_atomic_exchange_ptr_release(tp,p,x) mi_atomic_exchange_release(p,x) #define mi_atomic_exchange_ptr_acq_rel(tp,p,x) mi_atomic_exchange_acq_rel(p,x) #endif // These are used by the statistics -static inline int64_t mi_atomic_addi64_relaxed(volatile int64_t* p, int64_t add) { +static inline int64_t mi_atomic_volatile_addi64_relaxed(volatile int64_t* p, int64_t add) { return mi_atomic(fetch_add_explicit)((_Atomic(int64_t)*)p, add, mi_memory_order(relaxed)); } -static inline void mi_atomic_maxi64_relaxed(volatile int64_t* p, int64_t x) { +static inline void mi_atomic_volatile_void_addi64_relaxed(volatile int64_t* p, const volatile int64_t* padd) { + const int64_t add = mi_atomic_load_relaxed((_Atomic(int64_t)*)padd); + if (add != 0) { + mi_atomic(fetch_add_explicit)((_Atomic(int64_t)*)p, add, mi_memory_order(relaxed)); + } +} +static inline void mi_atomic_volatile_maxi64_relaxed(volatile int64_t* p, int64_t x) { int64_t current = mi_atomic_load_relaxed((_Atomic(int64_t)*)p); while (current < x && !mi_atomic_cas_weak_release((_Atomic(int64_t)*)p, ¤t, x)) { /* nothing */ }; } @@ -128,20 +159,23 @@ static inline void mi_atomic_maxi64_relaxed(volatile int64_t* p, int64_t x) { #define mi_atomic_casi64_strong_acq_rel(p,e,d) mi_atomic_cas_strong_acq_rel(p,e,d) #define mi_atomic_addi64_acq_rel(p,i) mi_atomic_add_acq_rel(p,i) +#define mi_atomic_addi64_relaxed(p,i) mi_atomic_add_relaxed(p,i) -#elif defined(_MSC_VER) +#elif defined(_MSC_VER) && !defined(__clang__) + +// Deprecated: MSVC plain C compilation wrapper that uses Interlocked operations to model C11 atomics. +// It is recommended to always compile as C++ when using MSVC. -// MSVC C compilation wrapper that uses Interlocked operations to model C11 atomics. -#define WIN32_LEAN_AND_MEAN -#include #include #ifdef _WIN64 -typedef LONG64 msc_intptr_t; -#define MI_64(f) f##64 +typedef LONG64 msc_intptr_t; +#define MI_MSC_64(f) f##64 +#define MI_MSC_XX(f) f##64 #else -typedef LONG msc_intptr_t; -#define MI_64(f) f +typedef LONG msc_intptr_t; +#define MI_MSC_64(f) f +#define MI_MSC_XX(f) f##32 #endif typedef enum mi_memory_order_e { @@ -155,23 +189,23 @@ typedef enum mi_memory_order_e { static inline uintptr_t mi_atomic_fetch_add_explicit(_Atomic(uintptr_t)*p, uintptr_t add, mi_memory_order mo) { (void)(mo); - return (uintptr_t)MI_64(_InterlockedExchangeAdd)((volatile msc_intptr_t*)p, (msc_intptr_t)add); + return (uintptr_t)MI_MSC_64(_InterlockedExchangeAdd)((volatile msc_intptr_t*)p, (msc_intptr_t)add); } static inline uintptr_t mi_atomic_fetch_sub_explicit(_Atomic(uintptr_t)*p, uintptr_t sub, mi_memory_order mo) { (void)(mo); - return (uintptr_t)MI_64(_InterlockedExchangeAdd)((volatile msc_intptr_t*)p, -((msc_intptr_t)sub)); + return (uintptr_t)MI_MSC_64(_InterlockedExchangeAdd)((volatile msc_intptr_t*)p, -((msc_intptr_t)sub)); } static inline uintptr_t mi_atomic_fetch_and_explicit(_Atomic(uintptr_t)*p, uintptr_t x, mi_memory_order mo) { (void)(mo); - return (uintptr_t)MI_64(_InterlockedAnd)((volatile msc_intptr_t*)p, (msc_intptr_t)x); + return (uintptr_t)MI_MSC_64(_InterlockedAnd)((volatile msc_intptr_t*)p, (msc_intptr_t)x); } static inline uintptr_t mi_atomic_fetch_or_explicit(_Atomic(uintptr_t)*p, uintptr_t x, mi_memory_order mo) { (void)(mo); - return (uintptr_t)MI_64(_InterlockedOr)((volatile msc_intptr_t*)p, (msc_intptr_t)x); + return (uintptr_t)MI_MSC_64(_InterlockedOr)((volatile msc_intptr_t*)p, (msc_intptr_t)x); } static inline bool mi_atomic_compare_exchange_strong_explicit(_Atomic(uintptr_t)*p, uintptr_t* expected, uintptr_t desired, mi_memory_order mo1, mi_memory_order mo2) { (void)(mo1); (void)(mo2); - uintptr_t read = (uintptr_t)MI_64(_InterlockedCompareExchange)((volatile msc_intptr_t*)p, (msc_intptr_t)desired, (msc_intptr_t)(*expected)); + const uintptr_t read = (uintptr_t)MI_MSC_64(_InterlockedCompareExchange)((volatile msc_intptr_t*)p, (msc_intptr_t)desired, (msc_intptr_t)(*expected)); if (read == *expected) { return true; } @@ -185,82 +219,145 @@ static inline bool mi_atomic_compare_exchange_weak_explicit(_Atomic(uintptr_t)*p } static inline uintptr_t mi_atomic_exchange_explicit(_Atomic(uintptr_t)*p, uintptr_t exchange, mi_memory_order mo) { (void)(mo); - return (uintptr_t)MI_64(_InterlockedExchange)((volatile msc_intptr_t*)p, (msc_intptr_t)exchange); -} -static inline void mi_atomic_thread_fence(mi_memory_order mo) { - (void)(mo); - _Atomic(uintptr_t) x = 0; - mi_atomic_exchange_explicit(&x, 1, mo); + return (uintptr_t)MI_MSC_64(_InterlockedExchange)((volatile msc_intptr_t*)p, (msc_intptr_t)exchange); } + static inline uintptr_t mi_atomic_load_explicit(_Atomic(uintptr_t) const* p, mi_memory_order mo) { (void)(mo); -#if defined(_M_IX86) || defined(_M_X64) - return *p; -#else - uintptr_t x = *p; - if (mo > mi_memory_order_relaxed) { - while (!mi_atomic_compare_exchange_weak_explicit((_Atomic(uintptr_t)*)p, &x, x, mo, mi_memory_order_relaxed)) { /* nothing */ }; - } - return x; -#endif + // assert(mo<=mi_memory_order_acquire); // others are not used by mimalloc + #if defined(_M_IX86) || defined(_M_X64) + // on x86/x64 we have a strong memory model so any load is acquire + return (uintptr_t)MI_MSC_XX(__iso_volatile_load)((volatile const intptr_t*)p); + #elif defined(_M_ARM) || defined(_M_ARM64) + if (mo == mi_memory_order_relaxed) { + return (uintptr_t)MI_MSC_XX(__iso_volatile_load)((volatile const intptr_t*)p); + } + #if !defined(__clang__) // work around __ldar missing in clang-cl, see https://github.com/llvm/llvm-project/issues/121689 + else if (mo <= mi_memory_order_acquire) { + return MI_MSC_XX(__ldar)((volatile const uintptr_t*)p); + } + #endif + else { + const uintptr_t u = (uintptr_t)MI_MSC_XX(__iso_volatile_load)((volatile const intptr_t*)p); + __dmb(15); // _ARM(64)_BARRIER_SY + return u; + } + #else + #warning "define mi_atomic_load_explicit for MSVC C compilation on this platform (which should be readonly, see issue #1277)" + return MI_MSC_XX(__iso_volatile_load)((volatile const intptr_t*)p); + #endif } static inline void mi_atomic_store_explicit(_Atomic(uintptr_t)*p, uintptr_t x, mi_memory_order mo) { (void)(mo); -#if defined(_M_IX86) || defined(_M_X64) - *p = x; -#else - mi_atomic_exchange_explicit(p, x, mo); -#endif + // assert(mo<=mi_memory_order_release); // others are not used by mimalloc + #if defined(_M_IX86) || defined(_M_X64) + MI_MSC_XX(__iso_volatile_store)((volatile intptr_t*)p, x); + #elif defined(_M_ARM) || defined(_M_ARM64) + if (mo == mi_memory_order_relaxed) { + MI_MSC_XX(__iso_volatile_store)((volatile intptr_t*)p, x); + } + #if !defined(__clang__) // work around __stlr missing in clang-cl, see https://github.com/llvm/llvm-project/issues/121689 + else if (mo <= mi_memory_order_release) { + MI_MSC_XX(__stlr)((volatile uintptr_t*)p,x); + } + #endif + else { + mi_atomic_exchange_explicit(p, x, mo); + } + #else + mi_atomic_exchange_explicit(p, x, mo); + #endif } + static inline int64_t mi_atomic_loadi64_explicit(_Atomic(int64_t)*p, mi_memory_order mo) { (void)(mo); -#if defined(_M_X64) - return *p; -#else - int64_t old = *p; - int64_t x = old; - while ((old = InterlockedCompareExchange64(p, x, old)) != x) { - x = old; - } - return x; -#endif + // assert(mo<=mi_memory_order_acquire); // others are not used by mimalloc + #if defined(_M_IX86) || defined(_M_X64) + return __iso_volatile_load64((volatile const int64_t*)p); + #elif defined(_M_ARM) || defined(_M_ARM64) + if (mo == mi_memory_order_relaxed) { + return __iso_volatile_load64((volatile const int64_t*)p); + } + #if defined(_M_ARM64) && !defined(__clang__) // work around __ldar64 missing in clang-cl, see https://github.com/llvm/llvm-project/issues/121689 + else if (mo <= mi_memory_order_acquire) { + return __ldar64((volatile const uintptr_t*)p); + } + #endif + else { + const int64_t i = __iso_volatile_load64((volatile const int64_t*)p); + __dmb(15); // _ARM(64)_BARRIER_SY + return i; + } + #else + #warning "define mi_atomic_loadi64_explicit for MSVC C compilation on this platform (which should be readonly, see issue #1277)" + return __iso_volatile_load64((volatile const int64_t*)p); + #endif } + static inline void mi_atomic_storei64_explicit(_Atomic(int64_t)*p, int64_t x, mi_memory_order mo) { (void)(mo); -#if defined(x_M_IX86) || defined(_M_X64) - *p = x; -#else - InterlockedExchange64(p, x); -#endif + // assert(mo<=mi_memory_order_release); // others are not used by mimalloc + #if defined(_M_IX86) || defined(_M_X64) + __iso_volatile_store64((volatile int64_t*)p,x); + #elif defined(_M_ARM) || defined(_M_ARM64) + if (mo == mi_memory_order_relaxed) { + __iso_volatile_store64((volatile int64_t*)p,x); + } + #if defined(_M_ARM64) && !defined(__clang__) // work around __stlr64 missing in clang-cl, see https://github.com/llvm/llvm-project/issues/121689 + else if (mo == mi_memory_order_release) { + __stlr64((volatile uint64_t*)p, (uint64_t)x); + } + #endif + else { + InterlockedExchange64(p, x); + } + #else + InterlockedExchange64(p, x); + #endif } // These are used by the statistics -static inline int64_t mi_atomic_addi64_relaxed(volatile _Atomic(int64_t)*p, int64_t add) { -#ifdef _WIN64 - return (int64_t)mi_atomic_addi((int64_t*)p, add); -#else - int64_t current; - int64_t sum; - do { - current = *p; - sum = current + add; - } while (_InterlockedCompareExchange64(p, sum, current) != current); - return current; -#endif +static inline int64_t mi_atomic_volatile_addi64_relaxed(volatile _Atomic(int64_t)*p, int64_t add) { + #ifdef _WIN64 + return (int64_t)mi_atomic_addi_relaxed((_Atomic(int64_t)*)p, add); + #elif defined(_M_ARM) + return _InterlockedExchangeAdd64(p, add); + #else + // x86 + int64_t current; + int64_t sum; + do { + current = __iso_volatile_load64((volatile const int64_t*)p); + sum = current + add; + } while (_InterlockedCompareExchange64(p, sum, current) != current); + return current; + #endif +} + +static inline void mi_atomic_volatile_void_addi64_relaxed(volatile int64_t* p, const volatile int64_t* padd) { + const int64_t add = *padd; + if (add != 0) { + mi_atomic_volatile_addi64_relaxed((volatile _Atomic(int64_t)*)p, add); + } } -static inline void mi_atomic_maxi64_relaxed(volatile _Atomic(int64_t)*p, int64_t x) { + +static inline void mi_atomic_volatile_maxi64_relaxed(volatile _Atomic(int64_t)* p, int64_t x) { int64_t current; do { current = *p; } while (current < x && _InterlockedCompareExchange64(p, x, current) != current); } -static inline void mi_atomic_addi64_acq_rel(volatile _Atomic(int64_t*)p, int64_t i) { - mi_atomic_addi64_relaxed(p, i); +static inline void mi_atomic_addi64_acq_rel(volatile _Atomic(int64_t)* p, int64_t i) { + mi_atomic_volatile_addi64_relaxed(p, i); +} + +static inline int64_t mi_atomic_addi64_relaxed(volatile _Atomic(int64_t)* p, int64_t i) { + return mi_atomic_volatile_addi64_relaxed(p, i); } -static inline bool mi_atomic_casi64_strong_acq_rel(volatile _Atomic(int64_t*)p, int64_t* exp, int64_t des) { - int64_t read = _InterlockedCompareExchange64(p, des, *exp); +static inline bool mi_atomic_casi64_strong_acq_rel(volatile _Atomic(int64_t)* p, int64_t* exp, int64_t des) { + const int64_t read = _InterlockedCompareExchange64(p, des, *exp); if (read == *exp) { return true; } @@ -271,15 +368,17 @@ static inline bool mi_atomic_casi64_strong_acq_rel(volatile _Atomic(int64_t*)p, } // The pointer macros cast to `uintptr_t`. -#define mi_atomic_load_ptr_acquire(tp,p) (tp*)mi_atomic_load_acquire((_Atomic(uintptr_t)*)(p)) -#define mi_atomic_load_ptr_relaxed(tp,p) (tp*)mi_atomic_load_relaxed((_Atomic(uintptr_t)*)(p)) +#define mi_atomic_load_ptr_acquire(tp,p) ((tp*)mi_atomic_load_acquire((_Atomic(uintptr_t)*)(p))) +#define mi_atomic_load_ptr_relaxed(tp,p) ((tp*)mi_atomic_load_relaxed((_Atomic(uintptr_t)*)(p))) #define mi_atomic_store_ptr_release(tp,p,x) mi_atomic_store_release((_Atomic(uintptr_t)*)(p),(uintptr_t)(x)) #define mi_atomic_store_ptr_relaxed(tp,p,x) mi_atomic_store_relaxed((_Atomic(uintptr_t)*)(p),(uintptr_t)(x)) #define mi_atomic_cas_ptr_weak_release(tp,p,exp,des) mi_atomic_cas_weak_release((_Atomic(uintptr_t)*)(p),(uintptr_t*)exp,(uintptr_t)des) #define mi_atomic_cas_ptr_weak_acq_rel(tp,p,exp,des) mi_atomic_cas_weak_acq_rel((_Atomic(uintptr_t)*)(p),(uintptr_t*)exp,(uintptr_t)des) #define mi_atomic_cas_ptr_strong_release(tp,p,exp,des) mi_atomic_cas_strong_release((_Atomic(uintptr_t)*)(p),(uintptr_t*)exp,(uintptr_t)des) -#define mi_atomic_exchange_ptr_release(tp,p,x) (tp*)mi_atomic_exchange_release((_Atomic(uintptr_t)*)(p),(uintptr_t)x) -#define mi_atomic_exchange_ptr_acq_rel(tp,p,x) (tp*)mi_atomic_exchange_acq_rel((_Atomic(uintptr_t)*)(p),(uintptr_t)x) +#define mi_atomic_cas_ptr_strong_acq_rel(tp,p,exp,des) mi_atomic_cas_strong_acq_rel((_Atomic(uintptr_t)*)(p),(uintptr_t*)exp,(uintptr_t)des) +#define mi_atomic_exchange_ptr_relaxed(tp,p,x) ((tp*)mi_atomic_exchange_relaxed((_Atomic(uintptr_t)*)(p),(uintptr_t)x)) +#define mi_atomic_exchange_ptr_release(tp,p,x) ((tp*)mi_atomic_exchange_release((_Atomic(uintptr_t)*)(p),(uintptr_t)x)) +#define mi_atomic_exchange_ptr_acq_rel(tp,p,x) ((tp*)mi_atomic_exchange_acq_rel((_Atomic(uintptr_t)*)(p),(uintptr_t)x)) #define mi_atomic_loadi64_acquire(p) mi_atomic(loadi64_explicit)(p,mi_memory_order(acquire)) #define mi_atomic_loadi64_relaxed(p) mi_atomic(loadi64_explicit)(p,mi_memory_order(relaxed)) @@ -289,29 +388,32 @@ static inline bool mi_atomic_casi64_strong_acq_rel(volatile _Atomic(int64_t*)p, #endif - -// Atomically add a signed value; returns the previous value. -static inline intptr_t mi_atomic_addi(_Atomic(intptr_t)*p, intptr_t add) { - return (intptr_t)mi_atomic_add_acq_rel((_Atomic(uintptr_t)*)p, (uintptr_t)add); +static inline mi_ssize_t mi_atomic_addi_acq_rel(_Atomic(mi_ssize_t)*p, mi_ssize_t add) { + return (mi_ssize_t)mi_atomic_add_acq_rel((_Atomic(size_t)*)p, (size_t)add); } - -// Atomically subtract a signed value; returns the previous value. -static inline intptr_t mi_atomic_subi(_Atomic(intptr_t)*p, intptr_t sub) { - return (intptr_t)mi_atomic_addi(p, -sub); +static inline mi_ssize_t mi_atomic_subi_acq_rel(_Atomic(mi_ssize_t)*p, mi_ssize_t sub) { + return (mi_ssize_t)mi_atomic_addi_acq_rel(p, -sub); } - -typedef _Atomic(uintptr_t) mi_atomic_once_t; - -// Returns true only on the first invocation -static inline bool mi_atomic_once( mi_atomic_once_t* once ) { - if (mi_atomic_load_relaxed(once) != 0) return false; // quick test - uintptr_t expected = 0; - return mi_atomic_cas_strong_acq_rel(once, &expected, (uintptr_t)1); // try to set to 1 +static inline mi_ssize_t mi_atomic_addi_relaxed(_Atomic(mi_ssize_t)*p, mi_ssize_t add) { + return (mi_ssize_t)mi_atomic_add_relaxed((_Atomic(size_t)*)p, (size_t)add); +} +static inline mi_ssize_t mi_atomic_subi_relaxed(_Atomic(mi_ssize_t)*p, mi_ssize_t sub) { + return (mi_ssize_t)mi_atomic_addi_relaxed(p, -sub); +} +static inline void mi_atomic_storess_relaxed(_Atomic(mi_ssize_t)* p, mi_ssize_t x) { + mi_atomic_store_relaxed((_Atomic(size_t)*)p, (size_t)x); } +static inline mi_ssize_t mi_atomic_loads_relaxed(_Atomic(mi_ssize_t)* p) { + return (mi_ssize_t)mi_atomic_load_relaxed((_Atomic(size_t)*)p); +} + +// ---------------------------------------------------------------------- +// Guard +// ---------------------------------------------------------------------- typedef _Atomic(uintptr_t) mi_atomic_guard_t; -// Allows only one thread to execute at a time +// Allows only one thread to execute at a time (without blocking anyone) #define mi_atomic_guard(guard) \ uintptr_t _mi_guard_expected = 0; \ for(bool _mi_guard_once = true; \ @@ -319,72 +421,202 @@ typedef _Atomic(uintptr_t) mi_atomic_guard_t; (mi_atomic_store_release(guard,(uintptr_t)0), _mi_guard_once = false) ) +// ---------------------------------------------------------------------- +// Locks +// These should be light-weight in-process only locks. +// Only used for reserving arena's and to maintain the abandoned list. +// ---------------------------------------------------------------------- +#if _MSC_VER +#pragma warning(disable:26110) // unlock with holding lock +#endif + +#define mi_lock(lock) for(bool _mi_go = (mi_lock_acquire(lock),true); _mi_go; (mi_lock_release(lock), _mi_go=false) ) +#define mi_lock_maybe(lock,acquire) for(bool _mi_go = (acquire ? (mi_lock_acquire(lock),true) : true); _mi_go; _mi_go = (acquire ? (mi_lock_release(lock),false) : false) ) + + +#if defined(_WIN32) || defined(__CYGWIN__) + +typedef struct mi_lock_s { + SRWLOCK mutex; // slim reader-writer lock +} mi_lock_t; + +#define MI_LOCK_INITIALIZER { SRWLOCK_INIT } + +static inline bool mi_lock_try_acquire(mi_lock_t* lock) { + return TryAcquireSRWLockExclusive(&lock->mutex); +} +static inline void mi_lock_acquire(mi_lock_t* lock) { + AcquireSRWLockExclusive(&lock->mutex); +} +static inline void mi_lock_release(mi_lock_t* lock) { + ReleaseSRWLockExclusive(&lock->mutex); +} +static inline void mi_lock_init(mi_lock_t* lock) { + InitializeSRWLock(&lock->mutex); +} +static inline void mi_lock_done(mi_lock_t* lock) { + (void)(lock); +} + +#elif defined(MI_USE_PTHREADS) + +#include // memcpy +void _mi_error_message(int err, const char* fmt, ...); + +typedef struct mi_lock_s { + pthread_mutex_t mutex; +} mi_lock_t; + +#define MI_LOCK_INITIALIZER { PTHREAD_MUTEX_INITIALIZER } + +static inline bool mi_lock_try_acquire(mi_lock_t* lock) { + return (pthread_mutex_trylock(&lock->mutex) == 0); +} +static inline void mi_lock_acquire(mi_lock_t* lock) { + const int err = pthread_mutex_lock(&lock->mutex); + if (err != 0) { + _mi_error_message(err, "internal error: lock cannot be acquired (err %i)\n", err); + } +} +static inline void mi_lock_release(mi_lock_t* lock) { + pthread_mutex_unlock(&lock->mutex); +} +static inline void mi_lock_init(mi_lock_t* lock) { + if(lock==NULL) return; + // use this instead of pthread_mutex_init since that can cause allocation on some platforms (and recursively initialize) + const pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; + memcpy(&lock->mutex,&mutex,sizeof(mutex)); +} +static inline void mi_lock_done(mi_lock_t* lock) { + pthread_mutex_destroy(&lock->mutex); +} + +#elif defined(__cplusplus) -// Yield -#if defined(__cplusplus) #include -static inline void mi_atomic_yield(void) { - std::this_thread::yield(); +#include +#include + +typedef struct mi_lock_s { + std::mutex mutex; +} mi_lock_t; + +#define MI_LOCK_INITIALIZER { } + +static inline bool mi_lock_try_acquire(mi_lock_t* lock) { + return lock->mutex.try_lock(); } -#elif defined(_WIN32) -#define WIN32_LEAN_AND_MEAN -#include -static inline void mi_atomic_yield(void) { - YieldProcessor(); -} -#elif defined(__SSE2__) -#include -static inline void mi_atomic_yield(void) { - _mm_pause(); -} -#elif (defined(__GNUC__) || defined(__clang__)) && \ - (defined(__x86_64__) || defined(__i386__) || \ - defined(__aarch64__) || defined(__arm__) || \ - defined(__powerpc__) || defined(__ppc__) || defined(__PPC__) || defined(__POWERPC__)) -#if defined(__x86_64__) || defined(__i386__) -static inline void mi_atomic_yield(void) { - __asm__ volatile ("pause" ::: "memory"); -} -#elif defined(__aarch64__) -static inline void mi_atomic_yield(void) { - __asm__ volatile("wfe"); -} -#elif defined(__arm__) -#if __ARM_ARCH >= 7 -static inline void mi_atomic_yield(void) { - __asm__ volatile("yield" ::: "memory"); +static inline void mi_lock_acquire(mi_lock_t* lock) { + lock->mutex.lock(); } -#else -static inline void mi_atomic_yield(void) { - __asm__ volatile ("nop" ::: "memory"); +static inline void mi_lock_release(mi_lock_t* lock) { + lock->mutex.unlock(); } -#endif -#elif defined(__powerpc__) || defined(__ppc__) || defined(__PPC__) || defined(__POWERPC__) -#ifdef __APPLE__ -static inline void mi_atomic_yield(void) { - __asm__ volatile ("or r27,r27,r27" ::: "memory"); +static inline void mi_lock_init(mi_lock_t* lock) { + new(&lock->mutex) std::mutex(); // in-place constructor +} +static inline void mi_lock_done(mi_lock_t* lock) { + lock->mutex.~mutex(); // in-place destructor } + #else -static inline void mi_atomic_yield(void) { - __asm__ __volatile__ ("or 27,27,27" ::: "memory"); + +// fall back to poor man's locks. +// this should only be the case in a single-threaded environment (like __wasi__) +#include +#ifndef EFAULT +#define EFAULT (14) +#endif +void _mi_error_message(int err, const char* fmt, ...); +void _mi_prim_thread_yield(void); + +typedef struct mi_lock_s { + _Atomic(uintptr_t) mutex; +} mi_lock_t; + +#define MI_LOCK_INITIALIZER { MI_ATOMIC_VAR_INIT(0) } + +static inline bool mi_lock_try_acquire(mi_lock_t* lock) { + uintptr_t expected = 0; + return mi_atomic_cas_strong_acq_rel(&lock->mutex, &expected, (uintptr_t)1); +} +static inline void mi_lock_acquire(mi_lock_t* lock) { + size_t ticks = 0; + for (int i = 0; i < 10000; i++) { // for at most 10000 tries? + if (mi_lock_try_acquire(lock)) return; + _mi_prim_thread_yield(); + } + _mi_error_message(EFAULT, "internal error: lock cannot be acquired (due to lack of native lock primitives)\n"); +} +static inline void mi_lock_release(mi_lock_t* lock) { + mi_atomic_store_release(&lock->mutex, (uintptr_t)0); } +static inline void mi_lock_init(mi_lock_t* lock) { + mi_lock_release(lock); +} +static inline void mi_lock_done(mi_lock_t* lock) { + (void)(lock); +} + +#endif + + +typedef struct mi_atomic_once_s { + _Atomic(uintptr_t) tid; + mi_lock_t lock; +} mi_atomic_once_t; + +// Returns `true` only on the first invocation, signifying we can execute an action once. +// If it returns `true`, the caller should call `_mi_atomic_once_release` after performing the action. +// Other threads (than the initial thread that entered) will block until `_mi_atomic_once_release` has been called. +bool _mi_atomic_once_enter(mi_atomic_once_t* once); // defined in `util/libc.c` +void _mi_atomic_once_release(mi_atomic_once_t* once); // defined in `util/libc.c` + +#define mi_atomic_do_once \ + static mi_atomic_once_t _mi_once = { MI_ATOMIC_VAR_INIT(0), MI_LOCK_INITIALIZER }; \ + for(bool _mi_exec = _mi_atomic_once_enter(&_mi_once); _mi_exec; (_mi_atomic_once_release(&_mi_once),_mi_exec=false)) + + + +/* ----------------------------------------------------------- + pthread thread locals +----------------------------------------------------------- */ + +#if MI_USE_PTHREADS + +#if defined(__APPLE__) && defined(__aarch64__) +#define MI_PTHREAD_KEY_INVALID ((pthread_key_t)(0)) // nicer codegen +#else +#define MI_PTHREAD_KEY_INVALID ((pthread_key_t)(-1)) #endif + +#if defined(__linux__) && defined(__GLIBC__) +// pthread_getspecific returns NULL for invalid keys. +// see also: +#define MI_PTHREADS_GET_INVALID_KEY_IS_NULL 1 #endif -#elif defined(__sun) -// Fallback for other archs -#include -static inline void mi_atomic_yield(void) { - smt_pause(); + +mi_decl_noinline bool _mi_pthread_key_create(pthread_key_t* pkey, void (*destruct)(void*), void* init); + +static inline void* mi_pthread_key_get(pthread_key_t key) { + #if !MI_PTHREADS_GET_INVALID_KEY_IS_NULL + if mi_unlikely(key==MI_PTHREAD_KEY_INVALID) return NULL; + #endif + return pthread_getspecific(key); } -#elif defined(__wasi__) -#include -static inline void mi_atomic_yield(void) { - sched_yield(); + +static inline bool mi_pthread_key_set(pthread_key_t* pkey, void* val) { + if mi_likely(*pkey!=MI_PTHREAD_KEY_INVALID) { pthread_setspecific(*pkey,val); return true; } + else if (val!=NULL) { return _mi_pthread_key_create(pkey,NULL,val); } + else return true; } -#else -#include -static inline void mi_atomic_yield(void) { - sleep(0); + +static inline void mi_pthread_key_delete(pthread_key_t* pkey) { + const pthread_key_t key = *pkey; + if (key!=MI_PTHREAD_KEY_INVALID) { + *pkey = MI_PTHREAD_KEY_INVALID; + pthread_key_delete(key); + } } #endif diff --git a/Include/internal/mimalloc/mimalloc/bits.h b/Include/internal/mimalloc/mimalloc/bits.h new file mode 100644 index 000000000000000..293dc0be05080c8 --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/bits.h @@ -0,0 +1,364 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2019-2024 Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +/* ---------------------------------------------------------------------------- + Bit operation, and platform dependent definition (MI_INTPTR_SIZE etc) +---------------------------------------------------------------------------- */ + +#pragma once +#ifndef MI_BITS_H +#define MI_BITS_H + +#include // size_t +#include // int64_t etc +#include // bool +#include // LONG_MAX + +#if defined(__cplusplus) +#define mi_decl_externc extern "C" +#define mi_init_struct_zero { } +#else +#define mi_decl_externc +#define mi_init_struct_zero { 0 } +#endif + +// ------------------------------------------------------ +// Size of a pointer. +// We assume that `sizeof(void*)==sizeof(intptr_t)` +// and it holds for all platforms we know of. +// +// However, the C standard only requires that: +// p == (void*)((intptr_t)p)) +// but we also need: +// i == (intptr_t)((void*)i) +// or otherwise one might define an intptr_t type that is larger than a pointer... +// ------------------------------------------------------ + +#if INTPTR_MAX > INT64_MAX +# define MI_INTPTR_SHIFT (4) // assume 128-bit (as on arm CHERI for example) +#elif INTPTR_MAX == INT64_MAX +# define MI_INTPTR_SHIFT (3) +#elif INTPTR_MAX == INT32_MAX +# define MI_INTPTR_SHIFT (2) +#else +#error platform pointers must be 32, 64, or 128 bits +#endif + +#if (INTPTR_MAX) > LONG_MAX +# define MI_PU(x) x##ULL +#else +# define MI_PU(x) x##UL +#endif + +#if SIZE_MAX == UINT64_MAX +# define MI_SIZE_SHIFT (3) +typedef int64_t mi_ssize_t; +#define MI_SSIZE_MAX INT64_MAX +#elif SIZE_MAX == UINT32_MAX +# define MI_SIZE_SHIFT (2) +typedef int32_t mi_ssize_t; +#define MI_SSIZE_MAX INT32_MAX +#else +#error platform objects must be 32 or 64 bits in size +#endif + +#if (SIZE_MAX/2) > LONG_MAX +# define MI_ZU(x) x##ULL +#else +# define MI_ZU(x) x##UL +#endif + +#define MI_INTPTR_SIZE (1< +#elif MI_ARCH_ARM64 && MI_OPT_SIMD +#include +#endif +#if defined(_MSC_VER) && (MI_ARCH_X64 || MI_ARCH_X86 || MI_ARCH_ARM64 || MI_ARCH_ARM32) +#include +#endif + +#if MI_ARCH_X64 && defined(__AVX2__) && !defined(__BMI2__) // avx2 implies bmi2 +#define __BMI2__ 1 +#endif +#if MI_ARCH_X64 && (defined(__AVX2__) || defined(__BMI2__) || defined(__BMI__)) && !defined(__BMI1__) // bmi2 implies bmi1 +#define __BMI1__ 1 +#endif +#if MI_ARCH_X64 && defined(__AVX2__) && !defined(__LZCNT__) // avx2 implies lzcnt +#define __LZCNT__ 1 +#endif + +// Define big endian if needed +// #define MI_BIG_ENDIAN 1 + +// maximum virtual address bits in a user-space pointer +#if MI_DEFAULT_VIRTUAL_ADDRESS_BITS > 0 +#define MI_MAX_VABITS MI_DEFAULT_VIRTUAL_ADDRESS_BITS +#elif MI_ARCH_X64 +#define MI_MAX_VABITS (47) +#elif MI_INTPTR_SIZE > 4 +#define MI_MAX_VABITS (48) +#else +#define MI_MAX_VABITS (32) +#endif + +// the MI_MIN_VABITS determine how many bits of the address space are always committed in the page_map +#if MI_MAX_VABITS <= 32 +#define MI_MIN_VABITS (32) +#elif MI_MAX_VABITS <= 42 +#define MI_MIN_VABITS MI_MAX_VABITS +#else +#define MI_MIN_VABITS (43) /* 8 TiB */ +#endif + +// use a flat page-map or a 2-level one +#ifndef MI_PAGE_MAP_FLAT +#if MI_MAX_VABITS <= 40 && !defined(__APPLE__) && !MI_SECURE && !MI_FREE_IS_CHECKED && MI_FREE_USE_PAGEMAP +#define MI_PAGE_MAP_FLAT 1 +#else +#define MI_PAGE_MAP_FLAT 0 +#endif +#endif + + +/* -------------------------------------------------------------------------------- + Builtin's +-------------------------------------------------------------------------------- */ + +#ifndef __has_builtin +#define __has_builtin(x) 0 +#endif + +#define mi_builtin(name) __builtin_##name +#define mi_has_builtin(name) __has_builtin(__builtin_##name) + +#if (LONG_MAX == INT32_MAX) +#define mi_builtin32(name) mi_builtin(name##l) +#define mi_has_builtin32(name) mi_has_builtin(name##l) +#else +#define mi_builtin32(name) mi_builtin(name) +#define mi_has_builtin32(name) mi_has_builtin(name) +#endif +#if (LONG_MAX == INT64_MAX) +#define mi_builtin64(name) mi_builtin(name##l) +#define mi_has_builtin64(name) mi_has_builtin(name##l) +#else +#define mi_builtin64(name) mi_builtin(name##ll) +#define mi_has_builtin64(name) mi_has_builtin(name##ll) +#endif + +#if (MI_SIZE_BITS == 32) +#define mi_builtinz(name) mi_builtin32(name) +#define mi_has_builtinz(name) mi_has_builtin32(name) +#define mi_msc_builtinz(name) name +#elif (MI_SIZE_BITS == 64) +#define mi_builtinz(name) mi_builtin64(name) +#define mi_has_builtinz(name) mi_has_builtin64(name) +#define mi_msc_builtinz(name) name##64 +#endif + +/* -------------------------------------------------------------------------------- + Popcount and count trailing/leading zero's +-------------------------------------------------------------------------------- */ + +size_t _mi_popcount_generic(size_t x); +extern bool _mi_cpu_has_popcnt; + +static inline size_t mi_popcount(size_t x) { + #if mi_has_builtinz(popcount) + return mi_builtinz(popcount)(x); + #elif defined(_MSC_VER) && (MI_ARCH_ARM64 || MI_ARCH_ARM32) + return mi_msc_builtinz(_CountOneBits)(x); + #elif defined(_MSC_VER) && (MI_ARCH_X64 || MI_ARCH_X86) + if (_mi_cpu_has_popcnt) { return mi_msc_builtinz(__popcnt)(x); } + else { return _mi_popcount_generic(x); } // see issue #1291 + #elif MI_ARCH_X64 && defined(__BMI1__) + return (size_t)_mm_popcnt_u64(x); + #else + #define MI_HAS_FAST_POPCOUNT 0 + return (x<=1 ? x : _mi_popcount_generic(x)); + #endif +} + +#ifndef MI_HAS_FAST_POPCOUNT +#define MI_HAS_FAST_POPCOUNT 1 +#endif + + + +size_t _mi_clz_generic(size_t x); +size_t _mi_ctz_generic(size_t x); + +static inline size_t mi_ctz(size_t x) { + #if defined(__GNUC__) && MI_ARCH_X64 && defined(__BMI1__) // on x64 tzcnt is defined for 0 + size_t r; + __asm ("tzcnt\t%1, %0" : "=r"(r) : "r"(x) : "cc"); + return r; + #elif defined(_MSC_VER) && MI_ARCH_X64 && defined(__BMI1__) + return _tzcnt_u64(x); + #elif defined(_MSC_VER) && (MI_ARCH_X64 || MI_ARCH_X86 || MI_ARCH_ARM64 || MI_ARCH_ARM32) + unsigned long idx; + return (mi_msc_builtinz(_BitScanForward)(&idx, x) ? (size_t)idx : MI_SIZE_BITS); + #elif mi_has_builtinz(ctz) + return (x!=0 ? (size_t)mi_builtinz(ctz)(x) : MI_SIZE_BITS); + #elif defined(__GNUC__) && (MI_ARCH_X64 || MI_ARCH_X86) + size_t r = MI_SIZE_BITS; // bsf leaves destination unmodified if the argument is 0 (see ) + __asm ("bsf\t%1, %0" : "+r"(r) : "r"(x) : "cc"); + return r; + #elif MI_HAS_FAST_POPCOUNT + return (x!=0 ? (mi_popcount(x^(x-1))-1) : MI_SIZE_BITS); + #else + #define MI_HAS_FAST_BITSCAN 0 + return (x!=0 ? _mi_ctz_generic(x) : MI_SIZE_BITS); + #endif +} + +static inline size_t mi_clz(size_t x) { + #if defined(__GNUC__) && MI_ARCH_X64 && defined(__LZCNT__) // on x64 lzcnt is defined for 0 + size_t r; + __asm ("lzcnt\t%1, %0" : "=r"(r) : "r"(x) : "cc"); + return r; + #elif defined(_MSC_VER) && MI_ARCH_X64 && defined(__LZCNT__) + return _lzcnt_u64(x); + #elif defined(_MSC_VER) && (MI_ARCH_X64 || MI_ARCH_X86 || MI_ARCH_ARM64 || MI_ARCH_ARM32) + unsigned long idx; + return (mi_msc_builtinz(_BitScanReverse)(&idx, x) ? MI_SIZE_BITS - 1 - (size_t)idx : MI_SIZE_BITS); + #elif mi_has_builtinz(clz) + return (x!=0 ? (size_t)mi_builtinz(clz)(x) : MI_SIZE_BITS); + #elif defined(__GNUC__) && (MI_ARCH_X64 || MI_ARCH_X86) + if (x==0) return MI_SIZE_BITS; + size_t r; + __asm ("bsr\t%1, %0" : "=r"(r) : "r"(x) : "cc"); + return (MI_SIZE_BITS - 1 - r); + #else + #define MI_HAS_FAST_BITSCAN 0 + return (x!=0 ? _mi_clz_generic(x) : MI_SIZE_BITS); + #endif +} + +#ifndef MI_HAS_FAST_BITSCAN +#define MI_HAS_FAST_BITSCAN 1 +#endif + +/* -------------------------------------------------------------------------------- + find trailing/leading zero (bit scan forward/reverse) +-------------------------------------------------------------------------------- */ + +// Bit scan forward: find the least significant bit that is set (i.e. count trailing zero's) +// return false if `x==0` (with `*idx` undefined) and true otherwise, +// with the `idx` is set to the bit index (`0 <= *idx < MI_BFIELD_BITS`). +static inline bool mi_bsf(size_t x, size_t* idx) { + #if defined(__GNUC__) && MI_ARCH_X64 && defined(__BMI1__) && (!defined(__clang_major__) || __clang_major__ >= 9) + // on x64 the carry flag is set on zero which gives better codegen + bool is_zero; + __asm ( "tzcnt\t%2, %1" : "=@ccc"(is_zero), "=r"(*idx) : "r"(x) : "cc" ); + return !is_zero; + #elif 0 && defined(_MSC_VER) && (MI_ARCH_X64 || MI_ARCH_X86 || MI_ARCH_ARM64 || MI_ARCH_ARM32) + unsigned long i; + return (mi_msc_builtinz(_BitScanForward)(&i, x) ? (*idx = (size_t)i, true) : false); + #else + return (x!=0 ? (*idx = mi_ctz(x), true) : false); + #endif +} + +// Bit scan reverse: find the most significant bit that is set +// return false if `x==0` (with `*idx` undefined) and true otherwise, +// with the `idx` is set to the bit index (`0 <= *idx < MI_BFIELD_BITS`). +static inline bool mi_bsr(size_t x, size_t* idx) { + #if 0 && defined(_MSC_VER) && (MI_ARCH_X64 || MI_ARCH_X86 || MI_ARCH_ARM64 || MI_ARCH_ARM32) + unsigned long i; + return (mi_msc_builtinz(_BitScanReverse)(&i, x) ? (*idx = (size_t)i, true) : false); + #else + return (x!=0 ? (*idx = MI_SIZE_BITS - 1 - mi_clz(x), true) : false); + #endif +} + + +/* -------------------------------------------------------------------------------- + rotate +-------------------------------------------------------------------------------- */ + +static inline size_t mi_rotr(size_t x, size_t r) { + #if (mi_has_builtin(rotateright64) && MI_SIZE_BITS==64) + return mi_builtin(rotateright64)(x,r); + #elif (mi_has_builtin(rotateright32) && MI_SIZE_BITS==32) + return mi_builtin(rotateright32)(x,r); + #elif defined(_MSC_VER) && (MI_ARCH_X64 || MI_ARCH_ARM64) + return _rotr64(x, (int)r); + #elif defined(_MSC_VER) && (MI_ARCH_X86 || MI_ARCH_ARM32) + return _lrotr(x,(int)r); + #else + // The term `(-rshift)&(BITS-1)` is written instead of `BITS - rshift` to + // avoid UB when `rshift==0`. See + const unsigned int rshift = (unsigned int)(r) & (MI_SIZE_BITS-1); + return ((x >> rshift) | (x << ((-rshift) & (MI_SIZE_BITS-1)))); + #endif +} + +static inline size_t mi_rotl(size_t x, size_t r) { + #if (mi_has_builtin(rotateleft64) && MI_SIZE_BITS==64) + return mi_builtin(rotateleft64)(x,r); + #elif (mi_has_builtin(rotateleft32) && MI_SIZE_BITS==32) + return mi_builtin(rotateleft32)(x,r); + #elif defined(_MSC_VER) && (MI_ARCH_X64 || MI_ARCH_ARM64) + return _rotl64(x, (int)r); + #elif defined(_MSC_VER) && (MI_ARCH_X86 || MI_ARCH_ARM32) + return _lrotl(x, (int)r); + #else + // The term `(-rshift)&(BITS-1)` is written instead of `BITS - rshift` to + // avoid UB when `rshift==0`. See + const unsigned int rshift = (unsigned int)(r) & (MI_SIZE_BITS-1); + return ((x << rshift) | (x >> ((-rshift) & (MI_SIZE_BITS-1)))); + #endif +} + +static inline uint32_t mi_rotl32(uint32_t x, uint32_t r) { + #if mi_has_builtin(rotateleft32) + return mi_builtin(rotateleft32)(x,r); + #elif defined(_MSC_VER) && (MI_ARCH_X64 || MI_ARCH_X86 || MI_ARCH_ARM64 || MI_ARCH_ARM32) + return _lrotl(x, (int)r); + #else + // The term `(-rshift)&(BITS-1)` is written instead of `BITS - rshift` to + // avoid UB when `rshift==0`. See + const unsigned int rshift = (unsigned int)(r) & 31; + return ((x << rshift) | (x >> ((-rshift) & 31))); + #endif +} + + +#endif // MI_BITS_H diff --git a/Include/internal/mimalloc/mimalloc/decl.h b/Include/internal/mimalloc/mimalloc/decl.h new file mode 100644 index 000000000000000..3be0aed561dd360 --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/decl.h @@ -0,0 +1,172 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2019-2026 Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +/* ---------------------------------------------------------------------------- + Compiler dependent definitions +---------------------------------------------------------------------------- */ + +#pragma once +#ifndef MI_DECL_H +#define MI_DECL_H + +// -------------------------------------------------------------------------- +// Compiler defines +// -------------------------------------------------------------------------- + +#define mi_decl_cache_align mi_decl_align(64) + +#if defined(_MSC_VER) +#pragma warning(disable:4127) // suppress constant conditional warning (due to MI_SECURE paths) +#pragma warning(disable:26812) // unscoped enum warning +#define mi_decl_forceinline __forceinline +#define mi_decl_noinline __declspec(noinline) +#define mi_decl_thread __declspec(thread) +#define mi_decl_noreturn __declspec(noreturn) +#define mi_decl_weak +#define mi_decl_hidden +#define mi_decl_cold +#define mi_assume_aligned(p,sz) (p) +#elif (defined(__GNUC__) && (__GNUC__ >= 3)) || defined(__clang__) // includes clang and icc +#if !MI_TRACK_ASAN +#define mi_decl_forceinline __attribute__((always_inline)) inline +#else +#define mi_decl_forceinline inline +#endif +#define mi_decl_noinline __attribute__((noinline)) +#define mi_decl_thread __thread +#define mi_decl_noreturn __attribute__((noreturn)) +#define mi_decl_weak __attribute__((weak)) +#if defined(__MINGW32__) || defined(__CYGWIN__) +#define mi_decl_hidden +#else +#define mi_decl_hidden __attribute__((visibility("hidden"))) +#endif +#if (defined(__GNUC__) && (__GNUC__ >= 4)) || defined(__clang__) +#define mi_decl_cold __attribute__((cold)) +#define mi_assume_aligned(p,sz) __builtin_assume_aligned(p,sz) +#else +#define mi_decl_cold +#define mi_assume_aligned(p,sz) (p) +#endif +#elif __cplusplus >= 201103L // c++11 +#define mi_decl_forceinline inline +#define mi_decl_noinline +#define mi_decl_thread thread_local +#define mi_decl_noreturn [[noreturn]] +#define mi_decl_weak +#define mi_decl_hidden +#define mi_decl_cold +#define mi_assume_aligned(p,sz) (p) +#else +#define mi_decl_forceinline inline +#define mi_decl_noinline +#define mi_decl_thread __thread // hope for the best :-) +#define mi_decl_noreturn +#define mi_decl_weak +#define mi_decl_hidden +#define mi_decl_cold +#define mi_assume_aligned(p,sz) (p) +#endif + +#if defined(__GNUC__) || defined(__clang__) +#define mi_unlikely(x) (__builtin_expect(!!(x),false)) +#define mi_likely(x) (__builtin_expect(!!(x),true)) +#elif (defined(__cplusplus) && (__cplusplus >= 202002L)) || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) +#define mi_unlikely(x) (x) [[unlikely]] +#define mi_likely(x) (x) [[likely]] +#else +#define mi_unlikely(x) (x) +#define mi_likely(x) (x) +#endif + +#if (defined(__GNUC__) && (__GNUC__ >= 7)) || defined(__clang__) +#define mi_decl_maybe_unused __attribute__((unused)) +#elif __cplusplus >= 201703L // c++17 +#define mi_decl_maybe_unused [[maybe_unused]] +#else +#define mi_decl_maybe_unused +#endif + +#ifndef __has_builtin +#define __has_builtin(x) 0 +#endif + +#if defined(__EMSCRIPTEN__) && !defined(__wasi__) +#define __wasi__ +#endif + +#define MI_UNUSED(x) (void)(x) +#if (MI_DEBUG>1) +#define MI_UNUSED_RELEASE(x) +#else +#define MI_UNUSED_RELEASE(x) MI_UNUSED(x) +#endif + +// ------------------------------------------------------ +// Assertions and tracing +// ------------------------------------------------------ + +#if (MI_DEBUG>0) +void _mi_trace_message(const char* fmt, ...); +#define mi_trace_message(...) _mi_trace_message(__VA_ARGS__) +#else +#define mi_trace_message(...) +#endif + + +#if (MI_DEBUG) +// use our own assertion to print without memory allocation +mi_decl_noreturn mi_decl_cold void _mi_assert_fail(const char* assertion, const char* fname, unsigned int line, const char* func) mi_attr_noexcept; +#define mi_assert(expr) ((expr) ? (void)0 : _mi_assert_fail(#expr,__FILE__,__LINE__,__func__)) +#else +#define mi_assert(x) +#endif + +#if (MI_DEBUG>1) +#define mi_assert_internal mi_assert +#else +#define mi_assert_internal(x) +#endif + +#if (MI_DEBUG>2) +#define mi_assert_expensive mi_assert +#else +#define mi_assert_expensive(x) +#endif + + +// ------------------------------------------------------ +// Initialization macros +// ------------------------------------------------------ + +#define MI_INIT4(x) x(),x(),x(),x() +#define MI_INIT8(x) MI_INIT4(x),MI_INIT4(x) +#define MI_INIT16(x) MI_INIT8(x),MI_INIT8(x) +#define MI_INIT32(x) MI_INIT16(x),MI_INIT16(x) +#define MI_INIT64(x) MI_INIT32(x),MI_INIT32(x) +#define MI_INIT128(x) MI_INIT64(x),MI_INIT64(x) +#define MI_INIT256(x) MI_INIT128(x),MI_INIT128(x) + +#define MI_INIT74(x) MI_INIT64(x),MI_INIT8(x),x(),x() +#define MI_INIT5(x) MI_INIT4(x),x() +#define MI_INIT6(x) MI_INIT4(x),x(),x() + +// ------------------------------------------------------ +// Interposing for macOS (DYLD) +// ------------------------------------------------------ + +#if defined(__APPLE__) && defined(__MACH__) +struct mi_interpose_s { + const void* replacement; + const void* target; +}; +#define MI_INTERPOSE_FUN(oldfun,newfun) { (const void*)&newfun, (const void*)&oldfun } +#define MI_INTERPOSE_MI(fun) MI_INTERPOSE_FUN(fun,mi_##fun) +#define MI_INTERPOSE_DECLS(name) __attribute__((used)) static const struct mi_interpose_s name[] __attribute__((section("__DATA, __interpose"))) +#endif + +#endif // MI_DECL_H diff --git a/Include/internal/mimalloc/mimalloc/heap/heap.h b/Include/internal/mimalloc/mimalloc/heap/heap.h new file mode 100644 index 000000000000000..0a8e94494265279 --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/heap/heap.h @@ -0,0 +1,48 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_HEAP_H +#define MI_INTERNAL_HEAP_H + +#include "../types.h" +#include "init.h" +#include "subproc.h" + +void _mi_heap_init(mi_heap_t* heap, mi_thread_local_t theap, mi_subproc_t* subproc, mi_arena_id_t exclusive_arena_id, bool use_qsbr); +mi_decl_cold mi_theap_t* _mi_heap_theap_get_or_init(const mi_heap_t* heap); // get (and possible create) the theap belonging to a heap +void _mi_heap_force_destroy(mi_heap_t* heap, bool acquire_heaps_lock); // allow destroying the main heap +mi_heap_t* _mi_heap_new_for_subproc(mi_subproc_t* subproc, mi_arena_id_t exclusive_arena_id, bool is_heap_main, bool use_qsbr); +bool _mi_heap_theap_set(mi_heap_t* heap, mi_theap_t* theap); + +static inline bool mi_heap_qsbr_is_enabled(const mi_heap_t* heap) { +#if MI_QSBR + return heap->qsbr_enabled; +#else + MI_UNUSED(heap); + return false; +#endif +} + +static inline long mi_heap_reclaim_on_free(mi_heap_t* heap) { + return heap->reclaim_on_free; +} + +static inline mi_heap_t* mi_heap_get_heap_main(const mi_heap_t* heap) { + return _mi_subproc_heap_main(heap->subproc); +} + +static inline bool mi_is_heap_main(const mi_heap_t* heap) { + mi_assert_internal(heap!=NULL); + return (mi_heap_get_heap_main(heap) == heap); +} + +static inline bool mi_is_process_heap_main(const mi_heap_t* heap) { + mi_assert_internal(heap!=NULL); + return (_mi_subproc_main()->heap_main == heap); +} + +#endif diff --git a/Include/internal/mimalloc/mimalloc/heap/init.h b/Include/internal/mimalloc/mimalloc/heap/init.h new file mode 100644 index 000000000000000..3f2c6506c66b2e7 --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/heap/init.h @@ -0,0 +1,27 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_INIT_H +#define MI_INTERNAL_INIT_H + +#include "../types.h" + +mi_heap_t* _mi_subproc_heap_main(mi_subproc_t* subproc); +mi_theap_t* _mi_theap_main(void); // current thread's backing theap, without initialization +mi_page_t* _mi_page_empty_get(void); +void _mi_auto_process_init(void); +void mi_cdecl _mi_auto_process_done(void) mi_attr_noexcept; +bool _mi_preloading(void); // true while the C runtime is not initialized yet +void _mi_thread_done(mi_theap_t* theap); +mi_theap_t* _mi_thread_init(void); +mi_theap_t* _mi_thread_init_with_heap(mi_heap_t* heap); +bool _mi_is_empty_theap(const mi_theap_t* theap); + +extern mi_decl_hidden const mi_theap_t _mi_theap_empty; // read-only empty theap, initial value of the thread local default theap (in the MI_TLS_MODEL_LOCAL) +extern mi_decl_hidden mi_theap_t _mi_theap_empty_wrong; // read-only empty theap used to signal that a theap for a heap could not be allocated + +#endif diff --git a/Include/internal/mimalloc/mimalloc/heap/page-map.h b/Include/internal/mimalloc/mimalloc/heap/page-map.h new file mode 100644 index 000000000000000..fd26187ac8adb22 --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/heap/page-map.h @@ -0,0 +1,173 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_PAGE_MAP_H +#define MI_INTERNAL_PAGE_MAP_H + +#include "../types.h" +#include "../util/libc.h" +#include "../util/options.h" + +bool _mi_page_map_init(void); +mi_decl_nodiscard bool _mi_page_map_register(mi_page_t* page); +void _mi_page_map_unregister(mi_page_t* page); +void _mi_page_map_unregister_range(void* start, size_t size); +void _mi_page_map_unsafe_destroy(void); +mi_page_t* _mi_safe_ptr_page(const void* p); + + +/* ----------------------------------------------------------- + The page map maps addresses to `mi_page_t` pointers +----------------------------------------------------------- */ + +#if MI_PAGE_MAP_FLAT + +// flat page-map committed on demand, using one byte per slice (64 KiB). +// single indirection and low commit, but large initial virtual reserve (4 GiB with 48 bit virtual addresses) +// used by default on <= 40 bit virtual address spaces. +extern mi_decl_hidden _Atomic(uint8_t*) _mi_page_map; +extern mi_decl_hidden _Atomic(void*) _mi_page_map_max_address; + +static inline size_t mi_page_map_index(const void* p) { + return (size_t)((uintptr_t)p >> MI_ARENA_SLICE_SHIFT); +} + +static inline uint8_t mi_page_map_at(size_t idx) { + return mi_atomic_load_ptr_relaxed(uint8_t,&_mi_page_map)[idx]; +} + +static inline mi_page_t* mi_ptr_page_ex(const void* p, bool* valid) { + const size_t idx = mi_page_map_index(p); + const size_t ofs = mi_page_map_at(idx); + if (valid != NULL) { *valid = (ofs != 0); } + return (mi_page_t*)((((uintptr_t)p >> MI_ARENA_SLICE_SHIFT) + 1 - ofs) << MI_ARENA_SLICE_SHIFT); +} + +static inline mi_page_t* mi_checked_ptr_page(const void* p) { + #if MI_MIN_VABITS < MI_INTPTR_BITS + if mi_unlikely(((uintptr_t)p >> MI_MIN_VABITS) != 0) { + if (p > mi_atomic_load_ptr_relaxed(void, &_mi_page_map_max_address)) return NULL; + } + #endif + bool valid; + mi_page_t* const page = mi_ptr_page_ex(p, &valid); + return (valid ? page : NULL); +} + +static inline mi_page_t* mi_unchecked_ptr_page(const void* p) { + return mi_ptr_page_ex(p, NULL); +} + +#else + +// 2-level page map: +// double indirection, but low commit and low virtual reserve. +// +// the page-map is usually 4 MiB (for 48 bit virtual addresses) and points to sub maps of 64 KiB. +// the page-map is committed on-demand (in 64 KiB parts) (and sub-maps are committed on-demand as well) +// one sub page-map = 64 KiB => covers 2^(16-3) * 2^16 = 2^29 = 512 MiB address space +// the page-map needs 48-(16+13) = 19 bits => 2^19 sub map pointers = 2^22 bytes = 4 MiB reserved size. +#define MI_PAGE_MAP_SUB_SHIFT (13) +#define MI_PAGE_MAP_SUB_COUNT (MI_ZU(1) << MI_PAGE_MAP_SUB_SHIFT) +#define MI_PAGE_MAP_SHIFT (MI_MAX_VABITS - MI_PAGE_MAP_SUB_SHIFT - MI_ARENA_SLICE_SHIFT) + +typedef mi_page_t** mi_submap_t; +typedef struct mi_page_map_s { + _Atomic(size_t) committed_count; // currently committed entries + size_t reserved_size; // full reserved size (mi_page_map_t + submaps) + mi_memid_t memid; // provenance + mi_lock_t lock; // used when allocating new submaps + _Atomic(mi_submap_t) submaps[1]; +} mi_page_map_t; + +extern mi_decl_hidden _Atomic(mi_page_map_t*) __mi_page_map; + +static inline size_t mi_page_map_index(const void* p, size_t* sub_idx) { + const size_t u = (size_t)((uintptr_t)p / MI_ARENA_SLICE_SIZE); + if (sub_idx != NULL) { *sub_idx = u % MI_PAGE_MAP_SUB_COUNT; } + return (u / MI_PAGE_MAP_SUB_COUNT); +} + +static inline mi_page_map_t* mi_page_map(void) { + return mi_atomic_load_ptr_relaxed(mi_page_map_t,&__mi_page_map); +} + +static inline mi_submap_t mi_page_map_at(const mi_page_map_t* pmap, size_t idx) { + return mi_atomic_load_ptr_acquire(mi_page_t*, &pmap->submaps[idx]); +} + +static inline mi_page_t* mi_unchecked_ptr_page(const void* p) { + const mi_page_map_t* pmap = mi_page_map(); + size_t sub_idx; + const size_t idx = mi_page_map_index(p, &sub_idx); + return mi_page_map_at(pmap,idx)[sub_idx]; // NULL if p==NULL +} + +static inline mi_page_t* mi_checked_ptr_page(const void* p) { + const mi_page_map_t* pmap = mi_page_map(); + size_t sub_idx; + const size_t idx = mi_page_map_index(p, &sub_idx); + const size_t committed_count = mi_atomic_load_relaxed(&pmap->committed_count); + if mi_unlikely(idx >= committed_count) return NULL; + // #if MI_MIN_VABITS < MI_INTPTR_BITS // is still invalid if free is called before the pagemap is initialized + // if mi_unlikely(((uintptr_t)p >> MI_MIN_VABITS) != 0) { + // const size_t committed_count = mi_atomic_load_relaxed(&pmap->committed_count); + // if mi_unlikely(idx >= committed_count) return NULL; + // } + // #endif + mi_submap_t const sub = mi_page_map_at(pmap,idx); + if mi_unlikely(sub == NULL) return NULL; + return sub[sub_idx]; +} + +#endif + +#if MI_PAGE_META_IS_ALIGNED +// if the page meta data is aligned in front of pages we can find it efficiently +// without needing to go through the page map (for valid pointers). +static inline mi_page_t* mi_aligned_ptr_page0(const void* p) { + mi_page_t* const page_metas = (mi_page_t*)mi_align_down_ptr(p,MI_PAGE_META_ALIGNMENT); + // const ptrdiff_t page_idx = ((uint8_t*)p - (uint8_t*)page_metas)/MI_ARENA_SLICE_SIZE; + const uintptr_t page_idx = ((uintptr_t)p / MI_ARENA_SLICE_SIZE) % (MI_PAGE_META_ALIGNMENT / MI_ARENA_SLICE_SIZE); + mi_assert_internal(page_idx <= MI_PAGE_META_ALIGNED_COUNT); + #if MI_ARCH_X64 || MI_ARCH_X86 || MI_ARCH_RISCV // better code on x64/x86/riscv64 + mi_page_t* const page = (mi_page_t*)((uintptr_t)page_metas | (page_idx * sizeof(mi_page_t))); + #else + mi_page_t* const page = &page_metas[page_idx]; + #endif + return page; + +} + +static inline mi_page_t* mi_aligned_ptr_page(const void* p) { + mi_page_t* const page = mi_aligned_ptr_page0(p); + if mi_unlikely(page==NULL) return NULL; + #if MI_DEBUG + mi_page_t* const cpage = mi_checked_ptr_page(p); + if mi_unlikely(cpage==NULL) { + _mi_error_message(EINVAL, "mi_aligned_ptr_page: invalid pointer: %p\n", p); + return NULL; + } + #endif + return mi_atomic_load_ptr_acquire(mi_page_t, &page->self); +} +#endif + +static inline mi_page_t* mi_ptr_page(const void* p) { + mi_assert_internal(p==NULL || mi_is_in_heap_region(p)); + #if MI_SECURE || MI_FREE_IS_CHECKED + return mi_checked_ptr_page(p); + #elif MI_PAGE_META_IS_ALIGNED + return mi_aligned_ptr_page(p); + #elif MI_DEBUG + return mi_checked_ptr_page(p); + #else + return mi_unchecked_ptr_page(p); + #endif +} + +#endif diff --git a/Include/internal/mimalloc/mimalloc/heap/subproc.h b/Include/internal/mimalloc/mimalloc/heap/subproc.h new file mode 100644 index 000000000000000..0b37a5dfb4d8d0e --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/heap/subproc.h @@ -0,0 +1,29 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_SUBPROC_H +#define MI_INTERNAL_SUBPROC_H + +#include "../types.h" + +mi_subproc_t* _mi_subproc_main_init(void); +void _mi_subproc_main_done(void); +mi_subproc_t* _mi_subproc_main(void); +bool _mi_subproc_is_main(mi_subproc_t* subproc); +mi_subproc_t* _mi_subproc(void); // current subproc of this thread +mi_subproc_t* _mi_subproc_from_id(mi_subproc_id_t subproc_id); +void _mi_subprocs_unsafe_destroy_all(void); +void _mi_fork_init(void); +void _mi_fork_child(void); + +void* _mi_meta_zalloc( mi_subproc_t* subproc, size_t size, mi_memid_t* memid ); +void* _mi_meta_rezalloc( mi_subproc_t* subproc, void* p, size_t newsize, mi_memid_t* memid ); +void* _mi_meta_zalloc_aligned( mi_subproc_t* subproc, size_t size, size_t alignment, mi_memid_t* memid ); +void _mi_meta_free(mi_subproc_t* subproc, void* p, mi_memid_t memid); +bool _mi_meta_is_meta_page(const mi_subproc_t* subproc, const mi_page_t* p); + +#endif diff --git a/Include/internal/mimalloc/mimalloc/heap/theap.h b/Include/internal/mimalloc/mimalloc/heap/theap.h new file mode 100644 index 000000000000000..86f42b8a82e252e --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/heap/theap.h @@ -0,0 +1,65 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_THEAP_H +#define MI_INTERNAL_THEAP_H + +#include "../types.h" + +size_t _mi_theap_random_next(mi_theap_t* theap); +void _mi_theap_init(mi_theap_t* theap, mi_heap_t* heap, mi_tld_t* tld); +mi_theap_t* _mi_theap_alloc(mi_heap_t* heap, mi_tld_t* tld); +mi_theap_t* _mi_theap_create(mi_heap_t* heap, mi_tld_t* tld); +void _mi_theap_collect_abandon(mi_theap_t* theap); +void _mi_theap_abandon_owned_pages(mi_theap_t* theap); +bool _mi_theap_area_visit_blocks(const mi_heap_area_t* area, mi_page_t* page, mi_block_visit_fun* visitor, void* arg); + +void _mi_heap_detach_theaps( mi_heap_t* heap ); +void _mi_tld_detach_theaps( mi_tld_t* tld ); +void _mi_theap_incref(mi_theap_t* theap); +void _mi_theap_decref(mi_theap_t* theap); +void _mi_theap_merge_stats(mi_theap_t* theap); + +void _mi_heap_area_init(mi_heap_area_t* area, mi_page_t* page); + +/*---------------------------------------------------------------------------------------- + Heap functions +------------------------------------------------------------------------------------------- */ + +static inline mi_heap_t* mi_theap_heap_peek(const mi_theap_t* theap) { + mi_assert_internal(theap!=NULL); + return mi_atomic_load_ptr_relaxed(mi_heap_t,&theap->heap); +} + +static inline mi_heap_t* mi_theap_heap(const mi_theap_t* theap) { + mi_heap_t* const heap = mi_theap_heap_peek(theap); + mi_assert_internal(heap!=NULL); + return heap; +} + +static inline bool mi_theap_is_initialized(const mi_theap_t* theap) { + return (theap != NULL && mi_theap_heap_peek(theap) != NULL); +} + +static inline mi_subproc_t* mi_theap_subproc(const mi_theap_t* theap) { + mi_subproc_t* const subproc = mi_atomic_load_ptr_relaxed(mi_subproc_t,&theap->subproc); + mi_assert_internal(!mi_theap_is_initialized(theap) || mi_theap_heap(theap)->subproc == subproc); + return subproc; +} + +static inline mi_page_t* mi_theap_get_free_small_page(mi_theap_t* theap, size_t xsize, bool is_wsize) { + mi_assert_internal(is_wsize ? xsize <= (MI_SMALL_WSIZE_MAX + MI_PADDING_WSIZE) : xsize <= (MI_SMALL_SIZE_MAX + MI_PADDING_SIZE)); + const size_t idx = (is_wsize ? xsize : mi_wsize_from_size(xsize)); + mi_assert_internal(idx < MI_PAGES_DIRECT); + return theap->pages_free_direct[idx]; +} + +static inline bool mi_theap_is_detached(mi_theap_t* theap) { + return (theap!=NULL && theap->tld->thread_id == MI_THREADID_DETACHED); +} + +#endif diff --git a/Include/internal/mimalloc/mimalloc/internal.h b/Include/internal/mimalloc/mimalloc/internal.h index 423693f33e1a65a..2f1ecf489edcebd 100644 --- a/Include/internal/mimalloc/mimalloc/internal.h +++ b/Include/internal/mimalloc/mimalloc/internal.h @@ -1,965 +1,50 @@ /* ---------------------------------------------------------------------------- -Copyright (c) 2018-2023, Microsoft Research, Daan Leijen +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen This is free software; you can redistribute it and/or modify it under the terms of the MIT license. A copy of the license can be found in the file "LICENSE" at the root of this distribution. -----------------------------------------------------------------------------*/ #pragma once -#ifndef MIMALLOC_INTERNAL_H -#define MIMALLOC_INTERNAL_H - +#ifndef MI_INTERNAL_H +#define MI_INTERNAL_H // -------------------------------------------------------------------------- -// This file contains the internal API's of mimalloc and various utility -// functions and macros. +// This file includes all the internal API's of mimalloc for convenience. +// (but is not used by mimalloc itself) // -------------------------------------------------------------------------- #include "types.h" #include "track.h" -#if (MI_DEBUG>0) -#define mi_trace_message(...) _mi_trace_message(__VA_ARGS__) -#else -#define mi_trace_message(...) -#endif - -#if defined(__cplusplus) -#define mi_decl_externc extern "C" -#else -#define mi_decl_externc -#endif - -// pthreads -#if !defined(_WIN32) && !defined(__wasm__) -#define MI_USE_PTHREADS -#include -#endif - -// "options.c" -void _mi_fputs(mi_output_fun* out, void* arg, const char* prefix, const char* message); -void _mi_fprintf(mi_output_fun* out, void* arg, const char* fmt, ...); -void _mi_warning_message(const char* fmt, ...); -void _mi_verbose_message(const char* fmt, ...); -void _mi_trace_message(const char* fmt, ...); -void _mi_options_init(void); -void _mi_error_message(int err, const char* fmt, ...); - -// random.c -void _mi_random_init(mi_random_ctx_t* ctx); -void _mi_random_init_weak(mi_random_ctx_t* ctx); -void _mi_random_reinit_if_weak(mi_random_ctx_t * ctx); -void _mi_random_split(mi_random_ctx_t* ctx, mi_random_ctx_t* new_ctx); -uintptr_t _mi_random_next(mi_random_ctx_t* ctx); -uintptr_t _mi_heap_random_next(mi_heap_t* heap); -uintptr_t _mi_os_random_weak(uintptr_t extra_seed); -static inline uintptr_t _mi_random_shuffle(uintptr_t x); - -// init.c -extern mi_decl_cache_align mi_stats_t _mi_stats_main; -extern mi_decl_cache_align const mi_page_t _mi_page_empty; -bool _mi_is_main_thread(void); -size_t _mi_current_thread_count(void); -bool _mi_preloading(void); // true while the C runtime is not initialized yet -mi_threadid_t _mi_thread_id(void) mi_attr_noexcept; -mi_heap_t* _mi_heap_main_get(void); // statically allocated main backing heap -void _mi_thread_done(mi_heap_t* heap); -void _mi_thread_data_collect(void); -void _mi_tld_init(mi_tld_t* tld, mi_heap_t* bheap); - -// os.c -void _mi_os_init(void); // called from process init -void* _mi_os_alloc(size_t size, mi_memid_t* memid, mi_stats_t* stats); -void _mi_os_free(void* p, size_t size, mi_memid_t memid, mi_stats_t* stats); -void _mi_os_free_ex(void* p, size_t size, bool still_committed, mi_memid_t memid, mi_stats_t* stats); - -size_t _mi_os_page_size(void); -size_t _mi_os_good_alloc_size(size_t size); -bool _mi_os_has_overcommit(void); -bool _mi_os_has_virtual_reserve(void); - -bool _mi_os_purge(void* p, size_t size, mi_stats_t* stats); -bool _mi_os_reset(void* addr, size_t size, mi_stats_t* tld_stats); -bool _mi_os_commit(void* p, size_t size, bool* is_zero, mi_stats_t* stats); -bool _mi_os_decommit(void* addr, size_t size, mi_stats_t* stats); -bool _mi_os_protect(void* addr, size_t size); -bool _mi_os_unprotect(void* addr, size_t size); -bool _mi_os_purge(void* p, size_t size, mi_stats_t* stats); -bool _mi_os_purge_ex(void* p, size_t size, bool allow_reset, mi_stats_t* stats); - -void* _mi_os_alloc_aligned(size_t size, size_t alignment, bool commit, bool allow_large, mi_memid_t* memid, mi_stats_t* stats); -void* _mi_os_alloc_aligned_at_offset(size_t size, size_t alignment, size_t align_offset, bool commit, bool allow_large, mi_memid_t* memid, mi_stats_t* tld_stats); - -void* _mi_os_get_aligned_hint(size_t try_alignment, size_t size); -bool _mi_os_use_large_page(size_t size, size_t alignment); -size_t _mi_os_large_page_size(void); - -void* _mi_os_alloc_huge_os_pages(size_t pages, int numa_node, mi_msecs_t max_secs, size_t* pages_reserved, size_t* psize, mi_memid_t* memid); - -// arena.c -mi_arena_id_t _mi_arena_id_none(void); -void _mi_arena_free(void* p, size_t size, size_t still_committed_size, mi_memid_t memid, mi_stats_t* stats); -void* _mi_arena_alloc(size_t size, bool commit, bool allow_large, mi_arena_id_t req_arena_id, mi_memid_t* memid, mi_os_tld_t* tld); -void* _mi_arena_alloc_aligned(size_t size, size_t alignment, size_t align_offset, bool commit, bool allow_large, mi_arena_id_t req_arena_id, mi_memid_t* memid, mi_os_tld_t* tld); -bool _mi_arena_memid_is_suitable(mi_memid_t memid, mi_arena_id_t request_arena_id); -bool _mi_arena_contains(const void* p); -void _mi_arena_collect(bool force_purge, mi_stats_t* stats); -void _mi_arena_unsafe_destroy_all(mi_stats_t* stats); - -// "segment-map.c" -void _mi_segment_map_allocated_at(const mi_segment_t* segment); -void _mi_segment_map_freed_at(const mi_segment_t* segment); - -// "segment.c" -extern mi_abandoned_pool_t _mi_abandoned_default; // global abandoned pool -mi_page_t* _mi_segment_page_alloc(mi_heap_t* heap, size_t block_size, size_t page_alignment, mi_segments_tld_t* tld, mi_os_tld_t* os_tld); -void _mi_segment_page_free(mi_page_t* page, bool force, mi_segments_tld_t* tld); -void _mi_segment_page_abandon(mi_page_t* page, mi_segments_tld_t* tld); -bool _mi_segment_try_reclaim_abandoned( mi_heap_t* heap, bool try_all, mi_segments_tld_t* tld); -void _mi_segment_thread_collect(mi_segments_tld_t* tld); -bool _mi_abandoned_pool_visit_blocks(mi_abandoned_pool_t* pool, uint8_t page_tag, bool visit_blocks, mi_block_visit_fun* visitor, void* arg); - - -#if MI_HUGE_PAGE_ABANDON -void _mi_segment_huge_page_free(mi_segment_t* segment, mi_page_t* page, mi_block_t* block); -#else -void _mi_segment_huge_page_reset(mi_segment_t* segment, mi_page_t* page, mi_block_t* block); -#endif - -uint8_t* _mi_segment_page_start(const mi_segment_t* segment, const mi_page_t* page, size_t* page_size); // page start for any page -void _mi_abandoned_reclaim_all(mi_heap_t* heap, mi_segments_tld_t* tld); -void _mi_abandoned_await_readers(mi_abandoned_pool_t *pool); -void _mi_abandoned_collect(mi_heap_t* heap, bool force, mi_segments_tld_t* tld); - -// "page.c" -void* _mi_malloc_generic(mi_heap_t* heap, size_t size, bool zero, size_t huge_alignment) mi_attr_noexcept mi_attr_malloc; - -void _mi_page_retire(mi_page_t* page) mi_attr_noexcept; // free the page if there are no other pages with many free blocks -void _mi_page_unfull(mi_page_t* page); -void _mi_page_free(mi_page_t* page, mi_page_queue_t* pq, bool force); // free the page -void _mi_page_abandon(mi_page_t* page, mi_page_queue_t* pq); // abandon the page, to be picked up by another thread... -void _mi_heap_delayed_free_all(mi_heap_t* heap); -bool _mi_heap_delayed_free_partial(mi_heap_t* heap); -void _mi_heap_collect_retired(mi_heap_t* heap, bool force); - -void _mi_page_use_delayed_free(mi_page_t* page, mi_delayed_t delay, bool override_never); -bool _mi_page_try_use_delayed_free(mi_page_t* page, mi_delayed_t delay, bool override_never); -size_t _mi_page_queue_append(mi_heap_t* heap, mi_page_queue_t* pq, mi_page_queue_t* append); -void _mi_deferred_free(mi_heap_t* heap, bool force); - -void _mi_page_free_collect(mi_page_t* page,bool force); -void _mi_page_reclaim(mi_heap_t* heap, mi_page_t* page); // callback from segments - -size_t _mi_bin_size(uint8_t bin); // for stats -uint8_t _mi_bin(size_t size); // for stats - -// "heap.c" -void _mi_heap_init_ex(mi_heap_t* heap, mi_tld_t* tld, mi_arena_id_t arena_id, bool no_reclaim, uint8_t tag); -void _mi_heap_destroy_pages(mi_heap_t* heap); -void _mi_heap_collect_abandon(mi_heap_t* heap); -void _mi_heap_set_default_direct(mi_heap_t* heap); -bool _mi_heap_memid_is_suitable(mi_heap_t* heap, mi_memid_t memid); -void _mi_heap_unsafe_destroy_all(void); -void _mi_heap_area_init(mi_heap_area_t* area, mi_page_t* page); -bool _mi_heap_area_visit_blocks(const mi_heap_area_t* area, mi_page_t *page, mi_block_visit_fun* visitor, void* arg); - -// "stats.c" -void _mi_stats_done(mi_stats_t* stats); -mi_msecs_t _mi_clock_now(void); -mi_msecs_t _mi_clock_end(mi_msecs_t start); -mi_msecs_t _mi_clock_start(void); - -// "alloc.c" -void* _mi_page_malloc(mi_heap_t* heap, mi_page_t* page, size_t size, bool zero) mi_attr_noexcept; // called from `_mi_malloc_generic` -void* _mi_heap_malloc_zero(mi_heap_t* heap, size_t size, bool zero) mi_attr_noexcept; -void* _mi_heap_malloc_zero_ex(mi_heap_t* heap, size_t size, bool zero, size_t huge_alignment) mi_attr_noexcept; // called from `_mi_heap_malloc_aligned` -void* _mi_heap_realloc_zero(mi_heap_t* heap, void* p, size_t newsize, bool zero) mi_attr_noexcept; -mi_block_t* _mi_page_ptr_unalign(const mi_segment_t* segment, const mi_page_t* page, const void* p); -bool _mi_free_delayed_block(mi_block_t* block); -void _mi_free_generic(const mi_segment_t* segment, mi_page_t* page, bool is_local, void* p) mi_attr_noexcept; // for runtime integration -void _mi_padding_shrink(const mi_page_t* page, const mi_block_t* block, const size_t min_size); - -// option.c, c primitives -char _mi_toupper(char c); -int _mi_strnicmp(const char* s, const char* t, size_t n); -void _mi_strlcpy(char* dest, const char* src, size_t dest_size); -void _mi_strlcat(char* dest, const char* src, size_t dest_size); -size_t _mi_strlen(const char* s); -size_t _mi_strnlen(const char* s, size_t max_len); - - -#if MI_DEBUG>1 -bool _mi_page_is_valid(mi_page_t* page); -#endif - - -// ------------------------------------------------------ -// Branches -// ------------------------------------------------------ - -#if defined(__GNUC__) || defined(__clang__) -#define mi_unlikely(x) (__builtin_expect(!!(x),false)) -#define mi_likely(x) (__builtin_expect(!!(x),true)) -#elif (defined(__cplusplus) && (__cplusplus >= 202002L)) || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) -#define mi_unlikely(x) (x) [[unlikely]] -#define mi_likely(x) (x) [[likely]] -#else -#define mi_unlikely(x) (x) -#define mi_likely(x) (x) -#endif - -#ifndef __has_builtin -#define __has_builtin(x) 0 -#endif - - -/* ----------------------------------------------------------- - Error codes passed to `_mi_fatal_error` - All are recoverable but EFAULT is a serious error and aborts by default in secure mode. - For portability define undefined error codes using common Unix codes: - ------------------------------------------------------------ */ -#include -#ifndef EAGAIN // double free -#define EAGAIN (11) -#endif -#ifndef ENOMEM // out of memory -#define ENOMEM (12) -#endif -#ifndef EFAULT // corrupted free-list or meta-data -#define EFAULT (14) -#endif -#ifndef EINVAL // trying to free an invalid pointer -#define EINVAL (22) -#endif -#ifndef EOVERFLOW // count*size overflow -#define EOVERFLOW (75) -#endif - - -/* ----------------------------------------------------------- - Inlined definitions ------------------------------------------------------------ */ -#define MI_UNUSED(x) (void)(x) -#if (MI_DEBUG>0) -#define MI_UNUSED_RELEASE(x) -#else -#define MI_UNUSED_RELEASE(x) MI_UNUSED(x) -#endif - -#define MI_INIT4(x) x(),x(),x(),x() -#define MI_INIT8(x) MI_INIT4(x),MI_INIT4(x) -#define MI_INIT16(x) MI_INIT8(x),MI_INIT8(x) -#define MI_INIT32(x) MI_INIT16(x),MI_INIT16(x) -#define MI_INIT64(x) MI_INIT32(x),MI_INIT32(x) -#define MI_INIT128(x) MI_INIT64(x),MI_INIT64(x) -#define MI_INIT256(x) MI_INIT128(x),MI_INIT128(x) - - -#include -// initialize a local variable to zero; use memset as compilers optimize constant sized memset's -#define _mi_memzero_var(x) memset(&x,0,sizeof(x)) - -// Is `x` a power of two? (0 is considered a power of two) -static inline bool _mi_is_power_of_two(uintptr_t x) { - return ((x & (x - 1)) == 0); -} - -// Is a pointer aligned? -static inline bool _mi_is_aligned(void* p, size_t alignment) { - mi_assert_internal(alignment != 0); - return (((uintptr_t)p % alignment) == 0); -} - -// Align upwards -static inline uintptr_t _mi_align_up(uintptr_t sz, size_t alignment) { - mi_assert_internal(alignment != 0); - uintptr_t mask = alignment - 1; - if ((alignment & mask) == 0) { // power of two? - return ((sz + mask) & ~mask); - } - else { - return (((sz + mask)/alignment)*alignment); - } -} - -// Align downwards -static inline uintptr_t _mi_align_down(uintptr_t sz, size_t alignment) { - mi_assert_internal(alignment != 0); - uintptr_t mask = alignment - 1; - if ((alignment & mask) == 0) { // power of two? - return (sz & ~mask); - } - else { - return ((sz / alignment) * alignment); - } -} - -// Divide upwards: `s <= _mi_divide_up(s,d)*d < s+d`. -static inline uintptr_t _mi_divide_up(uintptr_t size, size_t divider) { - mi_assert_internal(divider != 0); - return (divider == 0 ? size : ((size + divider - 1) / divider)); -} - -// Is memory zero initialized? -static inline bool mi_mem_is_zero(const void* p, size_t size) { - for (size_t i = 0; i < size; i++) { - if (((uint8_t*)p)[i] != 0) return false; - } - return true; -} - - -// Align a byte size to a size in _machine words_, -// i.e. byte size == `wsize*sizeof(void*)`. -static inline size_t _mi_wsize_from_size(size_t size) { - mi_assert_internal(size <= SIZE_MAX - sizeof(uintptr_t)); - return (size + sizeof(uintptr_t) - 1) / sizeof(uintptr_t); -} - -// Overflow detecting multiply -#if __has_builtin(__builtin_umul_overflow) || (defined(__GNUC__) && (__GNUC__ >= 5)) -#include // UINT_MAX, ULONG_MAX -#if defined(_CLOCK_T) // for Illumos -#undef _CLOCK_T -#endif -static inline bool mi_mul_overflow(size_t count, size_t size, size_t* total) { - #if (SIZE_MAX == ULONG_MAX) - return __builtin_umull_overflow(count, size, (unsigned long *)total); - #elif (SIZE_MAX == UINT_MAX) - return __builtin_umul_overflow(count, size, (unsigned int *)total); - #else - return __builtin_umulll_overflow(count, size, (unsigned long long *)total); - #endif -} -#else /* __builtin_umul_overflow is unavailable */ -static inline bool mi_mul_overflow(size_t count, size_t size, size_t* total) { - #define MI_MUL_NO_OVERFLOW ((size_t)1 << (4*sizeof(size_t))) // sqrt(SIZE_MAX) - *total = count * size; - // note: gcc/clang optimize this to directly check the overflow flag - return ((size >= MI_MUL_NO_OVERFLOW || count >= MI_MUL_NO_OVERFLOW) && size > 0 && (SIZE_MAX / size) < count); -} -#endif - -// Safe multiply `count*size` into `total`; return `true` on overflow. -static inline bool mi_count_size_overflow(size_t count, size_t size, size_t* total) { - if (count==1) { // quick check for the case where count is one (common for C++ allocators) - *total = size; - return false; - } - else if mi_unlikely(mi_mul_overflow(count, size, total)) { - #if MI_DEBUG > 0 - _mi_error_message(EOVERFLOW, "allocation request is too large (%zu * %zu bytes)\n", count, size); - #endif - *total = SIZE_MAX; - return true; - } - else return false; -} - - -/*---------------------------------------------------------------------------------------- - Heap functions -------------------------------------------------------------------------------------------- */ - -extern const mi_heap_t _mi_heap_empty; // read-only empty heap, initial value of the thread local default heap - -static inline bool mi_heap_is_backing(const mi_heap_t* heap) { - return (heap->tld->heap_backing == heap); -} - -static inline bool mi_heap_is_initialized(mi_heap_t* heap) { - mi_assert_internal(heap != NULL); - return (heap != &_mi_heap_empty); -} - -static inline uintptr_t _mi_ptr_cookie(const void* p) { - extern mi_heap_t _mi_heap_main; - mi_assert_internal(_mi_heap_main.cookie != 0); - return ((uintptr_t)p ^ _mi_heap_main.cookie); -} - -/* ----------------------------------------------------------- - Pages ------------------------------------------------------------ */ - -static inline mi_page_t* _mi_heap_get_free_small_page(mi_heap_t* heap, size_t size) { - mi_assert_internal(size <= (MI_SMALL_SIZE_MAX + MI_PADDING_SIZE)); - const size_t idx = _mi_wsize_from_size(size); - mi_assert_internal(idx < MI_PAGES_DIRECT); - return heap->pages_free_direct[idx]; -} - -// Segment that contains the pointer -// Large aligned blocks may be aligned at N*MI_SEGMENT_SIZE (inside a huge segment > MI_SEGMENT_SIZE), -// and we need align "down" to the segment info which is `MI_SEGMENT_SIZE` bytes before it; -// therefore we align one byte before `p`. -static inline mi_segment_t* _mi_ptr_segment(const void* p) { - mi_assert_internal(p != NULL); - return (mi_segment_t*)(((uintptr_t)p - 1) & ~MI_SEGMENT_MASK); -} - -static inline mi_page_t* mi_slice_to_page(mi_slice_t* s) { - mi_assert_internal(s->slice_offset== 0 && s->slice_count > 0); - return (mi_page_t*)(s); -} - -static inline mi_slice_t* mi_page_to_slice(mi_page_t* p) { - mi_assert_internal(p->slice_offset== 0 && p->slice_count > 0); - return (mi_slice_t*)(p); -} - -// Segment belonging to a page -static inline mi_segment_t* _mi_page_segment(const mi_page_t* page) { - mi_segment_t* segment = _mi_ptr_segment(page); - mi_assert_internal(segment == NULL || ((mi_slice_t*)page >= segment->slices && (mi_slice_t*)page < segment->slices + segment->slice_entries)); - return segment; -} - -static inline mi_slice_t* mi_slice_first(const mi_slice_t* slice) { - mi_slice_t* start = (mi_slice_t*)((uint8_t*)slice - slice->slice_offset); - mi_assert_internal(start >= _mi_ptr_segment(slice)->slices); - mi_assert_internal(start->slice_offset == 0); - mi_assert_internal(start + start->slice_count > slice); - return start; -} - -// Get the page containing the pointer (performance critical as it is called in mi_free) -static inline mi_page_t* _mi_segment_page_of(const mi_segment_t* segment, const void* p) { - mi_assert_internal(p > (void*)segment); - ptrdiff_t diff = (uint8_t*)p - (uint8_t*)segment; - mi_assert_internal(diff > 0 && diff <= (ptrdiff_t)MI_SEGMENT_SIZE); - size_t idx = (size_t)diff >> MI_SEGMENT_SLICE_SHIFT; - mi_assert_internal(idx <= segment->slice_entries); - mi_slice_t* slice0 = (mi_slice_t*)&segment->slices[idx]; - mi_slice_t* slice = mi_slice_first(slice0); // adjust to the block that holds the page data - mi_assert_internal(slice->slice_offset == 0); - mi_assert_internal(slice >= segment->slices && slice < segment->slices + segment->slice_entries); - return mi_slice_to_page(slice); -} - -// Quick page start for initialized pages -static inline uint8_t* _mi_page_start(const mi_segment_t* segment, const mi_page_t* page, size_t* page_size) { - return _mi_segment_page_start(segment, page, page_size); -} - -// Get the page containing the pointer -static inline mi_page_t* _mi_ptr_page(void* p) { - return _mi_segment_page_of(_mi_ptr_segment(p), p); -} - -// Get the block size of a page (special case for huge objects) -static inline size_t mi_page_block_size(const mi_page_t* page) { - const size_t bsize = page->xblock_size; - mi_assert_internal(bsize > 0); - if mi_likely(bsize < MI_HUGE_BLOCK_SIZE) { - return bsize; - } - else { - size_t psize; - _mi_segment_page_start(_mi_page_segment(page), page, &psize); - return psize; - } -} - -static inline bool mi_page_is_huge(const mi_page_t* page) { - return (_mi_page_segment(page)->kind == MI_SEGMENT_HUGE); -} - -// Get the usable block size of a page without fixed padding. -// This may still include internal padding due to alignment and rounding up size classes. -static inline size_t mi_page_usable_block_size(const mi_page_t* page) { - return mi_page_block_size(page) - MI_PADDING_SIZE; -} - -// size of a segment -static inline size_t mi_segment_size(mi_segment_t* segment) { - return segment->segment_slices * MI_SEGMENT_SLICE_SIZE; -} - -static inline uint8_t* mi_segment_end(mi_segment_t* segment) { - return (uint8_t*)segment + mi_segment_size(segment); -} - -// Thread free access -static inline mi_block_t* mi_page_thread_free(const mi_page_t* page) { - return (mi_block_t*)(mi_atomic_load_relaxed(&((mi_page_t*)page)->xthread_free) & ~3); -} - -static inline mi_delayed_t mi_page_thread_free_flag(const mi_page_t* page) { - return (mi_delayed_t)(mi_atomic_load_relaxed(&((mi_page_t*)page)->xthread_free) & 3); -} - -// Heap access -static inline mi_heap_t* mi_page_heap(const mi_page_t* page) { - return (mi_heap_t*)(mi_atomic_load_relaxed(&((mi_page_t*)page)->xheap)); -} - -static inline void mi_page_set_heap(mi_page_t* page, mi_heap_t* heap) { - mi_assert_internal(mi_page_thread_free_flag(page) != MI_DELAYED_FREEING); - mi_atomic_store_release(&page->xheap,(uintptr_t)heap); -} - -// Thread free flag helpers -static inline mi_block_t* mi_tf_block(mi_thread_free_t tf) { - return (mi_block_t*)(tf & ~0x03); -} -static inline mi_delayed_t mi_tf_delayed(mi_thread_free_t tf) { - return (mi_delayed_t)(tf & 0x03); -} -static inline mi_thread_free_t mi_tf_make(mi_block_t* block, mi_delayed_t delayed) { - return (mi_thread_free_t)((uintptr_t)block | (uintptr_t)delayed); -} -static inline mi_thread_free_t mi_tf_set_delayed(mi_thread_free_t tf, mi_delayed_t delayed) { - return mi_tf_make(mi_tf_block(tf),delayed); -} -static inline mi_thread_free_t mi_tf_set_block(mi_thread_free_t tf, mi_block_t* block) { - return mi_tf_make(block, mi_tf_delayed(tf)); -} - -// are all blocks in a page freed? -// note: needs up-to-date used count, (as the `xthread_free` list may not be empty). see `_mi_page_collect_free`. -static inline bool mi_page_all_free(const mi_page_t* page) { - mi_assert_internal(page != NULL); - return (page->used == 0); -} - -// are there any available blocks? -static inline bool mi_page_has_any_available(const mi_page_t* page) { - mi_assert_internal(page != NULL && page->reserved > 0); - return (page->used < page->reserved || (mi_page_thread_free(page) != NULL)); -} - -// are there immediately available blocks, i.e. blocks available on the free list. -static inline bool mi_page_immediate_available(const mi_page_t* page) { - mi_assert_internal(page != NULL); - return (page->free != NULL); -} - -// is more than 7/8th of a page in use? -static inline bool mi_page_mostly_used(const mi_page_t* page) { - if (page==NULL) return true; - uint16_t frac = page->reserved / 8U; - return (page->reserved - page->used <= frac); -} - -static inline mi_page_queue_t* mi_page_queue(const mi_heap_t* heap, size_t size) { - return &((mi_heap_t*)heap)->pages[_mi_bin(size)]; -} - - - -//----------------------------------------------------------- -// Page flags -//----------------------------------------------------------- -static inline bool mi_page_is_in_full(const mi_page_t* page) { - return page->flags.x.in_full; -} - -static inline void mi_page_set_in_full(mi_page_t* page, bool in_full) { - page->flags.x.in_full = in_full; -} - -static inline bool mi_page_has_aligned(const mi_page_t* page) { - return page->flags.x.has_aligned; -} - -static inline void mi_page_set_has_aligned(mi_page_t* page, bool has_aligned) { - page->flags.x.has_aligned = has_aligned; -} - - -/* ------------------------------------------------------------------- -Encoding/Decoding the free list next pointers - -This is to protect against buffer overflow exploits where the -free list is mutated. Many hardened allocators xor the next pointer `p` -with a secret key `k1`, as `p^k1`. This prevents overwriting with known -values but might be still too weak: if the attacker can guess -the pointer `p` this can reveal `k1` (since `p^k1^p == k1`). -Moreover, if multiple blocks can be read as well, the attacker can -xor both as `(p1^k1) ^ (p2^k1) == p1^p2` which may reveal a lot -about the pointers (and subsequently `k1`). - -Instead mimalloc uses an extra key `k2` and encodes as `((p^k2)<<> (MI_INTPTR_BITS - shift)))); -} -static inline uintptr_t mi_rotr(uintptr_t x, uintptr_t shift) { - shift %= MI_INTPTR_BITS; - return (shift==0 ? x : ((x >> shift) | (x << (MI_INTPTR_BITS - shift)))); -} - -static inline void* mi_ptr_decode(const void* null, const mi_encoded_t x, const uintptr_t* keys) { - void* p = (void*)(mi_rotr(x - keys[0], keys[0]) ^ keys[1]); - return (p==null ? NULL : p); -} - -static inline mi_encoded_t mi_ptr_encode(const void* null, const void* p, const uintptr_t* keys) { - uintptr_t x = (uintptr_t)(p==NULL ? null : p); - return mi_rotl(x ^ keys[1], keys[0]) + keys[0]; -} - -static inline mi_block_t* mi_block_nextx( const void* null, const mi_block_t* block, const uintptr_t* keys ) { - mi_track_mem_defined(block,sizeof(mi_block_t)); - mi_block_t* next; - #ifdef MI_ENCODE_FREELIST - next = (mi_block_t*)mi_ptr_decode(null, mi_atomic_load_relaxed((_Atomic(mi_encoded_t)*)&block->next), keys); - #else - MI_UNUSED(keys); MI_UNUSED(null); - next = (mi_block_t*)mi_atomic_load_relaxed((_Atomic(mi_encoded_t)*)&block->next); - #endif - mi_track_mem_noaccess(block,sizeof(mi_block_t)); - return next; -} - -static inline void mi_block_set_nextx(const void* null, mi_block_t* block, const mi_block_t* next, const uintptr_t* keys) { - mi_track_mem_undefined(block,sizeof(mi_block_t)); - #ifdef MI_ENCODE_FREELIST - mi_atomic_store_relaxed(&block->next, mi_ptr_encode(null, next, keys)); - #else - MI_UNUSED(keys); MI_UNUSED(null); - mi_atomic_store_relaxed(&block->next, (mi_encoded_t)next); - #endif - mi_track_mem_noaccess(block,sizeof(mi_block_t)); -} - -static inline mi_block_t* mi_block_next(const mi_page_t* page, const mi_block_t* block) { - #ifdef MI_ENCODE_FREELIST - mi_block_t* next = mi_block_nextx(page,block,page->keys); - // check for free list corruption: is `next` at least in the same page? - // TODO: check if `next` is `page->block_size` aligned? - if mi_unlikely(next!=NULL && !mi_is_in_same_page(block, next)) { - _mi_error_message(EFAULT, "corrupted free list entry of size %zub at %p: value 0x%zx\n", mi_page_block_size(page), block, (uintptr_t)next); - next = NULL; - } - return next; - #else - MI_UNUSED(page); - return mi_block_nextx(page,block,NULL); - #endif -} - -static inline void mi_block_set_next(const mi_page_t* page, mi_block_t* block, const mi_block_t* next) { - #ifdef MI_ENCODE_FREELIST - mi_block_set_nextx(page,block,next, page->keys); - #else - MI_UNUSED(page); - mi_block_set_nextx(page,block,next,NULL); - #endif -} - - -// ------------------------------------------------------------------- -// commit mask -// ------------------------------------------------------------------- - -static inline void mi_commit_mask_create_empty(mi_commit_mask_t* cm) { - for (size_t i = 0; i < MI_COMMIT_MASK_FIELD_COUNT; i++) { - cm->mask[i] = 0; - } -} - -static inline void mi_commit_mask_create_full(mi_commit_mask_t* cm) { - for (size_t i = 0; i < MI_COMMIT_MASK_FIELD_COUNT; i++) { - cm->mask[i] = ~((size_t)0); - } -} - -static inline bool mi_commit_mask_is_empty(const mi_commit_mask_t* cm) { - for (size_t i = 0; i < MI_COMMIT_MASK_FIELD_COUNT; i++) { - if (cm->mask[i] != 0) return false; - } - return true; -} - -static inline bool mi_commit_mask_is_full(const mi_commit_mask_t* cm) { - for (size_t i = 0; i < MI_COMMIT_MASK_FIELD_COUNT; i++) { - if (cm->mask[i] != ~((size_t)0)) return false; - } - return true; -} - -// defined in `segment.c`: -size_t _mi_commit_mask_committed_size(const mi_commit_mask_t* cm, size_t total); -size_t _mi_commit_mask_next_run(const mi_commit_mask_t* cm, size_t* idx); - -#define mi_commit_mask_foreach(cm,idx,count) \ - idx = 0; \ - while ((count = _mi_commit_mask_next_run(cm,&idx)) > 0) { - -#define mi_commit_mask_foreach_end() \ - idx += count; \ - } - - - -/* ----------------------------------------------------------- - memory id's ------------------------------------------------------------ */ - -static inline mi_memid_t _mi_memid_create(mi_memkind_t memkind) { - mi_memid_t memid; - _mi_memzero_var(memid); - memid.memkind = memkind; - return memid; -} - -static inline mi_memid_t _mi_memid_none(void) { - return _mi_memid_create(MI_MEM_NONE); -} - -static inline mi_memid_t _mi_memid_create_os(bool committed, bool is_zero, bool is_large) { - mi_memid_t memid = _mi_memid_create(MI_MEM_OS); - memid.initially_committed = committed; - memid.initially_zero = is_zero; - memid.is_pinned = is_large; - return memid; -} - - -// ------------------------------------------------------------------- -// Fast "random" shuffle -// ------------------------------------------------------------------- - -static inline uintptr_t _mi_random_shuffle(uintptr_t x) { - if (x==0) { x = 17; } // ensure we don't get stuck in generating zeros -#if (MI_INTPTR_SIZE==8) - // by Sebastiano Vigna, see: - x ^= x >> 30; - x *= 0xbf58476d1ce4e5b9UL; - x ^= x >> 27; - x *= 0x94d049bb133111ebUL; - x ^= x >> 31; -#elif (MI_INTPTR_SIZE==4) - // by Chris Wellons, see: - x ^= x >> 16; - x *= 0x7feb352dUL; - x ^= x >> 15; - x *= 0x846ca68bUL; - x ^= x >> 16; -#endif - return x; -} - -// ------------------------------------------------------------------- -// Optimize numa node access for the common case (= one node) -// ------------------------------------------------------------------- - -int _mi_os_numa_node_get(mi_os_tld_t* tld); -size_t _mi_os_numa_node_count_get(void); - -extern _Atomic(size_t) _mi_numa_node_count; -static inline int _mi_os_numa_node(mi_os_tld_t* tld) { - if mi_likely(mi_atomic_load_relaxed(&_mi_numa_node_count) == 1) { return 0; } - else return _mi_os_numa_node_get(tld); -} -static inline size_t _mi_os_numa_node_count(void) { - const size_t count = mi_atomic_load_relaxed(&_mi_numa_node_count); - if mi_likely(count > 0) { return count; } - else return _mi_os_numa_node_count_get(); -} - - - -// ----------------------------------------------------------------------- -// Count bits: trailing or leading zeros (with MI_INTPTR_BITS on all zero) -// ----------------------------------------------------------------------- - -#if defined(__GNUC__) - -#include // LONG_MAX -#define MI_HAVE_FAST_BITSCAN -static inline size_t mi_clz(uintptr_t x) { - if (x==0) return MI_INTPTR_BITS; -#if (INTPTR_MAX == LONG_MAX) - return __builtin_clzl(x); -#else - return __builtin_clzll(x); -#endif -} -static inline size_t mi_ctz(uintptr_t x) { - if (x==0) return MI_INTPTR_BITS; -#if (INTPTR_MAX == LONG_MAX) - return __builtin_ctzl(x); -#else - return __builtin_ctzll(x); -#endif -} - -#elif defined(_MSC_VER) - -#include // LONG_MAX -#include // BitScanReverse64 -#define MI_HAVE_FAST_BITSCAN -static inline size_t mi_clz(uintptr_t x) { - if (x==0) return MI_INTPTR_BITS; - unsigned long idx; -#if (INTPTR_MAX == LONG_MAX) - _BitScanReverse(&idx, x); -#else - _BitScanReverse64(&idx, x); -#endif - return ((MI_INTPTR_BITS - 1) - idx); -} -static inline size_t mi_ctz(uintptr_t x) { - if (x==0) return MI_INTPTR_BITS; - unsigned long idx; -#if (INTPTR_MAX == LONG_MAX) - _BitScanForward(&idx, x); -#else - _BitScanForward64(&idx, x); -#endif - return idx; -} - -#else -static inline size_t mi_ctz32(uint32_t x) { - // de Bruijn multiplication, see - static const unsigned char debruijn[32] = { - 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, - 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9 - }; - if (x==0) return 32; - return debruijn[((x & -(int32_t)x) * 0x077CB531UL) >> 27]; -} -static inline size_t mi_clz32(uint32_t x) { - // de Bruijn multiplication, see - static const uint8_t debruijn[32] = { - 31, 22, 30, 21, 18, 10, 29, 2, 20, 17, 15, 13, 9, 6, 28, 1, - 23, 19, 11, 3, 16, 14, 7, 24, 12, 4, 8, 25, 5, 26, 27, 0 - }; - if (x==0) return 32; - x |= x >> 1; - x |= x >> 2; - x |= x >> 4; - x |= x >> 8; - x |= x >> 16; - return debruijn[(uint32_t)(x * 0x07C4ACDDUL) >> 27]; -} - -static inline size_t mi_clz(uintptr_t x) { - if (x==0) return MI_INTPTR_BITS; -#if (MI_INTPTR_BITS <= 32) - return mi_clz32((uint32_t)x); -#else - size_t count = mi_clz32((uint32_t)(x >> 32)); - if (count < 32) return count; - return (32 + mi_clz32((uint32_t)x)); -#endif -} -static inline size_t mi_ctz(uintptr_t x) { - if (x==0) return MI_INTPTR_BITS; -#if (MI_INTPTR_BITS <= 32) - return mi_ctz32((uint32_t)x); -#else - size_t count = mi_ctz32((uint32_t)x); - if (count < 32) return count; - return (32 + mi_ctz32((uint32_t)(x>>32))); -#endif -} - -#endif - -// "bit scan reverse": Return index of the highest bit (or MI_INTPTR_BITS if `x` is zero) -static inline size_t mi_bsr(uintptr_t x) { - return (x==0 ? MI_INTPTR_BITS : MI_INTPTR_BITS - 1 - mi_clz(x)); -} - - -// --------------------------------------------------------------------------------- -// Provide our own `_mi_memcpy` for potential performance optimizations. -// -// For now, only on Windows with msvc/clang-cl we optimize to `rep movsb` if -// we happen to run on x86/x64 cpu's that have "fast short rep movsb" (FSRM) support -// (AMD Zen3+ (~2020) or Intel Ice Lake+ (~2017). See also issue #201 and pr #253. -// --------------------------------------------------------------------------------- +// -------------------------------------------------------------------------- +// Internal functions +// -------------------------------------------------------------------------- -#if !MI_TRACK_ENABLED && defined(_WIN32) && (defined(_M_IX86) || defined(_M_X64)) -#include -extern bool _mi_cpu_has_fsrm; -static inline void _mi_memcpy(void* dst, const void* src, size_t n) { - if (_mi_cpu_has_fsrm) { - __movsb((unsigned char*)dst, (const unsigned char*)src, n); - } - else { - memcpy(dst, src, n); - } -} -static inline void _mi_memzero(void* dst, size_t n) { - if (_mi_cpu_has_fsrm) { - __stosb((unsigned char*)dst, 0, n); - } - else { - memset(dst, 0, n); - } -} -#else -static inline void _mi_memcpy(void* dst, const void* src, size_t n) { - memcpy(dst, src, n); -} -static inline void _mi_memzero(void* dst, size_t n) { - memset(dst, 0, n); -} -#endif +#include "prim/os.h" +#include "prim/prim-tls.h" -// ------------------------------------------------------------------------------- -// The `_mi_memcpy_aligned` can be used if the pointers are machine-word aligned -// This is used for example in `mi_realloc`. -// ------------------------------------------------------------------------------- +#include "util/libc.h" +#include "util/options.h" +#include "util/random.h" +#include "util/stats.h" +#include "util/threadlocal.h" -#if (defined(__GNUC__) && (__GNUC__ >= 4)) || defined(__clang__) -// On GCC/CLang we provide a hint that the pointers are word aligned. -static inline void _mi_memcpy_aligned(void* dst, const void* src, size_t n) { - mi_assert_internal(((uintptr_t)dst % MI_INTPTR_SIZE == 0) && ((uintptr_t)src % MI_INTPTR_SIZE == 0)); - void* adst = __builtin_assume_aligned(dst, MI_INTPTR_SIZE); - const void* asrc = __builtin_assume_aligned(src, MI_INTPTR_SIZE); - _mi_memcpy(adst, asrc, n); -} +#include "arena/arena.h" +#include "arena/arena-alloc.h" +#include "arena/arena-page.h" -static inline void _mi_memzero_aligned(void* dst, size_t n) { - mi_assert_internal((uintptr_t)dst % MI_INTPTR_SIZE == 0); - void* adst = __builtin_assume_aligned(dst, MI_INTPTR_SIZE); - _mi_memzero(adst, n); -} -#else -// Default fallback on `_mi_memcpy` -static inline void _mi_memcpy_aligned(void* dst, const void* src, size_t n) { - mi_assert_internal(((uintptr_t)dst % MI_INTPTR_SIZE == 0) && ((uintptr_t)src % MI_INTPTR_SIZE == 0)); - _mi_memcpy(dst, src, n); -} +#include "heap/page-map.h" +#include "heap/subproc.h" +#include "heap/init.h" +#include "heap/theap.h" +#include "heap/heap.h" -static inline void _mi_memzero_aligned(void* dst, size_t n) { - mi_assert_internal((uintptr_t)dst % MI_INTPTR_SIZE == 0); - _mi_memzero(dst, n); -} -#endif +#include "alloc/page.h" +#include "alloc/alloc.h" +#include "alloc/free.h" +#include "profile/sample-profile.h" +#include "profile/sample-guarded.h" +#include "profile/pprof.h" -#endif +#endif // MI_INTERNAL_H diff --git a/Include/internal/mimalloc/mimalloc/prim.h b/Include/internal/mimalloc/mimalloc/prim.h index 322ab29e6b41c24..38e9499054b5fbb 100644 --- a/Include/internal/mimalloc/mimalloc/prim.h +++ b/Include/internal/mimalloc/mimalloc/prim.h @@ -1,5 +1,5 @@ /* ---------------------------------------------------------------------------- -Copyright (c) 2018-2023, Microsoft Research, Daan Leijen +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen This is free software; you can redistribute it and/or modify it under the terms of the MIT license. A copy of the license can be found in the file "LICENSE" at the root of this distribution. @@ -8,26 +8,30 @@ terms of the MIT license. A copy of the license can be found in the file #ifndef MIMALLOC_PRIM_H #define MIMALLOC_PRIM_H +#include "types.h" // -------------------------------------------------------------------------- // This file specifies the primitive portability API. // Each OS/host needs to implement these primitives, see `src/prim` // for implementations on Window, macOS, WASI, and Linux/Unix. // -// note: on all primitive functions, we always have result parameters != NUL, and: +// note: on all primitive functions, we always have result parameters != NULL, and: // addr != NULL and page aligned // size > 0 and page aligned -// return value is an error code an int where 0 is success. +// the return value is an error code as an `int` where 0 is success // -------------------------------------------------------------------------- // OS memory configuration typedef struct mi_os_mem_config_s { - size_t page_size; // 4KiB - size_t large_page_size; // 2MiB - size_t alloc_granularity; // smallest allocation size (on Windows 64KiB) - bool has_overcommit; // can we reserve more memory than can be actually committed? - bool must_free_whole; // must allocated blocks be freed as a whole (false for mmap, true for VirtualAlloc) - bool has_virtual_reserve; // supports virtual address space reservation? (if true we can reserve virtual address space without using commit or physical memory) + size_t page_size; // default to 4KiB + size_t large_page_size; // 0 if not supported, usually 2MiB + size_t alloc_granularity; // smallest allocation size (usually 4KiB, on Windows 64KiB) + size_t physical_memory_in_kib; // physical memory size in KiB + size_t virtual_address_bits; // usually 48 or 56 bits on 64-bit systems. (used to determine secure randomization) + bool has_overcommit; // can we reserve more memory than can be actually committed? + bool has_partial_free; // can allocated blocks be freed partially? (true for mmap, false for VirtualAlloc) + bool has_virtual_reserve; // supports virtual address space reservation? (if true we can reserve virtual address space without using commit or physical memory) + bool has_transparent_huge_pages; // true if transparent huge pages are enabled (on Linux) } mi_os_mem_config_t; // Initialize @@ -41,9 +45,10 @@ int _mi_prim_free(void* addr, size_t size ); // If `commit` is false, the virtual memory range only needs to be reserved (with no access) // which will later be committed explicitly using `_mi_prim_commit`. // `is_zero` is set to true if the memory was zero initialized (as on most OS's) +// The `hint_addr` address is either `NULL` or a preferred allocation address but can be ignored. // pre: !commit => !allow_large // try_alignment >= _mi_os_page_size() and a power of 2 -int _mi_prim_alloc(size_t size, size_t try_alignment, bool commit, bool allow_large, bool* is_large, bool* is_zero, void** addr); +int _mi_prim_alloc(void* hint_addr, size_t size, size_t try_alignment, bool commit, bool allow_large, bool* is_large, bool* is_zero, void** addr); // Commit memory. Returns error code or 0 on success. // For example, on Linux this would make the memory PROT_READ|PROT_WRITE. @@ -56,10 +61,15 @@ int _mi_prim_commit(void* addr, size_t size, bool* is_zero); // pre: needs_recommit != NULL int _mi_prim_decommit(void* addr, size_t size, bool* needs_recommit); -// Reset memory. The range keeps being accessible but the content might be reset. +// Reset memory. The range keeps being accessible but the content might be reset to zero at any moment. // Returns error code or 0 on success. int _mi_prim_reset(void* addr, size_t size); +// Reuse memory. This is called for memory that is already committed but +// may have been reset (`_mi_prim_reset`) or decommitted (`_mi_prim_decommit`) where `needs_recommit` was false. +// Returns error code or 0 on success. On most platforms this is a no-op. +int _mi_prim_reuse(void* addr, size_t size); + // Protect memory. Returns error code or 0 on success. int _mi_prim_protect(void* addr, size_t size, bool protect); @@ -98,7 +108,9 @@ void _mi_prim_out_stderr( const char* msg ); // Get an environment variable. (only for options) // name != NULL, result != NULL, result_size >= 64 -bool _mi_prim_getenv(const char* name, char* result, size_t result_size); +// Return 1 for success, 0 if not found, +// and -1 on error (for example, if `getenv` cannot be called yet during preloading). +int _mi_prim_getenv(const char* name, char* result, size_t result_size); // Fill a buffer with strong randomness; return `false` on error or if @@ -111,219 +123,32 @@ void _mi_prim_thread_init_auto_done(void); // Called on process exit and may take action to clean up resources associated with the thread auto done. void _mi_prim_thread_done_auto_done(void); -// Called when the default heap for a thread changes -void _mi_prim_thread_associate_default_heap(mi_heap_t* heap); +// Called when the default theap for a thread changes +void _mi_prim_thread_associate_default_theap(mi_theap_t* theap); +// Is this thread part of a thread pool? +bool _mi_prim_thread_is_in_threadpool(void); -//------------------------------------------------------------------- -// Thread id: `_mi_prim_thread_id()` -// -// Getting the thread id should be performant as it is called in the -// fast path of `_mi_free` and we specialize for various platforms as -// inlined definitions. Regular code should call `init.c:_mi_thread_id()`. -// We only require _mi_prim_thread_id() to return a unique id -// for each thread (unequal to zero). -//------------------------------------------------------------------- - -// defined in `init.c`; do not use these directly -extern mi_decl_thread mi_heap_t* _mi_heap_default; // default heap to allocate from -extern bool _mi_process_is_initialized; // has mi_process_init been called? - -static inline mi_threadid_t _mi_prim_thread_id(void) mi_attr_noexcept; - -#ifdef MI_PRIM_THREAD_ID - -static inline mi_threadid_t _mi_prim_thread_id(void) mi_attr_noexcept { - return MI_PRIM_THREAD_ID(); -} - -#elif defined(_WIN32) - -#define WIN32_LEAN_AND_MEAN -#include -static inline mi_threadid_t _mi_prim_thread_id(void) mi_attr_noexcept { - // Windows: works on Intel and ARM in both 32- and 64-bit - return (uintptr_t)NtCurrentTeb(); -} - -// We use assembly for a fast thread id on the main platforms. The TLS layout depends on -// both the OS and libc implementation so we use specific tests for each main platform. -// If you test on another platform and it works please send a PR :-) -// see also https://akkadia.org/drepper/tls.pdf for more info on the TLS register. -#elif defined(__GNUC__) && ( \ - (defined(__GLIBC__) && (defined(__x86_64__) || defined(__i386__) || (defined(__arm__) && __ARM_ARCH >= 7) || defined(__aarch64__))) \ - || (defined(__APPLE__) && (defined(__x86_64__) || defined(__aarch64__))) \ - || (defined(__BIONIC__) && (defined(__x86_64__) || defined(__i386__) || (defined(__arm__) && __ARM_ARCH >= 7) || defined(__aarch64__))) \ - || (defined(__FreeBSD__) && (defined(__x86_64__) || defined(__i386__) || defined(__aarch64__))) \ - || (defined(__OpenBSD__) && (defined(__x86_64__) || defined(__i386__) || defined(__aarch64__))) \ - ) - -static inline void* mi_prim_tls_slot(size_t slot) mi_attr_noexcept { - void* res; - const size_t ofs = (slot*sizeof(void*)); - #if defined(__i386__) - __asm__("movl %%gs:%1, %0" : "=r" (res) : "m" (*((void**)ofs)) : ); // x86 32-bit always uses GS - #elif defined(__APPLE__) && defined(__x86_64__) - __asm__("movq %%gs:%1, %0" : "=r" (res) : "m" (*((void**)ofs)) : ); // x86_64 macOSX uses GS - #elif defined(__x86_64__) && (MI_INTPTR_SIZE==4) - __asm__("movl %%fs:%1, %0" : "=r" (res) : "m" (*((void**)ofs)) : ); // x32 ABI - #elif defined(__x86_64__) - __asm__("movq %%fs:%1, %0" : "=r" (res) : "m" (*((void**)ofs)) : ); // x86_64 Linux, BSD uses FS - #elif defined(__arm__) - void** tcb; MI_UNUSED(ofs); - __asm__ volatile ("mrc p15, 0, %0, c13, c0, 3\nbic %0, %0, #3" : "=r" (tcb)); - res = tcb[slot]; - #elif defined(__aarch64__) - void** tcb; MI_UNUSED(ofs); - #if defined(__APPLE__) // M1, issue #343 - __asm__ volatile ("mrs %0, tpidrro_el0\nbic %0, %0, #7" : "=r" (tcb)); - #else - __asm__ volatile ("mrs %0, tpidr_el0" : "=r" (tcb)); - #endif - res = tcb[slot]; - #endif - return res; -} - -// setting a tls slot is only used on macOS for now -static inline void mi_prim_tls_slot_set(size_t slot, void* value) mi_attr_noexcept { - const size_t ofs = (slot*sizeof(void*)); - #if defined(__i386__) - __asm__("movl %1,%%gs:%0" : "=m" (*((void**)ofs)) : "rn" (value) : ); // 32-bit always uses GS - #elif defined(__APPLE__) && defined(__x86_64__) - __asm__("movq %1,%%gs:%0" : "=m" (*((void**)ofs)) : "rn" (value) : ); // x86_64 macOS uses GS - #elif defined(__x86_64__) && (MI_INTPTR_SIZE==4) - __asm__("movl %1,%%fs:%0" : "=m" (*((void**)ofs)) : "rn" (value) : ); // x32 ABI - #elif defined(__x86_64__) - __asm__("movq %1,%%fs:%0" : "=m" (*((void**)ofs)) : "rn" (value) : ); // x86_64 Linux, BSD uses FS - #elif defined(__arm__) - void** tcb; MI_UNUSED(ofs); - __asm__ volatile ("mrc p15, 0, %0, c13, c0, 3\nbic %0, %0, #3" : "=r" (tcb)); - tcb[slot] = value; - #elif defined(__aarch64__) - void** tcb; MI_UNUSED(ofs); - #if defined(__APPLE__) // M1, issue #343 - __asm__ volatile ("mrs %0, tpidrro_el0\nbic %0, %0, #7" : "=r" (tcb)); - #else - __asm__ volatile ("mrs %0, tpidr_el0" : "=r" (tcb)); - #endif - tcb[slot] = value; - #endif -} - -static inline mi_threadid_t _mi_prim_thread_id(void) mi_attr_noexcept { - #if defined(__BIONIC__) - // issue #384, #495: on the Bionic libc (Android), slot 1 is the thread id - // see: https://github.com/aosp-mirror/platform_bionic/blob/c44b1d0676ded732df4b3b21c5f798eacae93228/libc/platform/bionic/tls_defines.h#L86 - return (uintptr_t)mi_prim_tls_slot(1); - #else - // in all our other targets, slot 0 is the thread id - // glibc: https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=sysdeps/x86_64/nptl/tls.h - // apple: https://github.com/apple/darwin-xnu/blob/main/libsyscall/os/tsd.h#L36 - return (uintptr_t)mi_prim_tls_slot(0); - #endif -} - -#else - -// otherwise use portable C, taking the address of a thread local variable (this is still very fast on most platforms). -static inline mi_threadid_t _mi_prim_thread_id(void) mi_attr_noexcept { - return (uintptr_t)&_mi_heap_default; -} - -#endif - - - -/* ---------------------------------------------------------------------------------------- -The thread local default heap: `_mi_prim_get_default_heap()` -This is inlined here as it is on the fast path for allocation functions. - -On most platforms (Windows, Linux, FreeBSD, NetBSD, etc), this just returns a -__thread local variable (`_mi_heap_default`). With the initial-exec TLS model this ensures -that the storage will always be available (allocated on the thread stacks). - -On some platforms though we cannot use that when overriding `malloc` since the underlying -TLS implementation (or the loader) will call itself `malloc` on a first access and recurse. -We try to circumvent this in an efficient way: -- macOSX : we use an unused TLS slot from the OS allocated slots (MI_TLS_SLOT). On OSX, the - loader itself calls `malloc` even before the modules are initialized. -- OpenBSD: we use an unused slot from the pthread block (MI_TLS_PTHREAD_SLOT_OFS). -- DragonFly: defaults are working but seem slow compared to freeBSD (see PR #323) -------------------------------------------------------------------------------------------- */ - -static inline mi_heap_t* mi_prim_get_default_heap(void); - -#if defined(MI_MALLOC_OVERRIDE) -#if defined(__APPLE__) // macOS - #define MI_TLS_SLOT 89 // seems unused? - // #define MI_TLS_RECURSE_GUARD 1 - // other possible unused ones are 9, 29, __PTK_FRAMEWORK_JAVASCRIPTCORE_KEY4 (94), __PTK_FRAMEWORK_GC_KEY9 (112) and __PTK_FRAMEWORK_OLDGC_KEY9 (89) - // see -#elif defined(__OpenBSD__) - // use end bytes of a name; goes wrong if anyone uses names > 23 characters (ptrhread specifies 16) - // see - #define MI_TLS_PTHREAD_SLOT_OFS (6*sizeof(int) + 4*sizeof(void*) + 24) - // #elif defined(__DragonFly__) - // #warning "mimalloc is not working correctly on DragonFly yet." - // #define MI_TLS_PTHREAD_SLOT_OFS (4 + 1*sizeof(void*)) // offset `uniqueid` (also used by gdb?) -#elif defined(__ANDROID__) - // See issue #381 - #define MI_TLS_PTHREAD -#endif -#endif - - -#if defined(MI_TLS_SLOT) - -static inline mi_heap_t* mi_prim_get_default_heap(void) { - mi_heap_t* heap = (mi_heap_t*)mi_prim_tls_slot(MI_TLS_SLOT); - if mi_unlikely(heap == NULL) { - #ifdef __GNUC__ - __asm(""); // prevent conditional load of the address of _mi_heap_empty - #endif - heap = (mi_heap_t*)&_mi_heap_empty; - } - return heap; -} - -#elif defined(MI_TLS_PTHREAD_SLOT_OFS) - -static inline mi_heap_t** mi_prim_tls_pthread_heap_slot(void) { - pthread_t self = pthread_self(); - #if defined(__DragonFly__) - if (self==NULL) return NULL; - #endif - return (mi_heap_t**)((uint8_t*)self + MI_TLS_PTHREAD_SLOT_OFS); -} - -static inline mi_heap_t* mi_prim_get_default_heap(void) { - mi_heap_t** pheap = mi_prim_tls_pthread_heap_slot(); - if mi_unlikely(pheap == NULL) return _mi_heap_main_get(); - mi_heap_t* heap = *pheap; - if mi_unlikely(heap == NULL) return (mi_heap_t*)&_mi_heap_empty; - return heap; -} - -#elif defined(MI_TLS_PTHREAD) - -extern pthread_key_t _mi_heap_default_key; -static inline mi_heap_t* mi_prim_get_default_heap(void) { - mi_heap_t* heap = (mi_unlikely(_mi_heap_default_key == (pthread_key_t)(-1)) ? _mi_heap_main_get() : (mi_heap_t*)pthread_getspecific(_mi_heap_default_key)); - return (mi_unlikely(heap == NULL) ? (mi_heap_t*)&_mi_heap_empty : heap); -} - -#else // default using a thread local variable; used on most platforms. - -static inline mi_heap_t* mi_prim_get_default_heap(void) { - #if defined(MI_TLS_RECURSE_GUARD) - if (mi_unlikely(!_mi_process_is_initialized)) return _mi_heap_main_get(); - #endif - return _mi_heap_default; -} - -#endif // mi_prim_get_default_heap() - - - -#endif // MIMALLOC_PRIM_H +// Yield to other threads. Should be similar to `sleep(0)`. +// Is called only in rare situations and does not have to be lightning fast. +void _mi_prim_thread_yield(void); + +// Return `true` if the process is forcefully shutdown (with potentially killed threads that might still hold a lock) +// If this returns true, mimalloc skips graceful shutdown (as locks might still be held by threads that are killed). +// See also issue #1377 +bool _mi_prim_process_is_killed(void); + + +// The following are predefined by default + +// Is the mimalloc-redirect running (Windows only) +bool _mi_is_redirected(void); + +// Called to initialize the allocator +bool _mi_allocator_init(const char** message); + +// Called to clean up the allocator +void _mi_allocator_done(void); + + +#endif // MI_PRIM_H diff --git a/Include/internal/mimalloc/mimalloc/prim/os.h b/Include/internal/mimalloc/mimalloc/prim/os.h new file mode 100644 index 000000000000000..98847aec3148cd2 --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/prim/os.h @@ -0,0 +1,55 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_OS_H +#define MI_INTERNAL_OS_H + +#include "../types.h" + +void _mi_os_init(void); // called from process init +void* _mi_os_alloc(mi_subproc_t* subproc, size_t size, mi_memid_t* memid); +void* _mi_os_zalloc(mi_subproc_t* subproc, size_t size, mi_memid_t* memid); +void _mi_os_free(mi_subproc_t* subproc, void* p, size_t size, mi_memid_t memid); +void _mi_os_free_ex(mi_subproc_t* subproc, void* p, size_t size, bool still_committed, mi_memid_t memid ); + +size_t _mi_os_page_size(void); +size_t _mi_os_guard_page_size(void); +size_t _mi_os_good_alloc_size(size_t size); +bool _mi_os_has_overcommit(void); +bool _mi_os_has_virtual_reserve(void); +bool _mi_os_canuse_thp(void); +size_t _mi_os_virtual_address_bits(void); +size_t _mi_os_minimal_purge_size(void); + +bool _mi_os_reset(mi_subproc_t* subproc, void* addr, size_t size); +bool _mi_os_decommit(mi_subproc_t* subproc, void* addr, size_t size); +void _mi_os_reuse(mi_subproc_t* subproc, void* p, size_t size); +mi_decl_nodiscard bool _mi_os_commit(mi_subproc_t* subproc, void* p, size_t size, bool* is_zero); +mi_decl_nodiscard bool _mi_os_commit_ex(mi_subproc_t* subproc, void* addr, size_t size, bool* is_zero, size_t stat_already_committed); +mi_decl_nodiscard bool _mi_os_protect(void* addr, size_t size); +bool _mi_os_unprotect(void* addr, size_t size); +bool _mi_os_purge(mi_subproc_t* subproc, void* p, size_t size); +bool _mi_os_purge_ex(mi_subproc_t* subproc, void* p, size_t size, bool allow_reset, size_t stats_size, mi_commit_fun_t* commit_fun, void* commit_fun_arg); + +size_t _mi_os_secure_guard_page_size(void); +bool _mi_os_secure_guard_page_set_at(mi_subproc_t* subproc, void* addr, mi_memid_t memid); +bool _mi_os_secure_guard_page_set_before(mi_subproc_t* subproc, void* addr, mi_memid_t memid); +bool _mi_os_secure_guard_page_reset_at(mi_subproc_t* subproc, void* addr, mi_memid_t memid); +bool _mi_os_secure_guard_page_reset_before(mi_subproc_t* subproc, void* addr, mi_memid_t memid); + +int _mi_os_numa_node(void); +int _mi_os_numa_node_count(void); + +void* _mi_os_alloc_aligned(mi_subproc_t* subproc, size_t size, size_t alignment, bool commit, bool allow_large, mi_memid_t* memid); +void* _mi_os_alloc_aligned_at_offset(mi_subproc_t* subproc, size_t size, size_t alignment, size_t align_offset, bool commit, bool allow_large, mi_memid_t* memid); + +void* _mi_os_get_aligned_hint(size_t try_alignment, size_t size); +bool _mi_os_canuse_large_page(size_t size, size_t alignment); +size_t _mi_os_large_page_size(void); +void* _mi_os_alloc_huge_os_pages(mi_subproc_t* subproc, size_t pages, int numa_node, mi_msecs_t max_secs, size_t* pages_reserved, size_t* psize, mi_memid_t* memid); + +#endif diff --git a/Include/internal/mimalloc/mimalloc/prim/prim-tls.h b/Include/internal/mimalloc/mimalloc/prim/prim-tls.h new file mode 100644 index 000000000000000..a594c71decb8448 --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/prim/prim-tls.h @@ -0,0 +1,445 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_PRIM_TLS_H +#define MI_INTERNAL_PRIM_TLS_H + +#include "../types.h" +#include "../heap/heap.h" +#include "../heap/theap.h" +#include "../util/threadlocal.h" + +// -------------------------------------------------------------------------- +// We need fast access to both a unique thread id (in `alloc/free.c:mi_free`) and +// to a thread-local theap pointer (in `alloc/alloc.c:mi_malloc`). +// +// For performance, we tend to use specialized code for various platforms. +// This leads to quite a few ifdefs but it is just for performance and there +// is always a portable fallback (based on regular thread local variables). +// +// Windows : use NtCurrentTeB and TlsAlloc (MI_TLS_MODEL_WIN32) +// Linux,FreeBSD : use thread locals with the initial-exec model (MI_TLS_MODEL_LOCAL) +// macOS : use pthread locals with assembly for the thread-id (MI_TLS_MODEL_PTHREADS) +// Android,OpenBSD : use pthread locals (MI_TLS_MODEL_PTHREADS). todo: maybe on Android MI_TLS_MODEL_LOCAL is better? +// -------------------------------------------------------------------------- + +static inline mi_threadid_t _mi_prim_thread_id(void); // get a unique id for a thread +static inline mi_theap_t* _mi_theap_default(void); // the default thread local theap +static inline mi_theap_t* _mi_theap_cached(void); // last used thread local theap using the _heap_ api +static inline bool _mi_thread_is_initialized(void); // a thread is initialized if it has a default theap +static inline mi_theap_t* _mi_heap_theap(mi_heap_t* heap); // get the thread local theap belonging to a heap +static inline mi_theap_t* _mi_heap_theap_peek(const mi_heap_t* heap); // get the theap but don't update _mi_theap_cached +static inline mi_theap_t* _mi_page_associated_theap_peek(const mi_page_t* page); // get the theap associated with a page (used in `mi_free_collect_mt`) + +// Regular exports +void _mi_tls_slots_init(void); +void _mi_tls_slots_done(void); +mi_threadid_t _mi_thread_id(void) mi_attr_noexcept; +void _mi_theap_default_set(mi_theap_t* theap); +void _mi_theap_cached_set(mi_theap_t* theap); + + +static inline bool mi_theap_matches_thread(mi_theap_t* theap) { + const mi_threadid_t tid = _mi_thread_id(); + return (theap==NULL || theap->tld==NULL || theap->tld->thread_id == tid || mi_theap_is_detached(theap)); +} + + +// Default TLS model +#if !defined(MI_TLS_MODEL_LOCAL) && !defined(MI_TLS_MODEL_PTHREADS) && !defined(MI_TLS_MODEL_FIXED) && !defined(MI_TLS_MODEL_WIN32) +#if defined(_WIN32) +#define MI_TLS_MODEL_WIN32 1 +#elif defined(__APPLE__) || defined(__OpenBSD__) || defined(__ANDROID__) // and FreeBSD? +#define MI_TLS_MODEL_PTHREADS 1 +#else +#define MI_TLS_MODEL_LOCAL 1 +#endif +#endif + + +//------------------------------------------------------------------- +// Access to the thread pointer +//------------------------------------------------------------------- + +// Do we have __builtin_thread_pointer? This would be the preferred way to get a unique thread id +// but unfortunately, it seems we cannot test for this reliably at this time (see issue #883) +// Nevertheless, it seems needed on older graviton platforms (see issue #851). +// For now, we only enable this for specific platforms. +#if !defined(MI_USE_BUILTIN_THREAD_POINTER) /* allow user override */ + #if !defined(__APPLE__) /* on apple (M1) the wrong register is read (tpidr_el0 instead of tpidrro_el0) so fall back to TLS slot assembly ()*/ \ + && !defined(__CYGWIN__) \ + && !defined(MI_LIBC_MUSL) \ + && (!defined(__clang_major__) || __clang_major__ >= 14) /* older clang versions emit bad code; fall back to using the TLS slot () */ + #if (defined(__GNUC__) && (__GNUC__ >= 7) && defined(__aarch64__)) /* aarch64 for older gcc versions (issue #851) */ \ + || (defined(__GNUC__) && (__GNUC__ >= 7) && defined(__riscv)) \ + || (defined(__GNUC__) && (__GNUC__ >= 11) && defined(__x86_64__)) \ + || (defined(__clang_major__) && (__clang_major__ >= 14) && (defined(__aarch64__) || defined(__x86_64__) || defined(__riscv))) + #define MI_USE_BUILTIN_THREAD_POINTER 1 + #endif + #endif +#endif + +#if defined(MI_PRIM_THREAD_POINTER) // potential user override +static inline void** mi_prim_thread_pointer(void) { + return MI_PRIM_THREAD_POINTER(); +} +#elif defined(_WIN32) +static inline void** mi_prim_thread_pointer(void) { + return (void**)NtCurrentTeb(); +} +#elif MI_USE_BUILTIN_THREAD_POINTER +static inline void** mi_prim_thread_pointer(void) { + return (void**)__builtin_thread_pointer(); +} +#elif defined(__GNUC__) && !defined(__CYGWIN__) + #if defined(__aarch64__) + static inline void** mi_prim_thread_pointer(void) { + void** tcb; + #if defined(__APPLE__) // M1, issue rgb(62, 76, 62) + __asm__ ("mrs %0, tpidrro_el0\n\tbic %0, %0, #7" : "=r" (tcb)); + #else + __asm__ ("mrs %0, tpidr_el0" : "=r" (tcb)); + #endif + return tcb; + } + #elif defined(__riscv) + static inline void** mi_prim_thread_pointer(void) { + void** tcb; + __asm__ ("mv %0, tp" : "=r" (tcb)); + return tcb; + } + #elif defined(__arm__) + static inline void** mi_prim_thread_pointer(void) { + void** tcb; + __asm__ volatile ("mrc p15, 0, %0, c13, c0, 3\n\tbic %0, %0, #3" : "=r" (tcb)); + return tcb; + } + #elif defined(__i386__) + static inline void** mi_prim_thread_pointer(void) { + void** tcb; + __asm__ ("movl %%gs:0, %0" : "=r" (tcb) : : ); // x86 32-bit always uses GS + return tcb; + } + #elif defined(__x86_64__) + static inline void** mi_prim_thread_pointer(void) { + void** tcb; + #if defined(__APPLE__) + __asm__ ("movq %%gs:0, %0" : "=r" (tcb) : : ); // x86_64 macOSX uses GS + #elif (MI_SIZE_SIZE==4) + __asm__ ("movl %%fs:0, %0" : "=r" (tcb) : : ); // x32 ABI + #else + __asm__ ("movq %%fs:0, %0" : "=r" (tcb) : : ); // x86_64 Linux, BSD uses FS + #endif + return tcb; + } + #else + #define MI_NO_THREAD_POINTER (1) + #endif +#elif MI_USE_PTHREADS && defined(__APPLE__) +static inline void** mi_prim_thread_pointer(void) { + return (void**)pthread_self(); +} +#else +#define MI_NO_THREAD_POINTER (1) +#endif + +#if !MI_NO_THREAD_POINTER +#define MI_HAS_TLS_SLOT (1) +static inline void* mi_prim_tls_slot(size_t slot) { + #if defined(_WIN32) + #if (_M_X64 || _M_AMD64) && !defined(_M_ARM64EC) + return (void*)__readgsqword((unsigned long)(slot*sizeof(void*))); // direct load at offset from gs + #elif _M_IX86 && !defined(_M_ARM64EC) + return (void*)__readfsdword((unsigned long)(slot*sizeof(void*))); // direct load at offset from fs + #else + return mi_prim_thread_pointer()[slot]; + #endif + #else + return mi_prim_thread_pointer()[slot]; + #endif +} + +static inline void mi_prim_tls_slot_set(size_t slot, void* value) { + mi_prim_thread_pointer()[slot] = value; +} +#endif + +/* ---------------------------------------------------------------------------------------- + Get a unique thread-id for a thread +---------------------------------------------------------------------------------------- */ + +// Get a unique id for the current thread. +#if defined(MI_PRIM_THREAD_ID) +static inline mi_threadid_t __mi_prim_thread_id(void) { + return MI_PRIM_THREAD_ID(); // used for example by CPython for a free threaded build (see python/cpython#115488) +} +#elif !MI_NO_THREAD_POINTER +static inline mi_threadid_t __mi_prim_thread_id(void) { + #if defined(__BIONIC__) + return (mi_threadid_t)mi_prim_tls_slot(1); + #else + return (mi_threadid_t)mi_prim_thread_pointer(); + #endif +} +#else +// otherwise use portable C, taking the address of a thread local variable (this is still very fast on most platforms). +extern mi_decl_hidden mi_decl_thread void* __mi_thread_id_helper; +static inline mi_threadid_t __mi_prim_thread_id(void) { + return (uintptr_t)&__mi_thread_id_helper; +} +#endif + +static inline mi_threadid_t _mi_prim_thread_id(void) { + const mi_threadid_t tid = __mi_prim_thread_id(); + mi_assert_internal(tid > MI_THREADID_DETACHED); + mi_assert_internal((tid & MI_PAGE_FLAG_MASK) == 0); // bottom 2 bits are clear? + return tid; +} + + +/* ---------------------------------------------------------------------------------------- +Get the thread local default theap: `_mi_theap_default()` (and the cached heap `_mi_theap_cached`). + +This is inlined here as it is on the fast path for allocation functions. +We have 4 models: + +- MI_TLS_MODEL_LOCAL: use regular thread local (default on Linux, FreeBSD, etc) + On most platforms (Linux, FreeBSD, NetBSD, etc), this just returns a + thread local variable (`__mi_theap_default`). With the initial-exec TLS model this ensures + that the storage will always be available and properly initialized (with an empty theap). + + On some platforms the underlying TLS implementation (or the loader) will call itself `malloc` + on a first access to a thread local and recurse in the MI_TLS_MODEL_LOCAL. + A way around this is to define MI_TLS_RECURSE_GUARD which adds an extra check if the process + is initialized before accessing the thread-local. This is a check in the fast path though + so this should be avoided. + +- MI_TLS_MODEL_PTHREADS: use `pthread_getspecific`. (default on macOS and OpenBSD, maybe good for Android as well?) + Use pthread local storage. Can be as fast as thread locals on many platforms (like recent macOS). + +- MI_TLS_MODEL_FIXED: use a fixed slot in the TLS block. + This reserves an unused and fixed TLS slot. This is fast and avoids the problem + where the underlying TLS implementation (or the loader) will call itself `malloc` + on a first access to a thread local (and recurse in the MI_TLS_MODEL_LOCAL). + This goes wrong though if the OS or a library uses the same fixed slot, and also + prevents multiple instances of mimalloc in the same process. + +- MI_TLS_MODEL_WIN32: use a dynamically allocated slot with TlsAlloc. (default on Windows) + We use TlsAlloc'd slot. First tries to use one of the "direct" first 64 slots which + are the fastest, but falls back to using "expansion" slots when needed (up to 1088 slots). + (If the allocated slot happens to always be under 64 for a particular program, + one might use cmake with `-DMI_WIN_DIRECT_TLS=ON` to skip the expansion slot test in the fast path.) + +Each model should define `MI_THEAP_INITASNULL` to signify that the initial value +returned from `_mi_theap_default()` can be `NULL` (instead of the address of the empty heap). +This incurs an extra check in the fast path (but can often be combined in an existing check). +------------------------------------------------------------------------------------------- */ + +#if !defined(MI_TLS_RECURSE_GUARD) && MI_TLS_MODEL_LOCAL && defined(__APPLE__) +#define MI_TLS_RECURSE_GUARD 1 // macOS can allocate on thread-local initialization +#endif + +// Declared this way to optimize register spills and branches +mi_decl_cold mi_decl_noinline mi_theap_t* _mi_theap_empty_get(void); + +static inline mi_theap_t* __mi_theap_empty(void) { + #if __GNUC__ + __asm(""); // prevent conditional load + return (mi_theap_t*)&_mi_theap_empty; + #else + return _mi_theap_empty_get(); + #endif +} + +#if MI_TLS_MODEL_LOCAL +// Thread local with an initial value (default on Linux). Very efficient. +extern mi_decl_hidden mi_decl_thread mi_theap_t* __mi_theap_default; // default theap to allocate from +extern mi_decl_hidden mi_decl_thread mi_theap_t* __mi_theap_cached; // theap from the last used heap + +// defined in `heap/init.c`; do not use these directly +extern mi_decl_hidden bool _mi_process_is_initialized; // has mi_process_init been called? + +static inline mi_theap_t* _mi_theap_default(void) { + #if defined(MI_TLS_RECURSE_GUARD) + if mi_unlikely(!_mi_process_is_initialized) return _mi_theap_empty_get(); + #endif + return __mi_theap_default; +} + +static inline mi_theap_t* _mi_theap_cached(void) { + return __mi_theap_cached; +} + +#elif MI_TLS_MODEL_PTHREADS +// Dynamic pthread slots. This can be fast depending on the platform (default for macOS and OpenBSD) +// On some platforms (like macOS), the loader might allocate on thread local declarations which +// can be avoided with pthreads. +#define MI_THEAP_INITASNULL 1 + +extern mi_decl_hidden _Atomic(pthread_key_t) _mi_theap_default_key; +extern mi_decl_hidden _Atomic(pthread_key_t) _mi_theap_cached_key; + +static inline mi_theap_t* _mi_theap_default(void) { + pthread_key_t key = mi_atomic_load_relaxed(&_mi_theap_default_key); + #if defined(__APPLE__) && defined(__aarch64__) && MI_HAS_TLS_SLOT + // on apple arm64, the pthread specific slots are direct slots; inline it to avoid a stack frame setup in `mi_malloc` + // todo: this is probably also the case on x64 and power pc? + if (key == MI_PTHREAD_KEY_INVALID) return NULL; + return (mi_theap_t*)mi_prim_tls_slot(key); + #else + return (mi_theap_t*)mi_pthread_key_get(key); + #endif +} + +static inline mi_theap_t* _mi_theap_cached(void) { + pthread_key_t key = mi_atomic_load_relaxed(&_mi_theap_cached_key); + #if defined(__APPLE__) && defined(__aarch64__) && MI_HAS_TLS_SLOT + if (key == MI_PTHREAD_KEY_INVALID) return NULL; + return (mi_theap_t*)mi_prim_tls_slot(key); + #else + return (mi_theap_t*)mi_pthread_key_get(key); + #endif +} + +#elif MI_TLS_MODEL_WIN32 +// Dynamic TLS slots -- this is the default on Windows. +#define MI_THEAP_INITASNULL 1 + +// We try to use direct slots (64 available), but can also use the expansion slots (upto 1024 extra available) +// See for the offsets. +#if MI_SIZE_SIZE==4 +#define MI_TLS_EXPANSION_SLOT (0x0F94 / MI_SIZE_SIZE) +#else +#define MI_TLS_EXPANSION_SLOT (0x1780 / MI_SIZE_SIZE) +#endif + +extern mi_decl_hidden _Atomic(size_t) _mi_theap_default_slot; +extern mi_decl_hidden _Atomic(size_t) _mi_theap_cached_slot; +extern mi_decl_hidden _Atomic(size_t) _mi_theap_default_expansion_slot; +extern mi_decl_hidden _Atomic(size_t) _mi_theap_cached_expansion_slot; + +static inline mi_theap_t* _mi_theap_default(void) { + const size_t slot = mi_atomic_load_relaxed(&_mi_theap_default_slot); + mi_theap_t* theap = (mi_theap_t*)mi_prim_tls_slot(slot); + #if !MI_WIN_DIRECT_TLS + if mi_unlikely(slot==MI_TLS_EXPANSION_SLOT) { // in TlsExpansionSlots ? + mi_theap_t** const eslots = (mi_theap_t**)theap; // theap is actually the expansion slot entry + if mi_likely(eslots!=NULL) { // is it initialized? (on this thread) + theap = eslots[mi_atomic_load_relaxed(&_mi_theap_default_expansion_slot)]; + } + } + #endif + return theap; +} + +static inline mi_theap_t* _mi_theap_cached(void) { + const size_t slot = mi_atomic_load_relaxed(&_mi_theap_cached_slot); + mi_theap_t* theap = (mi_theap_t*)mi_prim_tls_slot(slot); + #if !MI_WIN_DIRECT_TLS + if mi_unlikely(slot==MI_TLS_EXPANSION_SLOT) { // in TlsExpansionSlots ? + mi_theap_t** const eslots = (mi_theap_t**)theap; // theap is the expansion slot entry + if mi_likely(eslots!=NULL) { // is it initialized? (on this thread) + theap = eslots[mi_atomic_load_relaxed(&_mi_theap_cached_expansion_slot)]; + } + } + #endif + return theap; +} + +#elif MI_TLS_MODEL_FIXED +// Fixed TLS slot. Can be the fastest approach, but does not work if there are multiple instances of +// mimalloc in the same process. Most OS's do not have official user reserved fixed slots so this cannot be +// guaranteed to work in general. +#define MI_THEAP_INITASNULL 1 + +#if !MI_HAS_TLS_SLOT +#error this platform cannot support MI_TLS_MODEL_FIXED without defining mi_prim_tls_slot +#endif + +#if !defined(MI_TLS_MODEL_FIXED_DEFAULT) + #if defined(__APPLE__) && !defined(__POWERPC__) // macOS on arm64 or x64 + // we use the last two swift framework slots which seem unused. + // we may want to use slot 6 and 11 instead which are only used by Windows emulation. + // see for assigned slots + #define MI_TLS_MODEL_FIXED_DEFAULT 108 + #define MI_TLS_MODEL_FIXED_CACHED 109 + #elif defined(_WIN32) + // we use two seemingly unused fields in the Windows TEB. + // see + #define MI_TLS_MODEL_FIXED_DEFAULT 5 // arbitrary user pointer + #define MI_TLS_MODEL_FIXED_CACHED 7 // environment pointer (used by OS2) + #else + #error define the TLS model fixed slots (or change the TLS model away from MI_TLS_MODEL_FIXED) + #endif +#endif + +static inline mi_theap_t* _mi_theap_default(void) { + return (mi_theap_t*)mi_prim_tls_slot(MI_TLS_MODEL_FIXED_DEFAULT); +} + +static inline mi_theap_t* _mi_theap_cached(void) { + return (mi_theap_t*)mi_prim_tls_slot(MI_TLS_MODEL_FIXED_CACHED); +} + +#else +#error "no TLS model is defined for this platform?" +#endif + + +// Check if a thread is initialized (without using a thread-local if using fixed slots) +static inline bool _mi_thread_is_initialized(void) { + return mi_theap_is_initialized(_mi_theap_default()); +} + +// Get (and possible create) the theap belonging to a heap +// We cache the last accessed theap in `_mi_theap_cached` for better performance. +static inline mi_theap_t* _mi_heap_theap(mi_heap_t* heap) { + mi_theap_t* theap = _mi_theap_cached(); + #if MI_THEAP_INITASNULL + if mi_likely(theap!=NULL && mi_theap_heap_peek(theap)==heap) return theap; + #else + if mi_likely(mi_theap_heap_peek(theap)==heap) return theap; + #endif + return _mi_heap_theap_get_or_init(heap); +} + +static inline mi_theap_t* _mi_heap_theap_cached(mi_heap_t* heap) { + mi_theap_t* theap = _mi_theap_cached(); + #if MI_THEAP_INITASNULL + if mi_likely(theap!=NULL && mi_theap_heap_peek(theap)==heap) return theap; + #else + if mi_likely(mi_theap_heap_peek(theap)==heap) return theap; + #endif + return NULL; +} + +// Get the theap belonging to a heap without creating it if it is not yet initialized. +static inline mi_theap_t* _mi_heap_theap_peek(const mi_heap_t* heap) { + mi_theap_t* theap = _mi_theap_cached(); + #if MI_THEAP_INITASNULL + if mi_likely(theap!=NULL && mi_theap_heap_peek(theap)==heap) return theap; + #else + if mi_likely(mi_theap_heap_peek(theap)==heap) return theap; + #endif + theap = (mi_theap_t*)_mi_thread_local_get(heap->theap); // don't update the cache on a query + mi_assert_internal(theap==NULL || (!_mi_is_empty_theap(theap) && theap->heap==heap)); + return theap; +} + +// Find the associated theap or NULL if it does not exist (during shutdown) +// Should be fast as it is called in `alloc/free.c:mi_free_try_collect`. +static inline mi_theap_t* _mi_page_associated_theap_peek(const mi_page_t* page) { + mi_heap_t* const heap = page->heap; + mi_theap_t* const theap = (mi_theap_t*)_mi_thread_local_get(heap->theap); + if (theap==NULL || theap->tld==NULL /* heap destroy */) return NULL; + if (theap->heap != heap) return NULL; // should never happen, but can happen for a free across subprocesses, which can happen during pthread tls storage deallocation + mi_assert_internal(!_mi_is_empty_theap(theap) && mi_theap_matches_thread(theap)); + // note: for pages allocated by a detached theap, the returned theap may not be detached + return theap; +} + +#endif // MI_PRIM_TLS_H diff --git a/Include/internal/mimalloc/mimalloc/profile/pprof.h b/Include/internal/mimalloc/mimalloc/profile/pprof.h new file mode 100644 index 000000000000000..c2c641401e7a10e --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/profile/pprof.h @@ -0,0 +1,16 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_PPROF_H +#define MI_INTERNAL_PPROF_H + +#include "../types.h" + +void _mi_pprof_profiler_init(void); +void _mi_pprof_profiler_done(void); + +#endif diff --git a/Include/internal/mimalloc/mimalloc/profile/sample-guarded.h b/Include/internal/mimalloc/mimalloc/profile/sample-guarded.h new file mode 100644 index 000000000000000..4e8cd06272bfbfd --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/profile/sample-guarded.h @@ -0,0 +1,31 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_SAMPLE_GUARDED_H +#define MI_INTERNAL_SAMPLE_GUARDED_H + +#include "../types.h" +#include "sample-profile.h" + +void _mi_page_unguard_all(mi_page_t* page); +mi_decl_restrict void* _mi_theap_malloc_guarded(mi_theap_t* theap, size_t size, bool zero, mi_page_t** ppage) mi_attr_noexcept; +void _mi_page_block_unguard(mi_page_t* page, mi_block_t* block, void* p); +void _mi_theap_guarded_init(mi_theap_t* theap); + +#define MI_BLOCK_TAG_GUARDED (~MI_BLOCK_TAG_ALIGNED) + +static inline bool mi_block_ptr_is_guarded(const mi_block_t* block, const void* p) { +#if MI_GUARDED + const ptrdiff_t offset = (uint8_t*)p - (uint8_t*)block; + return (offset >= (ptrdiff_t)(sizeof(mi_block_t)) && block->next == MI_BLOCK_TAG_GUARDED); +#else + MI_UNUSED(block); MI_UNUSED(p); + return false; +#endif +} + +#endif diff --git a/Include/internal/mimalloc/mimalloc/profile/sample-profile.h b/Include/internal/mimalloc/mimalloc/profile/sample-profile.h new file mode 100644 index 000000000000000..551887095d25978 --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/profile/sample-profile.h @@ -0,0 +1,70 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_SAMPLE_PROFILE_H +#define MI_INTERNAL_SAMPLE_PROFILE_H + +#include "../types.h" + +mi_decl_restrict void* _mi_theap_malloc_sampled(mi_theap_t* theap, size_t req_size, bool zero, mi_page_t** ppage) mi_attr_noexcept; +size_t _mi_theap_update_sample_rate(mi_theap_t* theap); + +mi_decl_restrict void* _mi_theap_malloc_profiled(mi_theap_t* theap, size_t size, uint64_t requested_since_last_sample, bool zero, mi_page_t** ppage) mi_attr_noexcept; +void _mi_page_profile_on_free(mi_page_t* page, mi_block_t* block, void* p); +size_t _mi_theap_set_profile_sample_rate(mi_theap_t* theap, size_t sample_rate); + +// permanently exclude a theap from profiling (mirrors `mi_heap_profile_disable` but at the theap level); +// used for detached/meta theaps used to bootstrap thread/theap metadata, since sampling those can call +// back into the profiler while allocating on a not yet (re-)initialized thread, causing deadlock. +static inline void mi_theap_profile_disable(mi_theap_t* theap) { + theap->profile_disabled = true; + theap->profile_sample_rate = 0; + theap->profile_sample_countdown = 0; +} + +/* ------------------------------------------------------------------- + Guarded and profiled objects +------------------------------------------------------------------- */ + +#define MI_SAMPLE_RATE_MAX (SIZE_MAX/4) +#define MI_SAMPLE_COUNTDOWN_MAX (MI_MAX_ALLOC_SIZE) + +static inline bool mi_theap_should_sample(mi_theap_t* theap, size_t req_size) { + // note: this should return `true` on an empty theap so we initialize it's countdown to `-1`. + mi_assert_internal(req_size <= SIZE_MAX/2); + // const size_t sample_countdown = theap->sample_countdown - req_size; + // return ((mi_ssize_t)sample_countdown < 0); + return ((mi_ssize_t)theap->sample_countdown < (mi_ssize_t)req_size); +} + +// we always align guarded pointers in a block at an offset +// the block `next` field is then used as a tag to distinguish regular offset aligned blocks from guarded ones +#define MI_BLOCK_TAG_ALIGNED ((mi_encoded_t)(0)) +#define MI_BLOCK_TAG_PROFILED ((mi_encoded_t)(1)) + + +static inline bool mi_block_ptr_is_sampled(const mi_block_t* block, const void* p) { +#if MI_GUARDED || MI_PROFILE + const ptrdiff_t offset = (uint8_t*)p - (uint8_t*)block; + return (offset >= (ptrdiff_t)(sizeof(mi_block_t)) && block->next != MI_BLOCK_TAG_ALIGNED); +#else + MI_UNUSED(block); MI_UNUSED(p); + return false; +#endif +} + +static inline bool mi_profiler_is_enabled(const mi_profiler_t* prof) { + _Atomic(size_t)* penabled = (_Atomic(size_t)*)&prof->reserved; + return (mi_atomic_load_acquire(penabled) != 0); +} + +static inline bool mi_profiler_set_enabled(mi_profiler_t* prof, bool enable) { + _Atomic(size_t)* penabled = (_Atomic(size_t)*)&prof->reserved; + return (mi_atomic_exchange_release(penabled, (enable ? 1 : 0)) != 0); +} + +#endif diff --git a/Include/internal/mimalloc/mimalloc/qsbr.h b/Include/internal/mimalloc/mimalloc/qsbr.h new file mode 100644 index 000000000000000..a91e8ad98dde652 --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/qsbr.h @@ -0,0 +1,56 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +/* ---------------------------------------------------------------------------- + Quiescent State-Based Reclamation (qsbr). + QSBR as used in CPython is a technique for safe memory reclamation in concurrent + environments. In such case we need to delay free-ing pages until there are no + more concurrent readers; the interface below provides the necessary primitives + for managing QSBR state so mimalloc can delay freeing pages until it is + safe to do so (and efficiently reuse them as well). + + See `/test/test-qsbr.c` for a minimal example implementation. +-----------------------------------------------------------------------------*/ + +#pragma once +#ifndef MI_QSBR_H +#define MI_QSBR_H + +#include +#include + +#ifndef MI_QSBR +#if defined(MI_CPYTHON) +#define MI_QSBR 1 +#else +#define MI_QSBR 0 +#endif +#endif + +// Create a qsbr enabled heap +mi_decl_nodiscard mi_decl_export mi_heap_t* mi_heap_new_qsbr(void); + +#if MI_QSBR +// Define these for the host runtime to implement qsbr bookkeeping. +typedef struct mi_page_qsbr_s mi_page_qsbr_t; +typedef struct mi_heap_qsbr_s mi_heap_qsbr_t; + +// Host-defined runtime bookkeeping primitives; see doc/qsbr.md. (called by mimalloc for pages in qsbr heaps) +#ifdef __cplusplus +extern "C" { +#endif +void mi_heap_qsbr_init(mi_heap_qsbr_t* heap); +void mi_page_qsbr_clear(mi_page_qsbr_t* page); +bool mi_page_qsbr_is_clear(const mi_page_qsbr_t* page); +// Establish a goal if needed, then check whether release of the empty page must be delayed. +bool mi_page_qsbr_should_delay(mi_page_qsbr_t* page, mi_heap_qsbr_t* heap, size_t page_bytes); +#ifdef __cplusplus +} +#endif +#endif + +#endif diff --git a/Include/internal/mimalloc/mimalloc/readme.md b/Include/internal/mimalloc/mimalloc/readme.md new file mode 100644 index 000000000000000..1fa5832ef833864 --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/readme.md @@ -0,0 +1,21 @@ +# Internal Headers + +- [`types.h`](types.h): Defines the types of all internal mimalloc datastructures +and configuration options. +- [`decl.h`](decl.h): Compiler attributes, assertions, initialization macros, and platform-specific declarations. +- [`bits.h`](bits.h): Architecture dependent type definitions, and bit operations. +- [`atomic.h`](atomic.h): Portable atomic operations and locks. +- [`prim.h`](prim.h): Primitive and minimal portability API needed from a host system. +- [`track.h`](track.h): Memory-tracking integration for Valgrind, AddressSanitizer, and Windows event tracing. +- [`qsbr.h`](qsbr.h): Quiescent-state based reclamation primitives (for runtime systems like CPython). +- [`internal.h`](internal.h): Deprecated: umbrella header for internal allocator declarations and utilities. + +# Source Headers + +- [`alloc`](alloc): Internal allocation, freeing, and page-management interfaces. +- [`arena`](arena): Arena management, slice allocation, and arena-backed page interfaces. +- [`heap`](heap): Heap, thread-local heap, subprocess, initialization, and page-map interfaces. +- [`prim`](prim): Higher-level OS memory operations and primitive thread-local access. +- [`profile`](profile): Sampled allocation, guarded allocation, and profiler interfaces. +- [`util`](util): Runtime utilities, options, randomness, statistics, and dynamic thread-local interfaces. + diff --git a/Include/internal/mimalloc/mimalloc/track.h b/Include/internal/mimalloc/mimalloc/track.h index fa1a048d846a9cf..f2a24aacfe617cb 100644 --- a/Include/internal/mimalloc/mimalloc/track.h +++ b/Include/internal/mimalloc/mimalloc/track.h @@ -5,8 +5,8 @@ terms of the MIT license. A copy of the license can be found in the file "LICENSE" at the root of this distribution. -----------------------------------------------------------------------------*/ #pragma once -#ifndef MIMALLOC_TRACK_H -#define MIMALLOC_TRACK_H +#ifndef MI_TRACK_H +#define MI_TRACK_H /* ------------------------------------------------------------------------------------------------------ Track memory ranges with macros for tools like Valgrind address sanitizer, or other memory checkers. @@ -47,7 +47,7 @@ defined, undefined, or not accessible at all: // valgrind tool #define MI_TRACK_ENABLED 1 -#define MI_TRACK_HEAP_DESTROY 1 // track free of individual blocks on heap_destroy +#define MI_TRACK_HEAP_DESTROY 1 // track free of individual blocks on theap_destroy #define MI_TRACK_TOOL "valgrind" #include @@ -82,14 +82,26 @@ defined, undefined, or not accessible at all: #define MI_TRACK_HEAP_DESTROY 1 #define MI_TRACK_TOOL "ETW" -#define WIN32_LEAN_AND_MEAN -#include #include "../src/prim/windows/etw.h" -#define mi_track_init() EventRegistermicrosoft_windows_mimalloc(); +#define mi_track_init() EventRegistermicrosoft_windows_mimalloc() +#define mi_track_done() EventUnregistermicrosoft_windows_mimalloc() #define mi_track_malloc_size(p,reqsize,size,zero) EventWriteETW_MI_ALLOC((UINT64)(p), size) #define mi_track_free_size(p,size) EventWriteETW_MI_FREE((UINT64)(p), size) +#elif defined(MI_TRACK_CUSTOM) +// application-defined hooks + +#define MI_TRACK_ENABLED 0 +#define MI_TRACK_HEAP_DESTROY 0 +#define MI_TRACK_TOOL "custom" + +void mi_track_malloc_hook(const void* p, size_t reqsize, size_t size, bool zero) mi_attr_noexcept; +void mi_track_free_hook(const void* p, size_t size) mi_attr_noexcept; + +#define mi_track_malloc_size(p,reqsize,size,zero) mi_track_malloc_hook((p),(reqsize),(size),(zero)) +#define mi_track_free_size(p,size) mi_track_free_hook((p),(size)) + #else // no tracking @@ -106,7 +118,7 @@ defined, undefined, or not accessible at all: // Utility definitions #ifndef mi_track_resize -#define mi_track_resize(p,oldsize,newsize) mi_track_free_size(p,oldsize); mi_track_malloc(p,newsize,false) +#define mi_track_resize(p,oldsize,newsize) do{ mi_track_free_size(p,oldsize); mi_track_malloc(p,newsize,false); } while(0) #endif #ifndef mi_track_align @@ -117,6 +129,10 @@ defined, undefined, or not accessible at all: #define mi_track_init() #endif +#ifndef mi_track_done +#define mi_track_done() +#endif + #ifndef mi_track_mem_defined #define mi_track_mem_defined(p,size) #endif @@ -130,18 +146,18 @@ defined, undefined, or not accessible at all: #endif -#if MI_PADDING +#if MI_PADDING_CHECK_BYTES && !MI_GUARDED #define mi_track_malloc(p,reqsize,zero) \ - if ((p)!=NULL) { \ + do { if ((p)!=NULL) { \ mi_assert_internal(mi_usable_size(p)==(reqsize)); \ mi_track_malloc_size(p,reqsize,reqsize,zero); \ - } + } } while(0) #else #define mi_track_malloc(p,reqsize,zero) \ - if ((p)!=NULL) { \ + do { if ((p)!=NULL) { \ mi_assert_internal(mi_usable_size(p)>=(reqsize)); \ mi_track_malloc_size(p,reqsize,mi_usable_size(p),zero); \ - } + } } while(0) #endif -#endif +#endif // MI_TRACK_H diff --git a/Include/internal/mimalloc/mimalloc/types.h b/Include/internal/mimalloc/mimalloc/types.h index 286e7bf668312db..ccbf0cb90ccf962 100644 --- a/Include/internal/mimalloc/mimalloc/types.h +++ b/Include/internal/mimalloc/mimalloc/types.h @@ -1,31 +1,40 @@ /* ---------------------------------------------------------------------------- -Copyright (c) 2018-2023, Microsoft Research, Daan Leijen +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen This is free software; you can redistribute it and/or modify it under the terms of the MIT license. A copy of the license can be found in the file "LICENSE" at the root of this distribution. -----------------------------------------------------------------------------*/ #pragma once -#ifndef MIMALLOC_TYPES_H -#define MIMALLOC_TYPES_H +#ifndef MI_TYPES_H +#define MI_TYPES_H // -------------------------------------------------------------------------- // This file contains the main type definitions for mimalloc: -// mi_heap_t : all data for a thread-local heap, contains -// lists of all managed heap pages. -// mi_segment_t : a larger chunk of memory (32GiB) from where pages -// are allocated. +// mi_heap_t : all data for a heap; usually there is just one main default heap. +// mi_theap_t : a thread local heap belonging to a specific heap: +// maintains lists of thread-local heap pages that have free space. // mi_page_t : a mimalloc page (usually 64KiB or 512KiB) from -// where objects are allocated. +// where objects of a single size are allocated. +// Note: we write "OS page" for OS memory pages while +// using plain "page" for mimalloc pages (`mi_page_t`). +// mi_arena_t : a large memory area where pages are allocated (process shared) +// mi_tld_t : thread local data +// mi_subproc_t : all heaps belong to a sub-process (usually just the main one) // -------------------------------------------------------------------------- - +#include "../mimalloc.h" +#include "../mimalloc-stats.h" +#include "../mimalloc-profile.h" #include // ptrdiff_t #include // uintptr_t, uint16_t, etc -#include "atomic.h" // _Atomic +#include // bool +#include // SIZE_MAX etc. +#include // error codes -#ifdef _MSC_VER -#pragma warning(disable:4214) // bitfield is not int -#endif +#include "decl.h" // compiler dependent definitions +#include "bits.h" // size defines (MI_SIZE_SIZE etc), bit operations +#include "atomic.h" // _Atomic primitives +#include "qsbr.h" // quiescent state-based reclamation (qsbr) primitives (for runtime systems like CPython) // Minimal alignment necessary. On most platforms 16 bytes are needed // due to SSE registers for example. This must be at least `sizeof(void*)` @@ -33,47 +42,8 @@ terms of the MIT license. A copy of the license can be found in the file #define MI_MAX_ALIGN_SIZE 16 // sizeof(max_align_t) #endif -#define MI_CACHE_LINE 64 -#if defined(_MSC_VER) -#pragma warning(disable:4127) // suppress constant conditional warning (due to MI_SECURE paths) -#pragma warning(disable:26812) // unscoped enum warning -#define mi_decl_noinline __declspec(noinline) -#define mi_decl_thread __declspec(thread) -#define mi_decl_cache_align __declspec(align(MI_CACHE_LINE)) -#elif (defined(__GNUC__) && (__GNUC__ >= 3)) || defined(__clang__) // includes clang and icc -#define mi_decl_noinline __attribute__((noinline)) -#define mi_decl_thread __thread -#define mi_decl_cache_align __attribute__((aligned(MI_CACHE_LINE))) -#else -#define mi_decl_noinline -#define mi_decl_thread __thread // hope for the best :-) -#define mi_decl_cache_align -#endif - -#if (MI_DEBUG) -#if defined(_MSC_VER) -#define mi_decl_noreturn __declspec(noreturn) -#elif (defined(__GNUC__) && (__GNUC__ >= 3)) || defined(__clang__) -#define mi_decl_noreturn __attribute__((__noreturn__)) -#else -#define mi_decl_noreturn -#endif - -/* - * 'cold' attribute seems to have been fully supported since GCC 4.x. - * See https://github.com/gcc-mirror/gcc/commit/52bf96d2f299e9e6. - */ -#if (defined(__GNUC__) && (__GNUC__ >= 4)) || defined(__clang__) -#define mi_decl_cold __attribute__((cold)) -#else -#define mi_decl_cold -#endif - -#if (defined(__GNUC__) && defined(__THROW)) -#define mi_decl_throw __THROW -#else -#define mi_decl_throw -#endif +#if MI_MAX_ALIGN_SIZE < MI_INTPTR_SIZE +#error MI_MAX_ALIGN_SIZE must be at least MI_INTPTR_SIZE #endif // ------------------------------------------------------ @@ -87,41 +57,77 @@ terms of the MIT license. A copy of the license can be found in the file // #define MI_TRACK_VALGRIND 1 // #define MI_TRACK_ASAN 1 // #define MI_TRACK_ETW 1 +// #define MI_TRACK_CUSTOM 1 -// Define MI_STAT as 1 to maintain statistics; set it to 2 to have detailed statistics (but costs some performance). -// #define MI_STAT 1 +// Define MI_STATS as 1 to maintain statistics; set it to 2 to have detailed statistics (but costs some performance). +// #define MI_STATS 1 // Define MI_SECURE to enable security mitigations -// #define MI_SECURE 1 // guard page around metadata -// #define MI_SECURE 2 // guard page around each mimalloc page -// #define MI_SECURE 3 // encode free lists (detect corrupted free list (buffer overflow), and invalid pointer free) -// #define MI_SECURE 4 // checks for double free. (may be more expensive) +// #define MI_SECURE 1 // check invalid pointer free, guard pages around meta data, randomize arena allocation addresses (like ASLR), abort on detected meta data corruption +// #define MI_SECURE 2 // randomize relative allocation addresses within mimalloc pages +// #define MI_SECURE 3 // check buffer overflow, check double free, encode free lists (and detect corrupted free lists) +// #define MI_SECURE 4 // same as level 3 for now (`-DMI_SECURE=ON`) +// #define MI_SECURE 5 // guard page at the end of each mimalloc page (expensive!) (`-DMI_SECURE_FULL=ON`), and byte-precise buffer overflow checks. #if !defined(MI_SECURE) #define MI_SECURE 0 #endif -// Define MI_DEBUG for debug mode -// #define MI_DEBUG 1 // basic assertion checks and statistics, check double free, corrupted free list, and invalid pointer free. -// #define MI_DEBUG 2 // + internal assertion checks +// Define MI_DEBUG for assertion and invariant checking +// #define MI_DEBUG 1 // basic assertion checks and statistics, check double free, corrupted free list, and invalid pointer free. (cmake -DMI_DEBUG=ON) +// #define MI_DEBUG 2 // + internal assertion checks (cmake -DMI_DEBUG_INTERNAL=ON) // #define MI_DEBUG 3 // + extensive internal invariant checking (cmake -DMI_DEBUG_FULL=ON) #if !defined(MI_DEBUG) -#if !defined(NDEBUG) || defined(_DEBUG) +#if defined(MI_BUILD_RELEASE) || defined(NDEBUG) +#define MI_DEBUG 0 +#else #define MI_DEBUG 2 +#endif +#endif + +// Statistics (0=only essential, 1=detailed (fast, can be enabled always), 2=slightly more expensive tracking of precise requested bytes) +#ifndef MI_STATS +#if (MI_DEBUG>0) +#define MI_STATS 2 #else -#define MI_DEBUG 0 +#define MI_STATS 1 +#endif +#endif + +// Enable profiling support (0=off, 1=fast, can be enabled always, 2=allow fine-grained sample rates(<64 KiB), a tad more expensive) +#ifndef MI_PROFILE +#define MI_PROFILE 1 +#endif + +// Enable guard pages behind objects of a certain size (set by the MIMALLOC_GUARDED_MIN/MAX/SAMPLE_RATE options) +#if !defined(MI_GUARDED) && MI_DEBUG && !defined(NDEBUG) && !MI_OPT_FREE_SMALL +#define MI_GUARDED 1 +#endif + +// For profiling or guarded pages we need to sample every once in a while. (0=no sampling, 1=fast, 2=allow fine grained sample rates(<64 KiB), a tad more expensive) +#ifndef MI_SAMPLE +#if MI_PROFILE>1 || MI_GUARDED>1 + #define MI_SAMPLE 2 +#elif MI_PROFILE || MI_GUARDED + #define MI_SAMPLE 1 +#else + #define MI_SAMPLE 0 #endif #endif -// Reserve extra padding at the end of each block to be more resilient against heap block overflows. -// The padding can detect buffer overflow on free. -#if !defined(MI_PADDING) && (MI_SECURE>=3 || MI_DEBUG>=1 || (MI_TRACK_VALGRIND || MI_TRACK_ASAN || MI_TRACK_ETW)) +// Reserve extra padding at the end of each block to be more resilient against theap block overflows. +// The padding can detect heap-block overflow on free, and provides byte-precise `mi_usable_size`. +#if !defined(MI_PADDING) +#if (MI_SECURE>=3 || MI_DEBUG>=1 || (MI_TRACK_VALGRIND || MI_TRACK_ASAN || MI_TRACK_ETW)) #define MI_PADDING 1 +#else +#define MI_PADDING 0 +#endif #endif -// Check padding bytes; allows byte-precise buffer overflow detection -#if !defined(MI_PADDING_CHECK) && MI_PADDING && (MI_SECURE>=3 || MI_DEBUG>=1) -#define MI_PADDING_CHECK 1 +// Check for byte-precise buffer overflow? +#if !defined(MI_PADDING_CHECK_BYTES) && MI_PADDING && (MI_SECURE>=5 || MI_DEBUG>=1) +#define MI_PADDING_CHECK_BYTES 1 #endif @@ -131,121 +137,255 @@ terms of the MIT license. A copy of the license can be found in the file #define MI_ENCODE_FREELIST 1 #endif +// Deprecated (double-free is now checked with padding) +// #if (MI_ENCODE_FREELIST && (MI_SECURE>=4 || MI_DEBUG!=0)) +// #define MI_CHECK_DOUBLE_FREE 1 +// #endif -// We used to abandon huge pages but to eagerly deallocate if freed from another thread, -// but that makes it not possible to visit them during a heap walk or include them in a -// `mi_heap_destroy`. We therefore instead reset/decommit the huge blocks if freed from -// another thread so most memory is available until it gets properly freed by the owning thread. -// #define MI_HUGE_PAGE_ABANDON 1 - +#if MI_SECURE>=4 || MI_PADDING || (MI_FREE_IS_CHECKED && MI_FREE_USE_PAGEMAP) +#define MI_PAGE_KEY_COUNT 2 +#else +#define MI_PAGE_KEY_COUNT 1 +#endif -// ------------------------------------------------------ -// Platform specific values -// ------------------------------------------------------ +// Enable large pages for objects between 64KiB and 512KiB. +// This should perhaps be disabled by default as for many workloads the block sizes above 64 KiB +// are quite random which can lead to too many partially used large pages (but see issue #1104). +#ifndef MI_ENABLE_LARGE_PAGES +#define MI_ENABLE_LARGE_PAGES 1 +#endif -// ------------------------------------------------------ -// Size of a pointer. -// We assume that `sizeof(void*)==sizeof(intptr_t)` -// and it holds for all platforms we know of. -// -// However, the C standard only requires that: -// p == (void*)((intptr_t)p)) -// but we also need: -// i == (intptr_t)((void*)i) -// or otherwise one might define an intptr_t type that is larger than a pointer... -// ------------------------------------------------------ +// Place page meta info at the start of the page area or keep it separate? +// Separate keeps the page info at the arena start (default) which is more secure +// and reduces wasted space due to alignment and block sizes. +#if !defined(MI_PAGE_META_IS_SEPARATED) +#if MI_PAGE_MAP_FLAT +#define MI_PAGE_META_IS_SEPARATED 0 +#else +#define MI_PAGE_META_IS_SEPARATED 1 +#endif +#endif -#if INTPTR_MAX > INT64_MAX -# define MI_INTPTR_SHIFT (4) // assume 128-bit (as on arm CHERI for example) -#elif INTPTR_MAX == INT64_MAX -# define MI_INTPTR_SHIFT (3) -#elif INTPTR_MAX == INT32_MAX -# define MI_INTPTR_SHIFT (2) +// We can choose to page meta info aligned at the start of every MI_PAGE_META_ALIGNED_CHUNKS +// This can be used to have a faster `mi_free(_small)` as we can avoid a page_map lookup. +// This only works if valid pointers are passed to `mi_free` though. However, checked +// free `mi_cfree` can still use the page map to validate pointers. +#if !MI_FREE_IS_CHECKED && !MI_FREE_USE_PAGEMAP +#if MI_PAGE_META_IS_SEPARATED +#define MI_PAGE_META_IS_ALIGNED 1 +#define MI_PAGE_META_ALIGNED_CHUNKS MI_SIZE_SIZE #else -#error platform pointers must be 32, 64, or 128 bits +#warning "cannot optimize free with alignment since the page meta data is not separated (due to MI_PAGE_MAP_FLAT?)" +#endif #endif -#if SIZE_MAX == UINT64_MAX -# define MI_SIZE_SHIFT (3) -typedef int64_t mi_ssize_t; -#elif SIZE_MAX == UINT32_MAX -# define MI_SIZE_SHIFT (2) -typedef int32_t mi_ssize_t; +// We can choose to only put page info of small pages at the start of the page area. +// This can be used to have a slightly faster `mi_free_small` function for specialized +// cases (like language runtime systems). +#if !defined(MI_PAGE_META_SMALL_IS_ALIGNED) +#if defined(MI_OPT_FREE_SMALL) && MI_OPT_FREE_SMALL==0 +#define MI_PAGE_META_SMALL_IS_ALIGNED 0 +#elif (MI_OPT_FREE_SMALL || MI_PAGE_META_IS_ALIGNED) && !MI_SECURE && !MI_GUARDED // guarded can put small allocations into big blocks in a medium page (which would make `mi_free_small` fail) +#define MI_PAGE_META_SMALL_IS_ALIGNED 1 #else -#error platform objects must be 32 or 64 bits +#define MI_PAGE_META_SMALL_IS_ALIGNED 0 +#endif +#endif + +// Configuration checks +#if !MI_PAGE_META_IS_SEPARATED && MI_SECURE +#error "secure mode should use separated page infos" +#endif +#if MI_PAGE_META_SMALL_IS_ALIGNED && MI_SECURE +#error "secure mode cannot use MI_OPT_FREE_SMALL (MI_PAGE_META_SMALL_IS_ALIGNED)" +#endif +#if MI_PAGE_META_IS_SEPARATED && MI_PAGE_MAP_FLAT +#error "cannot have a flat page map with separated page infos" +#endif + + +// -------------------------------------------------------------- +// Sizes of internal data-structures +// (comments specify sizes on 64-bit, usually 32-bit is halved) +// -------------------------------------------------------------- + +// Main size parameter; determines max arena sizes and max arena object sizes etc. +#ifndef MI_ARENA_SLICE_SHIFT + #ifdef MI_SMALL_PAGE_SHIFT // backward compatibility + #define MI_ARENA_SLICE_SHIFT MI_SMALL_PAGE_SHIFT + #elif MI_SECURE>=5 && __APPLE__ && MI_ARCH_ARM64 + #define MI_ARENA_SLICE_SHIFT (17) // 128 KiB to not waste too much due to 16 KiB guard pages + #elif MI_SECURE>=5 && defined(PAGE_SIZE) && !defined(__wasi__) // PAGE_SIZE is not a preprocessor constant on WASI + #if PAGE_SIZE >= 16*MI_KiB + #define MI_ARENA_SLICE_SHIFT (17) + #endif + #endif + #ifndef MI_ARENA_SLICE_SHIFT + #define MI_ARENA_SLICE_SHIFT (13 + MI_SIZE_SHIFT) // 64 KiB (32 KiB on 32-bit) + #endif +#endif +#if MI_ARENA_SLICE_SHIFT < 13 +#error Arena slices should be at least 8KiB +#endif + +#ifndef MI_BCHUNK_BITS_SHIFT + #if MI_ARENA_SLICE_SHIFT <= 13 // <= 8KiB + #define MI_BCHUNK_BITS_SHIFT (7) // 128 bits + #elif MI_ARENA_SLICE_SHIFT < 16 // <= 32KiB + #define MI_BCHUNK_BITS_SHIFT (8) // 256 bits + #else + #define MI_BCHUNK_BITS_SHIFT (6 + MI_SIZE_SHIFT) // 512 bits (or 256 on 32-bit) + #endif +#endif + +#define MI_BCHUNK_BITS (1 << MI_BCHUNK_BITS_SHIFT) // sub-bitmaps in arena's are "bchunks" of 512 bits +#define MI_ARENA_SLICE_SIZE (MI_ZU(1) << MI_ARENA_SLICE_SHIFT) // arena's allocate in slices of 64 KiB +#define MI_ARENA_SLICE_ALIGN (MI_ARENA_SLICE_SIZE) +#define MI_ARENA_CHUNK_SIZE (MI_BCHUNK_BITS * MI_ARENA_SLICE_SIZE) + +#define MI_ARENA_MIN_OBJ_SLICES (1) +#define MI_ARENA_MAX_CHUNK_OBJ_SLICES (MI_BCHUNK_BITS) // 32 MiB (or 8 MiB on 32-bit) + +#define MI_ARENA_MIN_OBJ_SIZE (MI_ARENA_MIN_OBJ_SLICES * MI_ARENA_SLICE_SIZE) +#define MI_ARENA_MAX_CHUNK_OBJ_SIZE (MI_ARENA_MAX_CHUNK_OBJ_SLICES * MI_ARENA_SLICE_SIZE) + +#if MI_ARENA_MAX_CHUNK_OBJ_SIZE < MI_SIZE_SIZE*1024 +#error maximum object size may be too small to hold local thread data #endif -#if (SIZE_MAX/2) > LONG_MAX -# define MI_ZU(x) x##ULL -# define MI_ZI(x) x##LL +#define MI_SMALL_PAGE_SIZE MI_ARENA_MIN_OBJ_SIZE // 64 KiB +#define MI_MEDIUM_PAGE_SIZE (8*MI_SMALL_PAGE_SIZE) // 512 KiB (=byte in the bchunk bitmap) +#define MI_LARGE_PAGE_SIZE (MI_SIZE_SIZE*MI_MEDIUM_PAGE_SIZE) // 4 MiB (=word in the bchunk bitmap) + + +// Maximum number of size classes. (spaced exponentially in 12.5% increments) +#if MI_BIN_HUGE != 73U +#error "mimalloc internal: expecting 73 bins" +#endif +#define MI_BIN_FULL (MI_BIN_HUGE+1) +#define MI_BIN_COUNT (MI_BIN_FULL+1) + +// We never allocate more than PTRDIFF_MAX (see also ) +#define MI_MAX_ALLOC_SIZE PTRDIFF_MAX + +// Minimal commit for a page on-demand commit +#define MI_PAGE_MIN_COMMIT_SIZE (16*MI_KiB) /* MI_ARENA_SLICE_SIZE */ + +#if MI_PAGE_META_IS_ALIGNED +#define MI_PAGE_META_ALIGNED_COUNT (MI_PAGE_META_ALIGNED_CHUNKS * MI_BCHUNK_BITS) +#define MI_PAGE_META_ALIGNMENT (MI_PAGE_META_ALIGNED_COUNT * MI_ARENA_SLICE_SIZE) // 256 MiB (32 MiB on 32-bit) +#define MI_ARENA_ALIGNMENT MI_PAGE_META_ALIGNMENT #else -# define MI_ZU(x) x##UL -# define MI_ZI(x) x##L +#define MI_ARENA_ALIGNMENT MI_ARENA_SLICE_ALIGN #endif -#define MI_INTPTR_SIZE (1< 4 -#define MI_SEGMENT_SHIFT ( 9 + MI_SEGMENT_SLICE_SHIFT) // 32MiB -#else -#define MI_SEGMENT_SHIFT ( 7 + MI_SEGMENT_SLICE_SHIFT) // 4MiB on 32-bit -#endif -#define MI_SMALL_PAGE_SHIFT (MI_SEGMENT_SLICE_SHIFT) // 64KiB -#define MI_MEDIUM_PAGE_SHIFT ( 3 + MI_SMALL_PAGE_SHIFT) // 512KiB +// --------------------------------------------------------------- +// a memory id tracks the provenance of arena/OS allocated memory +// --------------------------------------------------------------- +// Memory can reside in arena's, direct OS allocated, meta-data pages, or statically allocated. +// The memid keeps track of this. +typedef enum mi_memkind_e { + MI_MEM_NONE, // not allocated (or static) + MI_MEM_EXTERNAL, // not owned by mimalloc but provided externally (via `mi_manage_os_memory` for example) + MI_MEM_STATIC, // allocated in a static area and should not be freed (the initial main theap data for example (`heap/init.c`)) + MI_MEM_OS, // allocated from the OS + MI_MEM_OS_HUGE, // allocated as huge OS pages (usually 1GiB, pinned to physical memory) + MI_MEM_OS_REMAP, // allocated in a remapable area (i.e. using `mremap`) + MI_MEM_ARENA, // allocated from an arena (the usual case) (`arena/arena-alloc.c`) + MI_MEM_MALLOC // allocated with mi_malloc +} mi_memkind_t; -// Derived constants -#define MI_SEGMENT_SIZE (MI_ZU(1)<= MI_MEM_OS && memkind <= MI_MEM_OS_REMAP); +} -#define MI_SMALL_PAGE_SIZE (MI_ZU(1)<= 655360) -#error "mimalloc internal: define more bins" -#endif +typedef struct mi_memid_arena_info { + mi_arena_t* arena; // arena that contains this memory + uint32_t slice_index; // slice index in the arena + uint32_t slice_count; // allocated slices +} mi_memid_arena_info_t; + +typedef struct mi_memid_malloc_info { + void* base; // returned pointer + size_t size; // allocated size +} mi_memid_malloc_info_t; + +typedef struct mi_memid_s { + union { + mi_memid_os_info_t os; // only used for MI_MEM_OS(_HUGE/_REMAP) + mi_memid_arena_info_t arena; // only used for MI_MEM_ARENA + mi_memid_malloc_info_t malloc; // only used for MI_MEM_MALLOC + } mem; + mi_memkind_t memkind; + bool is_pinned; // `true` if we cannot decommit/reset/protect in this memory (e.g. when allocated using large (2Mib) or huge (1GiB) OS pages) + bool initially_committed;// `true` if the memory was originally allocated as committed + bool initially_zero; // `true` if the memory was originally zero initialized +} mi_memid_t; -// Maximum slice offset (15) -#define MI_MAX_SLICE_OFFSET ((MI_ALIGNMENT_MAX / MI_SEGMENT_SLICE_SIZE) - 1) +#define MI_MEMID_INIT(kind) {{{NULL,0}}, kind, true /* pinned */, true /* committed */, false /* zero */ } +#define MI_MEMID_STATIC MI_MEMID_INIT(MI_MEM_STATIC) + +static inline bool mi_memid_is_os(mi_memid_t memid) { + return mi_memkind_is_os(memid.memkind); +} -// Used as a special value to encode block sizes in 32 bits. -#define MI_HUGE_BLOCK_SIZE ((uint32_t)(2*MI_GiB)) +static inline bool mi_memid_needs_no_free(mi_memid_t memid) { + return mi_memkind_needs_no_free(memid.memkind); +} -// blocks up to this size are always allocated aligned -#define MI_MAX_ALIGN_GUARANTEE (8*MI_MAX_ALIGN_SIZE) +static inline mi_arena_t* mi_memid_arena(mi_memid_t memid) { + return (memid.memkind == MI_MEM_ARENA ? memid.mem.arena.arena : NULL); +} -// Alignments over MI_ALIGNMENT_MAX are allocated in dedicated huge page segments -#define MI_ALIGNMENT_MAX (MI_SEGMENT_SIZE >> 1) +// defined in arena.c +mi_memid_t _mi_memid_create(mi_memkind_t memkind); +mi_memid_t _mi_memid_none(void); +mi_memid_t _mi_memid_create_os(void* base, size_t size, bool committed, bool is_zero, bool is_large); +mi_memid_t _mi_memid_create_static(void* p, size_t size); +mi_memid_t _mi_memid_create_malloc(void* p, size_t size, bool iszero); +size_t _mi_memid_size(mi_memid_t memid); // ------------------------------------------------------ @@ -261,44 +401,57 @@ typedef size_t mi_threadid_t; // free lists contain blocks typedef struct mi_block_s { - _Atomic(mi_encoded_t) next; + mi_encoded_t next; } mi_block_t; -// The delayed flags are used for efficient multi-threaded free-ing -typedef enum mi_delayed_e { - MI_USE_DELAYED_FREE = 0, // push on the owning heap thread delayed list - MI_DELAYED_FREEING = 1, // temporary: another thread is accessing the owning heap - MI_NO_DELAYED_FREE = 2, // optimize: push on page local thread free queue if another block is already in the heap thread delayed free list - MI_NEVER_DELAYED_FREE = 3 // sticky, only resets on page reclaim -} mi_delayed_t; +// The page flags are put in the bottom 2 bits of the thread_id (for a fast test in `mi_free`) +// If `has_interior_pointers` is true if the page has pointers at an offset in a block (so we have to unalign to the block start before free-ing) +// `in_full_queue` is true if the page is full and resides in the full queue (so we move it to a regular queue on free-ing) +#define MI_PAGE_IN_FULL_QUEUE MI_ZU(0x01) +#define MI_PAGE_HAS_INTERIOR_POINTERS MI_ZU(0x02) +#define MI_PAGE_FLAG_MASK MI_ZU(0x03) +#define MI_PAGE_FLAG_BITS (2) +typedef size_t mi_page_flags_t; - -// The `in_full` and `has_aligned` page flags are put in a union to efficiently -// test if both are false (`full_aligned == 0`) in the `mi_free` routine. -#if !MI_TSAN -typedef union mi_page_flags_s { - uint8_t full_aligned; - struct { - uint8_t in_full : 1; - uint8_t has_aligned : 1; - } x; -} mi_page_flags_t; -#else -// under thread sanitizer, use a byte for each flag to suppress warning, issue #130 -typedef union mi_page_flags_s { - uint16_t full_aligned; - struct { - uint8_t in_full; - uint8_t has_aligned; - } x; -} mi_page_flags_t; -#endif +// There are two special threadid's: 0 for pages that are abandoned (and not in a theap queue), +// and 4 for abandoned & mapped threads -- abandoned-mapped pages are abandoned but also mapped +// in an arena (in `mi_heap_t.arena_pages.pages_abandoned`) so these can be quickly found for reuse. +// Abandoning partially used pages allows for sharing of this memory between threads (in particular if threads are blocked) +#define MI_THREADID_ABANDONED MI_ZU(0) +#define MI_THREADID_ABANDONED_MAPPED (MI_ZU(1) << MI_PAGE_FLAG_BITS) +#define MI_THREADID_DETACHED (MI_ZU(2) << MI_PAGE_FLAG_BITS) // Thread free list. -// We use the bottom 2 bits of the pointer for mi_delayed_t flags +// Points to a list of blocks that are freed by other threads. +// The least-bit is set if the page is owned by the current thread. (`mi_page_is_owned`). +// Ownership is required before we can read any non-atomic fields in the page. +// This way we can push a block on the thread free list and try to claim ownership atomically in `alloc/free.c:mi_free_block_mt`. typedef uintptr_t mi_thread_free_t; +// We store the currently used block count together with the total malloc call count as 16-bit numbers. +// This is done for better codegen `mi_malloc/mi_free` (where we can increment both at once as `used_alloc += 0x10001` for example). +// The `last_used` is the `used` count since the statistics are last updated; on 32-bit platforms this +// is a separate field in `mi_page_t` but on 64-bit we use the upper 32-bits to store it. +// We need the `alloc_count` and `last_used` to efficiently calculate allocation and free statistics even +// in a release build; this way we can update the stats in the slow path (`_mi_page_update_stats`). +typedef union mi_used_s { + size_t used_alloc; // used + alloc_count + // the following struct is unused but nice for debugging + struct { + uint16_t used_count; + uint16_t alloc_count; + #if MI_SIZE_SIZE >= 8 + uint16_t last_used; + uint16_t last_alloc; + #endif + } le; // little-endian layout +} mi_used_t; + +static inline size_t mi_xused_used_count(mi_used_t xused) { return (xused.used_alloc & 0xFFFF); } +static inline size_t mi_xused_alloc_count(mi_used_t xused) { return ((xused.used_alloc>>16) & 0xFFFF); } +static inline mi_used_t mi_xused_used_reset(mi_used_t xused) { xused.used_alloc = xused.used_alloc & ~0xFFFF; return xused; } + // A page contains blocks of one specific size (`block_size`). // Each page has three list of free blocks: // `free` for blocks that can be allocated, @@ -307,243 +460,161 @@ typedef uintptr_t mi_thread_free_t; // The `local_free` and `thread_free` lists are migrated to the `free` list // when it is exhausted. The separate `local_free` list is necessary to // implement a monotonic heartbeat. The `thread_free` list is needed for -// avoiding atomic operations in the common case. -// +// avoiding atomic operations when allocating from the owning thread. // // `used - |thread_free|` == actual blocks that are in use (alive) // `used - |thread_free| + |free| + |local_free| == capacity` // -// We don't count `freed` (as |free|) but use `used` to reduce +// We don't count "freed" (as |free|) but use only the `used` field to reduce // the number of memory accesses in the `mi_page_all_free` function(s). +// Use `_mi_page_free_collect` to collect the thread_free list and update the `used` count. // // Notes: -// - Access is optimized for `mi_free` and `mi_page_alloc` (in `alloc.c`) -// - Using `uint16_t` does not seem to slow things down -// - The size is 8 words on 64-bit which helps the page index calculations -// (and 10 words on 32-bit, and encoded free lists add 2 words. Sizes 10 -// and 12 are still good for address calculation) -// - To limit the structure size, the `xblock_size` is 32-bits only; for -// blocks > MI_HUGE_BLOCK_SIZE the size is determined from the segment page size -// - `thread_free` uses the bottom bits as a delayed-free flags to optimize -// concurrent frees where only the first concurrent free adds to the owning -// heap `thread_delayed_free` list (see `alloc.c:mi_free_block_mt`). -// The invariant is that no-delayed-free is only set if there is -// at least one block that will be added, or as already been added, to -// the owning heap `thread_delayed_free` list. This guarantees that pages -// will be freed correctly even if only other threads free blocks. -typedef struct mi_page_s { - // "owned" by the segment - uint32_t slice_count; // slices in this page (0 if not a page) - uint32_t slice_offset; // distance from the actual page data slice (0 if a page) - uint8_t is_committed : 1; // `true` if the page virtual memory is committed - uint8_t is_zero_init : 1; // `true` if the page was initially zero initialized - uint8_t use_qsbr : 1; // delay page freeing using qsbr - uint8_t tag : 4; // tag from the owning heap - uint8_t debug_offset; // number of bytes to preserve when filling freed or uninitialized memory - - // layout like this to optimize access in `mi_malloc` and `mi_free` - uint16_t capacity; // number of blocks committed, must be the first field, see `segment.c:page_clear` - uint16_t reserved; // number of blocks reserved in memory - mi_page_flags_t flags; // `in_full` and `has_aligned` flags (8 bits) - uint8_t free_is_zero : 1; // `true` if the blocks in the free list are zero initialized - uint8_t retire_expire : 7; // expiration count for retired blocks - - mi_block_t* free; // list of available free blocks (`malloc` allocates from this list) - uint32_t used; // number of blocks in use (including blocks in `local_free` and `thread_free`) - uint32_t xblock_size; // size available in each block (always `>0`) - mi_block_t* local_free; // list of deferred free blocks by this thread (migrates to `free`) +// - Non-atomic fields can only be accessed if having _ownership_ (low bit of `xthread_free` is 1). +// Combining the `thread_free` list with an ownership bit allows a concurrent `free` to atomically +// free an object and (re)claim ownership if the page was abandoned. +// - If a page is not part of a theap it is called "abandoned" (`theap==NULL`) -- in +// that case the `xthreadid` is 0 or 4 (4 is for abandoned pages that +// are in the `pages_abandoned` lists of an arena, these are called "mapped" abandoned pages). +// - page flags are in the bottom 3 bits of `xthread_id` for the fast path in `mi_free`. +// - The layout below is optimized for `alloc/free.c:mi_free` and `alloc/alloc.c:mi_page_alloc` - #if (MI_ENCODE_FREELIST || MI_PADDING) - uintptr_t keys[2]; // two random keys to encode the free lists (see `_mi_block_next`) or padding canary +typedef struct mi_page_s { + #if (MI_PAGE_META_IS_ALIGNED) + _Atomic(struct mi_page_s*) self; // points to the actual page info (for pages that span multiple slices) + #endif + _Atomic(mi_threadid_t) xthread_id; // thread this page belongs to. (= `theap->thread_id (or 0 or 4 if abandoned) | page_flags`) + mi_block_t* free; // list of available free blocks (`malloc` allocates from this list) + mi_used_t xused; // number of blocks in use (including blocks in `thread_free`) (and the allocated count for statistics) + #if MI_SIZE_SIZE < 8 + uint16_t xlast_used; // for statistics; on 64-bit platforms it is in bits 32..47 of xused. + uint16_t xlast_alloc; // for sampling; on 64-bit platforms it is in bits 48..63 of xused. #endif + mi_block_t* local_free; // list of deferred free blocks by this thread (migrates to `free`) - _Atomic(mi_thread_free_t) xthread_free; // list of deferred free blocks freed by other threads - _Atomic(uintptr_t) xheap; + size_t block_size; // const: size available in each block (always `>0`) + size_t page_offset; // const: relative offset from the page to the start of the blocks + uint16_t capacity; // number of blocks committed + uint16_t reserved; // number of blocks reserved in memory + uint16_t slice_pcommitted; // committed size in OS page sizes relative to the first arena slice of the page data (or 0 if the page is fully committed already) + uint8_t retire_expire; // expiration count for retired blocks + bool free_is_zero; // `true` if the blocks in the free list are zero initialized - struct mi_page_s* next; // next page owned by this thread with the same `block_size` - struct mi_page_s* prev; // previous page owned by this thread with the same `block_size` + // next cache line + _Atomic(mi_thread_free_t) xthread_free; // list of deferred free blocks freed by other threads (= `mi_block_t* | (1 if owned)`) + mi_theap_t* theap; // the theap owning this page (may not be valid or NULL for abandoned pages) + mi_heap_t* heap; // const: the heap owning this page -#ifdef Py_GIL_DISABLED - struct llist_node qsbr_node; - uint64_t qsbr_goal; -#endif + struct mi_page_s* next; // next page owned by the theap with the same `block_size` + struct mi_page_s* prev; // previous page owned by the theap with the same `block_size` + mi_memid_t memid; // const: provenance of the page memory - // 64-bit 9 words, 32-bit 12 words, (+2 for secure) - #if MI_INTPTR_SIZE==8 && !defined(Py_GIL_DISABLED) - uintptr_t padding[1]; + #if (MI_ENCODE_FREELIST || MI_PADDING) + uintptr_t keys[MI_PAGE_KEY_COUNT]; // const: one or two random keys to encode the free lists (see `_mi_block_next`) or padding canary + #endif + #if MI_QSBR + struct mi_page_s* qsbr_next; + struct mi_page_s* qsbr_prev; + bool qsbr_in_queue; + mi_page_qsbr_t qsbr; #endif } mi_page_t; - - // ------------------------------------------------------ -// Mimalloc segments contain mimalloc pages +// Object sizes // ------------------------------------------------------ -typedef enum mi_page_kind_e { - MI_PAGE_SMALL, // small blocks go into 64KiB pages inside a segment - MI_PAGE_MEDIUM, // medium blocks go into medium pages inside a segment - MI_PAGE_LARGE, // larger blocks go into a page of just one block - MI_PAGE_HUGE, // huge blocks (> 16 MiB) are put into a single page in a single segment. -} mi_page_kind_t; - -typedef enum mi_segment_kind_e { - MI_SEGMENT_NORMAL, // MI_SEGMENT_SIZE size with pages inside. - MI_SEGMENT_HUGE, // > MI_LARGE_SIZE_MAX segment with just one huge page inside. -} mi_segment_kind_t; - -// ------------------------------------------------------ -// A segment holds a commit mask where a bit is set if -// the corresponding MI_COMMIT_SIZE area is committed. -// The MI_COMMIT_SIZE must be a multiple of the slice -// size. If it is equal we have the most fine grained -// decommit (but setting it higher can be more efficient). -// The MI_MINIMAL_COMMIT_SIZE is the minimal amount that will -// be committed in one go which can be set higher than -// MI_COMMIT_SIZE for efficiency (while the decommit mask -// is still tracked in fine-grained MI_COMMIT_SIZE chunks) -// ------------------------------------------------------ - -#define MI_MINIMAL_COMMIT_SIZE (1*MI_SEGMENT_SLICE_SIZE) -#define MI_COMMIT_SIZE (MI_SEGMENT_SLICE_SIZE) // 64KiB -#define MI_COMMIT_MASK_BITS (MI_SEGMENT_SIZE / MI_COMMIT_SIZE) -#define MI_COMMIT_MASK_FIELD_BITS MI_SIZE_BITS -#define MI_COMMIT_MASK_FIELD_COUNT (MI_COMMIT_MASK_BITS / MI_COMMIT_MASK_FIELD_BITS) - -#if (MI_COMMIT_MASK_BITS != (MI_COMMIT_MASK_FIELD_COUNT * MI_COMMIT_MASK_FIELD_BITS)) -#error "the segment size must be exactly divisible by the (commit size * size_t bits)" +#define MI_PAGE_ALIGN MI_ARENA_SLICE_ALIGN // page area's must be aligned on this for the page map. +#define MI_PAGE_MIN_START_BLOCK_ALIGN MI_MAX_ALIGN_SIZE // minimal block alignment for the first block in a page (16b) +#define MI_PAGE_MAX_START_BLOCK_ALIGN2 (4*MI_KiB) // maximal block alignment for "power of 2"-sized blocks (such that we guarantee natural alignment) +#define MI_PAGE_OSPAGE_BLOCK_ALIGN2 (4*MI_KiB) // also aligns any multiple of this size to avoid TLB misses. +#define MI_PAGE_MAX_OVERALLOC_ALIGN MI_ARENA_SLICE_SIZE // (64 KiB) limit for which we overallocate in arena pages, beyond this use OS allocation + +// Offset the first block in pages with larger blocks (using the unused space at the end of the page) spread across cache sets (issue #1121). +#if !defined(MI_PAGE_CACHE_OFFSET) +#define MI_PAGE_CACHE_OFFSET 1 +#endif +#define MI_PAGE_CACHE_OFFSET_UNIT (128) // offset granularity (power of 2 between 16 and 4KiB) +#define MI_PAGE_CACHE_OFFSET_MAX (MI_PAGE_OSPAGE_BLOCK_ALIGN2 - MI_PAGE_CACHE_OFFSET_UNIT) // maximal offset (32 possible offsets) +// Offset the first block in pages with small blocks (< 512) +#define MI_PAGE_SMALL_CACHE_OFFSET_UNIT (64) +#define MI_PAGE_SMALL_CACHE_OFFSET_MAX (7*MI_PAGE_SMALL_CACHE_OFFSET_UNIT) + +// The max object sizes are intended to not waste more than ~ 12.5% internally over the page sizes. +#define MI_SMALL_MAX_OBJ_SIZE ((MI_SMALL_PAGE_SIZE-MI_PAGE_OSPAGE_BLOCK_ALIGN2)/6) // = 10 KiB +#if MI_ENABLE_LARGE_PAGES +#define MI_MEDIUM_MAX_OBJ_SIZE ((MI_MEDIUM_PAGE_SIZE-MI_PAGE_OSPAGE_BLOCK_ALIGN2)/6) // ~ 84 KiB (but see `alloc/page.h:mi_block_size_is_medium`) +#define MI_LARGE_MAX_OBJ_SIZE (MI_LARGE_PAGE_SIZE/8) // <= 512 KiB. note: this must be a nice power of 2 or we get rounding issues with `_mi_bin` +#else +#define MI_MEDIUM_MAX_OBJ_SIZE (MI_MEDIUM_PAGE_SIZE/8) // <= 64 KiB +#define MI_LARGE_MAX_OBJ_SIZE MI_MEDIUM_MAX_OBJ_SIZE // note: this must be a nice power of 2 or we get rounding issues with `_mi_bin` #endif +#define MI_LARGE_MAX_OBJ_WSIZE (MI_LARGE_MAX_OBJ_SIZE/MI_SIZE_SIZE) +#define MI_MAX_RECLAIM_OBJ_SIZE (MI_LARGE_MAX_OBJ_SIZE/4) // only reclaim abandoned pages on free up to this block size -typedef struct mi_commit_mask_s { - size_t mask[MI_COMMIT_MASK_FIELD_COUNT]; -} mi_commit_mask_t; - -typedef mi_page_t mi_slice_t; -typedef int64_t mi_msecs_t; - - -// Memory can reside in arena's, direct OS allocated, or statically allocated. The memid keeps track of this. -typedef enum mi_memkind_e { - MI_MEM_NONE, // not allocated - MI_MEM_EXTERNAL, // not owned by mimalloc but provided externally (via `mi_manage_os_memory` for example) - MI_MEM_STATIC, // allocated in a static area and should not be freed (for arena meta data for example) - MI_MEM_OS, // allocated from the OS - MI_MEM_OS_HUGE, // allocated as huge os pages - MI_MEM_OS_REMAP, // allocated in a remapable area (i.e. using `mremap`) - MI_MEM_ARENA // allocated from an arena (the usual case) -} mi_memkind_t; - -static inline bool mi_memkind_is_os(mi_memkind_t memkind) { - return (memkind >= MI_MEM_OS && memkind <= MI_MEM_OS_REMAP); -} - -typedef struct mi_memid_os_info { - void* base; // actual base address of the block (used for offset aligned allocations) - size_t alignment; // alignment at allocation -} mi_memid_os_info_t; - -typedef struct mi_memid_arena_info { - size_t block_index; // index in the arena - mi_arena_id_t id; // arena id (>= 1) - bool is_exclusive; // the arena can only be used for specific arena allocations -} mi_memid_arena_info_t; - -typedef struct mi_memid_s { - union { - mi_memid_os_info_t os; // only used for MI_MEM_OS - mi_memid_arena_info_t arena; // only used for MI_MEM_ARENA - } mem; - bool is_pinned; // `true` if we cannot decommit/reset/protect in this memory (e.g. when allocated using large OS pages) - bool initially_committed;// `true` if the memory was originally allocated as committed - bool initially_zero; // `true` if the memory was originally zero initialized - mi_memkind_t memkind; -} mi_memid_t; - - -// Segments are large allocated memory blocks (8mb on 64 bit) from -// the OS. Inside segments we allocated fixed size _pages_ that -// contain blocks. -typedef struct mi_segment_s { - // constant fields - mi_memid_t memid; // memory id for arena allocation - bool allow_decommit; - bool allow_purge; - size_t segment_size; - - // segment fields - mi_msecs_t purge_expire; - mi_commit_mask_t purge_mask; - mi_commit_mask_t commit_mask; - - _Atomic(struct mi_segment_s*) abandoned_next; - - // from here is zero initialized - struct mi_segment_s* next; // the list of freed segments in the cache (must be first field, see `segment.c:mi_segment_init`) - - size_t abandoned; // abandoned pages (i.e. the original owning thread stopped) (`abandoned <= used`) - size_t abandoned_visits; // count how often this segment is visited in the abandoned list (to force reclaim if it is too long) - size_t used; // count of pages in use - uintptr_t cookie; // verify addresses in debug mode: `mi_ptr_cookie(segment) == segment->cookie` - - size_t segment_slices; // for huge segments this may be different from `MI_SLICES_PER_SEGMENT` - size_t segment_info_slices; // initial slices we are using segment info and possible guard pages. - - // layout like this to optimize access in `mi_free` - mi_segment_kind_t kind; - size_t slice_entries; // entries in the `slices` array, at most `MI_SLICES_PER_SEGMENT` - _Atomic(mi_threadid_t) thread_id; // unique id of the thread owning this segment - - mi_slice_t slices[MI_SLICES_PER_SEGMENT+1]; // one more for huge blocks with large alignment -} mi_segment_t; +#if (MI_LARGE_MAX_OBJ_WSIZE >= 655360) +#error "mimalloc internal: define more bins" +#endif -typedef uintptr_t mi_tagged_segment_t; +// static invariant: MI_MAX_SINGLETON_BIN >= _mi_bin(MI_LARGE_MAX_OBJ_SIZE) (See heap/init.c for the size bins) +#if (MI_LARGE_MAX_OBJ_WSIZE <= 8192) // 64 KiB +#define MI_MAX_SINGLETON_BIN (48) +#elif (MI_LARGE_MAX_OBJ_WSIZE <= 32768) // 256KiB +#define MI_MAX_SINGLETON_BIN (56) +#elif (MI_LARGE_MAX_OBJ_WSIZE <= 65536) // 512KiB +#define MI_MAX_SINGLETON_BIN (60) +#else +#define MI_MAX_SINGLETON_BIN MI_BIN_HUGE +#endif -// Segments unowned by any thread are put in a shared pool -typedef struct mi_abandoned_pool_s { - // This is a list of visited abandoned pages that were full at the time. - // this list migrates to `abandoned` when that becomes NULL. The use of - // this list reduces contention and the rate at which segments are visited. - mi_decl_cache_align _Atomic(mi_segment_t*) abandoned_visited; // = NULL +// The small object max size must be larger than twice the small size max minus one, +// such that any aligned allocation with `alignment <= size <= MI_SMALL_SIZE_MAX` will +// be allocated in a small page (so `mi_free_csize` can delegate correctly to `mi_free_small`. +#if MI_SMALL_MAX_OBJ_SIZE <= (2*(MI_SMALL_WSIZE_MAX * MI_SIZE_SIZE)-1) +#error "mimalloc internal: the small object max size is too small" +#endif - // The abandoned page list (tagged as it supports pop) - mi_decl_cache_align _Atomic(mi_tagged_segment_t) abandoned; // = NULL +// ------------------------------------------------------ +// Page kinds +// ------------------------------------------------------ - // Maintain these for debug purposes (these counts may be a bit off) - mi_decl_cache_align _Atomic(size_t) abandoned_count; - mi_decl_cache_align _Atomic(size_t) abandoned_visited_count; +typedef enum mi_page_kind_e { + MI_PAGE_SMALL, // small blocks go into 64KiB pages + MI_PAGE_MEDIUM, // medium blocks go into 512KiB pages + MI_PAGE_LARGE, // larger blocks go into 4MiB pages (if `MI_ENABLE_LARGE_PAGES==1`) + MI_PAGE_SINGLETON // page containing a single block. + // used for blocks `> MI_LARGE_MAX_OBJ_SIZE` or an alignment `> MI_PAGE_MAX_OVERALLOC_ALIGN`. +} mi_page_kind_t; - // We also maintain a count of current readers of the abandoned list - // in order to prevent resetting/decommitting segment memory if it might - // still be read. - mi_decl_cache_align _Atomic(size_t) abandoned_readers; // = 0 -} mi_abandoned_pool_t; // ------------------------------------------------------ -// Heaps -// Provide first-class heaps to allocate from. -// A heap just owns a set of pages for allocation and +// A "theap" is a thread local heap which owns pages. +// (making them thread-local avoids atomic operations) +// +// All theaps belong to a (non-thread-local) heap. +// A theap just owns a set of pages for allocation and // can only be allocate/reallocate from the thread that created it. // Freeing blocks can be done from any thread though. -// Per thread, the segments are shared among its heaps. -// Per thread, there is always a default heap that is -// used for allocation; it is initialized to statically -// point to an empty heap to avoid initialization checks -// in the fast path. +// +// Per thread, there is always a default theap that belongs +// to the default heap. It is initialized to statically +// point initially to an empty theap to avoid initialization +// checks in the fast path. // ------------------------------------------------------ // Thread local data -typedef struct mi_tld_s mi_tld_t; +typedef struct mi_tld_s mi_tld_t; // defined below // Pages of a certain block size are held in a queue. typedef struct mi_page_queue_s { mi_page_t* first; mi_page_t* last; + size_t count; size_t block_size; + size_t xcollect_score; // how often full pages here revived by cross-thread frees } mi_page_queue_t; -#define MI_BIN_FULL (MI_BIN_HUGE+1) - // Random context typedef struct mi_random_cxt_s { uint32_t input[16]; @@ -554,13 +625,13 @@ typedef struct mi_random_cxt_s { // In debug mode there is a padding structure at the end of the blocks to check for buffer overflows -#if (MI_PADDING) +#if MI_PADDING typedef struct mi_padding_s { uint32_t canary; // encoded block value to check validity of the padding (in case of overflow) - uint32_t delta; // padding bytes before the block. (mi_usable_size(p) - delta == exact allocated bytes) + uint32_t delta; // padding bytes before the block. (mi_full_usable_size(p) - delta == exact allocated bytes) } mi_padding_t; #define MI_PADDING_SIZE (sizeof(mi_padding_t)) -#define MI_PADDING_WSIZE ((MI_PADDING_SIZE + MI_INTPTR_SIZE - 1) / MI_INTPTR_SIZE) +#define MI_PADDING_WSIZE ((MI_PADDING_SIZE + MI_SIZE_SIZE - 1) / MI_SIZE_SIZE) #else #define MI_PADDING_SIZE 0 #define MI_PADDING_WSIZE 0 @@ -569,180 +640,278 @@ typedef struct mi_padding_s { #define MI_PAGES_DIRECT (MI_SMALL_WSIZE_MAX + MI_PADDING_WSIZE + 1) -// A heap owns a set of pages. -struct mi_heap_s { - mi_tld_t* tld; +// A thread-local heap ("theap") owns a set of thread-local pages. +struct mi_theap_s { + // put in front for fast small allocations mi_page_t* pages_free_direct[MI_PAGES_DIRECT]; // optimize: array where every entry points a page with possibly free blocks in the corresponding queue for that size. - mi_page_queue_t pages[MI_BIN_FULL + 1]; // queue of pages for each size class (or "bin") - _Atomic(mi_block_t*) thread_delayed_free; - mi_threadid_t thread_id; // thread this heap belongs too - mi_arena_id_t arena_id; // arena id if the heap belongs to a specific arena (or 0) - uintptr_t cookie; // random cookie to verify pointers (see `_mi_ptr_cookie`) - uintptr_t keys[2]; // two random keys used to encode the `thread_delayed_free` list + + // less frequently accessed fields + mi_tld_t* tld; // thread-local data + _Atomic(mi_heap_t*) heap; // the heap this theap belongs to. + _Atomic(mi_subproc_t*)subproc; // subproc this belongs too (always `subproc == heap->subproc` but needed for safe destruction) + _Atomic(size_t) refcount; // reference count (needed for safe heap destroy) + + // config + long page_full_retain; // how many full pages can be retained per queue (before abandoning them) + bool allow_page_reclaim; // `true` if this theap can reclaim abandoned pages + bool allow_page_abandon; // `true` if this theap can abandon pages to reduce memory footprint + bool is_detached; // `true` if `tld->thread_id == MI_THREADID_DETACHED` + bool profile_disabled; // if `true`, this theap is permanently excluded from profiling + + // sampling + size_t sample_countdown; // sample countdown in requested bytes (don't change the field order; see `internal.h:mi_theap_get_free_small_page`) + size_t sample_rate; // current sampling rate in requested bytes (or 0 to disable) (for profiling and guarded mode) + uint64_t sample_requested; // total allocated/requested bytes since the last sample + size_t profile_sample_rate; // sampling rate in requested bytes for profiling + size_t profile_sample_countdown; // countdown in requested bytes for profiling + size_t guarded_sample_rate; // sampling rate in requested bytes for guarded objects + size_t guarded_sample_countdown; // countdown in requested bytes for guarded objects + size_t guarded_size_min; // minimal size for guarded objects + size_t guarded_size_max; // maximal size for guarded objects + uintptr_t profiler_reserved1; // two fields available for a profiler (to avoid thread locals) + uintptr_t profiler_reserved2; + + // stats + unsigned long long heartbeat; // monotonic heartbeat count mi_random_ctx_t random; // random number context used for secure allocation size_t page_count; // total number of pages in the `pages` queues. + size_t page_count_os; // number of non-arena pages in the `pages` queues size_t page_retired_min; // smallest retired index (retired pages are fully free, but still in the page queues) size_t page_retired_max; // largest retired index into the `pages` array. - mi_heap_t* next; // list of heaps per thread - bool no_reclaim; // `true` if this heap should not reclaim abandoned pages - uint8_t tag; // custom identifier for this heap - uint8_t debug_offset; // number of bytes to preserve when filling freed or uninitialized memory - bool page_use_qsbr; // should freeing pages be delayed using QSBR + size_t pages_full_size; // optimization: total size of blocks in the pages of the full queue (issue #1220) + long generic_count; // how often is `_mi_malloc_generic` called? + long generic_collect_count; // how often is `_mi_malloc_generic` called without collecting? + + // theaps belong to heaps and threads + mi_theap_t* tnext; // list of theaps in this thread + mi_theap_t* tprev; + mi_theap_t* hnext; // list of theaps of the owning `heap` + mi_theap_t* hprev; + + // page queues + mi_page_queue_t pages[MI_BIN_COUNT]; // queue of pages for each size class (or "bin") + mi_memid_t memid; // provenance of the theap struct itself (meta or os) + mi_stats_t stats; // thread-local statistics + #if MI_QSBR + mi_page_t* qsbr_first; + mi_page_t* qsbr_last; + #endif }; + // ------------------------------------------------------ -// Debug +// Heaps contain allocated blocks. Heaps are self-contained +// but share the (sub-process) memory in the arena's. // ------------------------------------------------------ -#if !defined(MI_DEBUG_UNINIT) -#define MI_DEBUG_UNINIT (0xD0) -#endif -#if !defined(MI_DEBUG_FREED) -#define MI_DEBUG_FREED (0xDF) -#endif -#if !defined(MI_DEBUG_PADDING) -#define MI_DEBUG_PADDING (0xDE) -#endif +// Keep track of all owned and abandoned pages in the arena's +struct mi_arena_pages_s; +typedef struct mi_arena_pages_s mi_arena_pages_t; -#if (MI_DEBUG) -// use our own assertion to print without memory allocation -mi_decl_noreturn mi_decl_cold -void _mi_assert_fail(const char* assertion, const char* fname, unsigned int line, const char* func) mi_decl_throw; -#define mi_assert(expr) ((expr) ? (void)0 : _mi_assert_fail(#expr,__FILE__,__LINE__,__func__)) -#else -#define mi_assert(x) -#endif +#define MI_MAX_ARENAS (160) // Limited for now (and takes up .bss).. but arena's scale up exponentially (see `_mi_arena_reserve`) + // 160 arenas is enough for ~2 TiB memory -#if (MI_DEBUG>1) -#define mi_assert_internal mi_assert -#else -#define mi_assert_internal(x) -#endif +// A dynamic thread-local variable; 0 for an invalid thread-local +typedef size_t mi_thread_local_t; + +typedef struct mi_heap_s { + mi_subproc_t* subproc; // a heap belongs to a subprocess + size_t heap_seq; // unique sequence number for heaps in this subprocess + mi_heap_t* next; // list of heaps in this subprocess + mi_heap_t* prev; + mi_thread_local_t theap; // dynamic thread local for the thread-local theaps of this heap + + mi_arena_t* exclusive_arena; // if the heap should only allocate from a specific arena (or NULL) + long reclaim_on_free; // reclaim policy captured at heap initialization + int numa_node; // if >=0, prefer this numa node for allocations + #if MI_QSBR + bool qsbr_enabled; // immutable after heap initialization + #endif + _Atomic(size_t) debug_fill_preserve; // in debug mode, the first N bytes of a block are not filled with MI_DEBUG_UNINIT/FREED + + mi_theap_t* theaps; // list of all thread-local theaps belonging to this heap (using the `hnext`/`hprev` fields) + mi_lock_t theaps_lock; // protects the theaps list and serializes heap walks with thread-exit abandonment + + _Atomic(mi_profiler_t*) profiler; + + _Atomic(size_t) abandoned_count[MI_BIN_COUNT]; // total count of abandoned pages in this heap + mi_page_t* os_abandoned_pages; // list of pages that are OS allocated and not in an arena + mi_lock_t os_abandoned_pages_lock; // lock for the os abandoned pages list (this lock protects list operations) + + _Atomic(mi_arena_pages_t*) arena_pages[MI_MAX_ARENAS]; // track owned and abandoned pages in the arenas (entries can be NULL) + mi_lock_t arena_pages_lock; // lock to update the arena_pages array + mi_memid_t memid; // provenance of the heap memory + mi_stats_t stats; // statistics for this heap; periodically updated by merging from each theap + #if MI_QSBR + mi_heap_qsbr_t qsbr; + #endif +} mi_heap_t; -#if (MI_DEBUG>2) -#define mi_assert_expensive mi_assert -#else -#define mi_assert_expensive(x) -#endif // ------------------------------------------------------ -// Statistics +// Sub processes do not reclaim or visit pages from other sub processes. +// These are essentially the static variables of a process, and +// usually there is only one subprocess. This can be used for example +// by CPython to have separate interpreters within one process. +// Each thread can only belong to one subprocess +// (and needs to call `mi_subproc_add_current_thread` before any allocations). // ------------------------------------------------------ -#ifndef MI_STAT -#if (MI_DEBUG>0) -#define MI_STAT 2 -#else -#define MI_STAT 0 -#endif -#endif - -typedef struct mi_stat_count_s { - int64_t allocated; - int64_t freed; - int64_t peak; - int64_t current; -} mi_stat_count_t; - -typedef struct mi_stat_counter_s { - int64_t total; - int64_t count; -} mi_stat_counter_t; - -typedef struct mi_stats_s { - mi_stat_count_t segments; - mi_stat_count_t pages; - mi_stat_count_t reserved; - mi_stat_count_t committed; - mi_stat_count_t reset; - mi_stat_count_t purged; - mi_stat_count_t page_committed; - mi_stat_count_t segments_abandoned; - mi_stat_count_t pages_abandoned; - mi_stat_count_t threads; - mi_stat_count_t normal; - mi_stat_count_t huge; - mi_stat_count_t large; - mi_stat_count_t malloc; - mi_stat_count_t segments_cache; - mi_stat_counter_t pages_extended; - mi_stat_counter_t mmap_calls; - mi_stat_counter_t commit_calls; - mi_stat_counter_t reset_calls; - mi_stat_counter_t purge_calls; - mi_stat_counter_t page_no_retire; - mi_stat_counter_t searches; - mi_stat_counter_t normal_count; - mi_stat_counter_t huge_count; - mi_stat_counter_t large_count; -#if MI_STAT>1 - mi_stat_count_t normal_bins[MI_BIN_HUGE+1]; -#endif -} mi_stats_t; - - -void _mi_stat_increase(mi_stat_count_t* stat, size_t amount); -void _mi_stat_decrease(mi_stat_count_t* stat, size_t amount); -void _mi_stat_counter_increase(mi_stat_counter_t* stat, size_t amount); - -#if (MI_STAT) -#define mi_stat_increase(stat,amount) _mi_stat_increase( &(stat), amount) -#define mi_stat_decrease(stat,amount) _mi_stat_decrease( &(stat), amount) -#define mi_stat_counter_increase(stat,amount) _mi_stat_counter_increase( &(stat), amount) -#else -#define mi_stat_increase(stat,amount) (void)0 -#define mi_stat_decrease(stat,amount) (void)0 -#define mi_stat_counter_increase(stat,amount) (void)0 -#endif +struct mi_subproc_s { + size_t subproc_seq; // unique id for sub-processes + mi_subproc_t* next; // list of all sub-processes + mi_subproc_t* prev; + _Atomic(mi_meta_page_t*) meta_pages; // meta data pages + + _Atomic(size_t) arena_count; // current count of arena's + _Atomic(mi_arena_t*) arenas[MI_MAX_ARENAS]; // arena's of this sub-process + mi_lock_t arena_reserve_lock; // lock to ensure arena's get reserved one at a time + mi_decl_align(8) // needed on some 32-bit platforms + _Atomic(int64_t) purge_expire; // expiration is set if any arenas can be purged + + _Atomic(mi_heap_t*) heap_main; // main heap for this sub process + mi_heap_t* heaps; // heaps belonging to this sub-process + mi_lock_t heaps_lock; + + mi_theap_t* theap_meta; // detached theap for allocating meta-data + mi_lock_t theap_meta_lock; // all allocations in theap_meta need a lock + + _Atomic(size_t) thread_count; // current threads associated with this sub-process + _Atomic(size_t) thread_total_count; // total created threads associated with this sub-process + _Atomic(size_t) heap_count; // current heaps in this sub-process (== |heaps|) + _Atomic(size_t) heap_total_count; // total created heaps in this sub-process + + _Atomic(mi_profiler_t*) profiler; + + mi_memid_t memid; // provenance of this memory block (meta or static) + mi_subproc_t* parent; // subproc in which this one was allocated + mi_decl_align(8) // needed on some 32-bit platforms + mi_stats_t stats; // subprocess statistics; updated for arena/OS stats like committed, + // and otherwise merged with heap stats when those are deleted +}; -#define mi_heap_stat_counter_increase(heap,stat,amount) mi_stat_counter_increase( (heap)->tld->stats.stat, amount) -#define mi_heap_stat_increase(heap,stat,amount) mi_stat_increase( (heap)->tld->stats.stat, amount) -#define mi_heap_stat_decrease(heap,stat,amount) mi_stat_decrease( (heap)->tld->stats.stat, amount) // ------------------------------------------------------ // Thread Local data // ------------------------------------------------------ -// A "span" is an available range of slices. The span queues keep -// track of slice spans of at most the given `slice_count` (but more than the previous size class). -typedef struct mi_span_queue_s { - mi_slice_t* first; - mi_slice_t* last; - size_t slice_count; -} mi_span_queue_t; - -#define MI_SEGMENT_BIN_MAX (35) // 35 == mi_segment_bin(MI_SLICES_PER_SEGMENT) - -// OS thread local data -typedef struct mi_os_tld_s { - size_t region_idx; // start point for next allocation - mi_stats_t* stats; // points to tld stats -} mi_os_tld_t; - - -// Segments thread local data -typedef struct mi_segments_tld_s { - mi_span_queue_t spans[MI_SEGMENT_BIN_MAX+1]; // free slice spans inside segments - size_t count; // current number of segments; - size_t peak_count; // peak number of segments - size_t current_size; // current size of all segments - size_t peak_size; // peak size of all segments - mi_stats_t* stats; // points to tld stats - mi_os_tld_t* os; // points to os stats - mi_abandoned_pool_t* abandoned; // pool of abandoned segments -} mi_segments_tld_t; +// Milliseconds as in `int64_t` to avoid overflows +typedef int64_t mi_msecs_t; // Thread local data struct mi_tld_s { - unsigned long long heartbeat; // monotonic heartbeat count - bool recurse; // true if deferred was called; used to prevent infinite recursion. - mi_heap_t* heap_backing; // backing heap of this thread (cannot be deleted) - mi_heap_t* heaps; // list of heaps in this thread (so we can abandon all when the thread terminates) - mi_segments_tld_t segments; // segment tld - mi_os_tld_t os; // os tld - mi_stats_t stats; // statistics + mi_threadid_t thread_id; // thread id of this thread + size_t thread_seq; // thread sequence id (linear count of created threads) + int numa_node; // thread preferred numa node + mi_subproc_t* subproc; // sub-process this thread belongs to. + mi_theap_t* theaps; // list of theaps in this thread (so we can abandon all when the thread terminates) + mi_lock_t theaps_lock; // lock as the theaps list is sometimes accessed from another thread (on `mi_heap_free`) + bool recurse; // true if deferred was called; used to prevent infinite recursion. + bool is_in_threadpool; // true if this thread is part of a threadpool (and can run arbitrary tasks) + mi_memid_t memid; // provenance of the tld memory itself (meta or OS) +}; + + +/* ---------------------------------------------------------------------------- + Arenas are fixed area's of OS memory from which we can allocate + large blocks (>= MI_ARENA_MIN_BLOCK_SIZE). + In contrast to the rest of mimalloc, the arenas are shared between + threads and need to be accessed using atomic operations (using atomic `mi_bitmap_t`'s). + + Arenas are also used to for huge OS page (1GiB) reservations or for reserving + OS memory upfront which can be improve performance or is sometimes needed + on embedded devices. We can also employ this with WASI or `sbrk` systems + to reserve large arenas upfront and be able to reuse the memory more effectively. +-----------------------------------------------------------------------------*/ + +#define MI_ARENA_BIN_COUNT (MI_MAX_SINGLETON_BIN+1) +#define MI_ARENA_MIN_SIZE (MI_BCHUNK_BITS * MI_ARENA_SLICE_SIZE) // 32 MiB (or 8 MiB on 32-bit) +#define MI_ARENA_MAX_SIZE (MI_BITMAP_MAX_BIT_COUNT * MI_ARENA_SLICE_SIZE) // 16 GiB + +typedef struct mi_bitmap_s mi_bitmap_t; // atomic bitmap (defined in `src/arena/bitmap.h`) +typedef struct mi_bbitmap_s mi_bbitmap_t; // atomic binned bitmap (defined in `src/arena/bitmap.h`) + +struct mi_arena_pages_s { + mi_bitmap_t* pages; // all registered pages (abandoned and owned) + mi_bitmap_t* pages_abandoned[MI_ARENA_BIN_COUNT]; // abandoned pages per size bin (a set bit means the start of the page) + // followed by the bitmaps (whose siz`es depend on the arena size) }; + +// A memory arena +typedef struct mi_arena_s { + mi_memid_t memid; // provenance of the memory area + mi_subproc_t* subproc; // subprocess this arena belongs to (`this 'element-of' this->subproc->arenas`) + size_t arena_idx; // index in the arenas array + void* start; // actual start of the arena area (the arena_s info may come later due to guard pages etc.) + + size_t slice_count; // total size of the area in arena slices (of `MI_ARENA_SLICE_SIZE`) + size_t info_slices; // initial slices reserved for the arena bitmaps + int numa_node; // associated NUMA node + bool is_exclusive; // only allow allocations if specifically for this arena + mi_decl_align(8) // needed on some 32-bit platforms + _Atomic(mi_msecs_t) purge_expire; // expiration time when slices can be purged from `slices_purge`. + mi_commit_fun_t* commit_fun; // custom commit/decommit memory + void* commit_fun_arg; // user argument for a custom commit function + + size_t total_size; // for (user given) memory more than MI_ARENA_MAX_SIZE, we use N arena's to cover it. The first (parent) has the total size (and the other sub-arena's 0). + mi_arena_t* parent; // if this is a sub arena, this points to the first one in the memory area. + + mi_bbitmap_t* slices_free; // is the slice free? (a binned bitmap with size classes) + mi_bitmap_t* slices_committed; // is the slice committed? (i.e. accessible) + mi_bitmap_t* slices_dirty; // is the slice potentially non-zero? + mi_bitmap_t* slices_purge; // slices that can be purged + mi_page_t* pages_meta; // pre-allocated `slice_count` page meta info -- only used if `MI_PAGE_META_IS_SEPARATED!=0` + mi_arena_pages_t pages_main; // arena page bitmaps for the main heap are allocated up front as well + + // followed by the bitmaps (whose sizes depend on the arena size) + // note: when adding bitmaps revise `mi_arena_info_slices_needed` +} mi_arena_t; + + + +/* ----------------------------------------------------------- + Error codes passed to `_mi_fatal_error` + All are recoverable but EFAULT is a serious error and aborts by default in secure mode. + For portability define undefined error codes using common Unix codes: + +----------------------------------------------------------- */ + +#ifndef EAGAIN // double free +#define EAGAIN (11) +#endif +#ifndef ENOMEM // out of memory +#define ENOMEM (12) +#endif +#ifndef EFAULT // corrupted free-list or meta-data +#define EFAULT (14) +#endif +#ifndef EINVAL // trying to free an invalid pointer +#define EINVAL (22) +#endif +#ifndef EOVERFLOW // count*size overflow +#define EOVERFLOW (75) #endif +#ifndef ENOENT // environment variable not found +#define ENOENT (2) +#endif + + +/* ----------------------------------------------------------- + Debug constants +----------------------------------------------------------- */ + +#if !defined(MI_DEBUG_UNINIT) +#define MI_DEBUG_UNINIT (0xD0) +#endif +#if !defined(MI_DEBUG_FREED) +#define MI_DEBUG_FREED (0xDF) +#endif +#if !defined(MI_DEBUG_PADDING) +#define MI_DEBUG_PADDING (0xDE) +#endif + + +#endif // MI_TYPES_H diff --git a/Include/internal/mimalloc/mimalloc/util/libc.h b/Include/internal/mimalloc/mimalloc/util/libc.h new file mode 100644 index 000000000000000..f3305f36a06f9cf --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/util/libc.h @@ -0,0 +1,225 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_LIBC_H +#define MI_INTERNAL_LIBC_H + +#include "../types.h" + +#include // memset, memcpy, memcmp +#include // va_list, va_start, va_end, va_copy + +int _mi_vsnprintf(char* buf, size_t bufsize, const char* fmt, va_list args); +int _mi_snprintf(char* buf, size_t buflen, const char* fmt, ...); +char _mi_toupper(char c); +int _mi_strnicmp(const char* s, const char* t, size_t n); +bool _mi_strlcpy(char* dest, const char* src, size_t dest_size); // returns true if the entire src was copied +bool _mi_strlcat(char* dest, const char* src, size_t dest_size); // returns true if the entire src was appended +size_t _mi_strlen(const char* s); +size_t _mi_strnlen(const char* s, size_t max_len); +char* _mi_strnstr(char* s, size_t max_len, const char* pat); +const char* _mi_strchr(const char* s, char c); +const char* _mi_strrchr(const char* s, char c); +bool _mi_streq(const char* s, const char* t); +int _mi_getenv(const char* name, char* result, size_t result_size); +void _mi_detect_cpu_features(void); + + +// minimum +static inline size_t mi_min(size_t x, size_t y) { return (x <= y ? x : y); } + +// maximum +static inline size_t mi_max(size_t x, size_t y) { return (x >= y ? x : y); } + +// Is `x` a power of two? (0 is considered a power of two) +static inline bool mi_is_power_of_two(uintptr_t x) { + return ((x & (x - 1)) == 0); +} + +// valid alignment values are as posix memalign: +static inline bool mi_alignment_is_valid(size_t alignment) { + return ((alignment!=0) && mi_is_power_of_two(alignment)); +} + +// Is a pointer aligned? +static inline bool mi_is_aligned(const void* p, size_t alignment) { + return (alignment==0 || ((uintptr_t)p % alignment) == 0); +} + +// Align upwards +static inline uintptr_t mi_align_up(uintptr_t sz, size_t alignment) { + mi_assert_internal(alignment != 0); + const uintptr_t mask = alignment - 1; + if ((alignment & mask) == 0) { // power of two? + return ((sz + mask) & ~mask); + } + else { + return (((sz + mask)/alignment)*alignment); + } +} + +// Align a pointer upwards +static inline void* mi_align_up_ptr(const void* p, size_t alignment) { + return (void*)mi_align_up((uintptr_t)p, alignment); +} + +// Align down +static inline uintptr_t mi_align_down(uintptr_t sz, size_t alignment) { + mi_assert_internal(alignment != 0); + const uintptr_t mask = alignment - 1; + if ((alignment & mask) == 0) { // power of two? + return (sz & ~mask); + } + else { + return ((sz/alignment)*alignment); + } +} + +// Align a pointer downwards +static inline void* mi_align_down_ptr(const void* p, size_t alignment) { + return (void*)mi_align_down((uintptr_t)p, alignment); +} + +// Divide upwards: `s <= mi_divide_up(s,d)*d < s+d`. +static inline uintptr_t mi_divide_up(uintptr_t size, size_t divider) { + mi_assert_internal(divider != 0); + return (divider == 0 ? size : ((size + divider - 1) / divider)); +} + + +// clamp an integer +static inline size_t mi_clamp(size_t sz, size_t min, size_t max) { + if (sz < min) return min; + else if (sz > max) return max; + else return sz; +} + +// Overflow detecting multiply +#if __has_builtin(__builtin_umul_overflow) || (defined(__GNUC__) && (__GNUC__ >= 5)) +#include // UINT_MAX, ULONG_MAX +#if defined(_CLOCK_T) // for Illumos +#undef _CLOCK_T +#endif +static inline bool mi_mul_overflow(size_t count, size_t size, size_t* total) { + #if (SIZE_MAX == UINT_MAX) + return __builtin_umul_overflow(count, size, (unsigned int *)total); + #elif (SIZE_MAX == ULONG_MAX) + return __builtin_umull_overflow(count, size, (unsigned long *)total); + #else + return __builtin_umulll_overflow(count, size, (unsigned long long *)total); + #endif +} +#else /* __builtin_umul_overflow is unavailable */ +static inline bool mi_mul_overflow(size_t count, size_t size, size_t* total) { + *total = count*size; + if mi_likely(((size|count)>>(4*MI_SIZE_SIZE))==0) { // did size and count fit both in the lower half bits of a size_t? + return false; + } + else { + return (size!=0 && (SIZE_MAX / size) < count); + } +} +#endif + +// initialize a local variable to zero; use memset as compilers optimize constant sized memset's +#define _mi_memzero_var(x) memset(&x,0,sizeof(x)) + +// Is memory zero initialized? +static inline bool mi_mem_is_zero(const void* p, size_t size) { + for (size_t i = 0; i < size; i++) { + if (((uint8_t*)p)[i] != 0) return false; + } + return true; +} + +// --------------------------------------------------------------------------------- +// Provide our own `mi_memcpy/set` for potential performance optimizations. +// --------------------------------------------------------------------------------- + +static inline int mi_memcmp(const void* dst, const void* src, size_t n) { + return memcmp(dst, src, n); +} + +static inline void* mi_memcpy(void* dst, const void* src, size_t n) { + return memcpy(dst, src, n); +} + +static inline void* mi_memset(void* dst, int val, size_t n) { + return memset(dst, val, n); +} + +static inline void* mi_memset_backward(void* dst, int val, size_t n) { + memset((uint8_t*)dst - n, val, n); + return dst; +} + +static inline void* mi_memzero(void* dst, size_t n) { + return mi_memset(dst, 0, n); +} + +static inline void* mi_memzero_backward(void* dst, size_t n) { + return mi_memset_backward(dst, 0, n); +} + +static inline void* mi_memcpy_aligned(void* dst, const void* src, size_t n) { + // on gcc/clang we can provide a hint that the pointers are word aligned. + mi_assert_internal(mi_is_aligned(dst,MI_SIZE_SIZE) && mi_is_aligned(src,MI_SIZE_SIZE)); + void* adst = mi_assume_aligned(dst, MI_SIZE_SIZE); + const void* asrc = mi_assume_aligned(src, MI_SIZE_SIZE); + return mi_memcpy(adst, asrc, n); +} + +static inline void* mi_memset_aligned(void* dst, int val, size_t n) { + mi_assert_internal(mi_is_aligned(dst,MI_SIZE_SIZE)); + void* adst = mi_assume_aligned(dst, MI_SIZE_SIZE); + return mi_memset(adst, val, n); +} + +static inline void* mi_memzero_aligned(void* dst, size_t n) { + return mi_memset_aligned(dst, 0, n); +} + + +// Zero a block: blocks are always aligned with a positive bsize in machine-word bytes. +static mi_decl_forceinline void* mi_memzero_block(mi_block_t* dst, size_t bsize) { + mi_assert_internal(bsize%MI_SIZE_SIZE == 0); + mi_assert_internal(bsize > 0); + mi_assert_internal(mi_is_aligned(dst,MI_SIZE_SIZE)); + mi_assert_internal(bsize < MI_MAX_ALIGN_SIZE || mi_is_aligned(dst,MI_MAX_ALIGN_SIZE)); + + // fast memzero for small sizes based on overlapping writes (and assuming non-zero size_t-multiple size, and size_t aligned) + // assumes constant memset(p,0,N) gets optimized to fast simd stores by the compiler + // note: disabled on riscv for now as a constant memset is not always replaced correctly by current compilers. + // (compile with -DMI_USE_MEMZERO16X=0 to disable this) + #if (!defined(MI_USE_MEMZERO16X) && !MI_ARCH_RISCV) || (MI_USE_MEMZERO16X != 0) // 16x MI_SIZE_SIZE (128 bytes on 64-bit) + if mi_unlikely(bsize < 2*MI_SIZE_SIZE) { // bsize < 16 (8) + *((size_t*)dst) = 0; + return dst; + } + mi_assert_internal(mi_is_aligned(dst,MI_MAX_ALIGN_SIZE)); + uint8_t* const start = (uint8_t*)mi_assume_aligned(dst, MI_MAX_ALIGN_SIZE); + uint8_t* const end = start + bsize; // note: if bsize is always a multiple of 16 then end is always aligned as well (but due to padding this does not hold) + if mi_likely(bsize < 8*MI_SIZE_SIZE) { // bsize < 64 (32) + const size_t ofs = (bsize>>1)&(2*MI_SIZE_SIZE); mi_assert_internal(bsize < 4*MI_SIZE_SIZE ? ofs==0 : ofs==2*MI_SIZE_SIZE); // ofs == 16 (8) + mi_memzero(start, 2*MI_SIZE_SIZE); + mi_memzero(start+ofs, 2*MI_SIZE_SIZE); + mi_memzero_backward(end-ofs, 2*MI_SIZE_SIZE); + mi_memzero_backward(end, 2*MI_SIZE_SIZE); + return dst; + } + if mi_likely(bsize <= 16*MI_SIZE_SIZE) { // bsize < 128 (64) + mi_memzero(start, 8*MI_SIZE_SIZE); + mi_memzero_backward(end, 8*MI_SIZE_SIZE); + return dst; + } + #endif + // fallback to regular memset for larger sizes + void* const wdst = mi_assume_aligned(dst,MI_SIZE_SIZE); + return mi_memzero_aligned(wdst, bsize); +} + +#endif diff --git a/Include/internal/mimalloc/mimalloc/util/options.h b/Include/internal/mimalloc/mimalloc/util/options.h new file mode 100644 index 000000000000000..4334dcd8df0402d --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/util/options.h @@ -0,0 +1,43 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_OPTIONS_H +#define MI_INTERNAL_OPTIONS_H + +#include "../types.h" + +void _mi_fputs(mi_output_fun* out, void* arg, const char* prefix, const char* message); +void _mi_fprintf(mi_output_fun* out, void* arg, const char* fmt, ...); +void _mi_raw_message(const char* fmt, ...); +void _mi_warning_message(const char* fmt, ...); +void _mi_verbose_message(const char* fmt, ...); +void _mi_trace_message(const char* fmt, ...); +void _mi_options_init(void); +void _mi_options_post_init(void); +long _mi_option_get_fast(mi_option_t option); +void _mi_error_message(int err, const char* fmt, ...); +mi_block_t* _mi_block_next_is_corrupted(const mi_page_t* page, const mi_block_t* block, const mi_block_t* next); + +/* ----------------------------------------------------------- + Options (exposed for the debugger) +----------------------------------------------------------- */ +typedef enum mi_option_init_e { + MI_OPTION_UNINIT, // not yet initialized + MI_OPTION_DEFAULTED, // not found in the environment, use default value + MI_OPTION_INITIALIZED // found in environment or set explicitly +} mi_option_init_t; + +typedef struct mi_option_desc_s { + long value; // the value + mi_option_init_t init; // is it initialized yet? (from the environment) + mi_option_t option; // for debugging: the option index should match the option + const char* name; // option name without `mimalloc_` prefix + const char* legacy_name; // potential legacy option name +} mi_option_desc_t; + + +#endif diff --git a/Include/internal/mimalloc/mimalloc/util/random.h b/Include/internal/mimalloc/mimalloc/util/random.h new file mode 100644 index 000000000000000..458835accc14d19 --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/util/random.h @@ -0,0 +1,44 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_RANDOM_H +#define MI_INTERNAL_RANDOM_H + +#include "../types.h" + +void _mi_random_init(mi_random_ctx_t* ctx); +void _mi_random_init_weak(mi_random_ctx_t* ctx); +void _mi_random_reinit_if_weak(mi_random_ctx_t * ctx); +void _mi_random_split(mi_random_ctx_t* ctx, mi_random_ctx_t* new_ctx); +size_t _mi_random_next(mi_random_ctx_t* ctx); +size_t _mi_os_random_weak(size_t extra_seed); + +// ------------------------------------------------------------------- +// Fast "random" shuffle +// ------------------------------------------------------------------- + +static inline size_t mi_random_shuffle(size_t x) { + if (x==0) { x = 17; } // ensure we don't get stuck in generating zeros +#if (MI_SIZE_SIZE>=8) + // by Sebastiano Vigna, see: + x ^= x >> 30; + x *= 0xbf58476d1ce4e5b9UL; + x ^= x >> 27; + x *= 0x94d049bb133111ebUL; + x ^= x >> 31; +#elif (MI_SIZE_SIZE==4) + // by Chris Wellons, see: + x ^= x >> 16; + x *= 0x7feb352dUL; + x ^= x >> 15; + x *= 0x846ca68bUL; + x ^= x >> 16; +#endif + return x; +} + +#endif diff --git a/Include/internal/mimalloc/mimalloc/util/stats.h b/Include/internal/mimalloc/mimalloc/util/stats.h new file mode 100644 index 000000000000000..b46979d9c74453e --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/util/stats.h @@ -0,0 +1,76 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_STATS_H +#define MI_INTERNAL_STATS_H + +#include "../types.h" + +void _mi_stats_init(void); +void _mi_stats_merge_into(mi_stats_t* to, mi_stats_t* from); + +mi_msecs_t _mi_clock_now(void); +mi_msecs_t _mi_clock_end(mi_msecs_t start); +mi_msecs_t _mi_clock_start(void); + +/* ----------------------------------------------------------- + Statistics (in `util/stats.c`) +----------------------------------------------------------- */ + +// add to stat keeping track of the peak +void __mi_stat_increase(mi_stat_count_t* stat, uint64_t amount); +void __mi_stat_decrease(mi_stat_count_t* stat, uint64_t amount); +void __mi_stat_increase_mt(mi_stat_count_t* stat, uint64_t amount); +void __mi_stat_decrease_mt(mi_stat_count_t* stat, uint64_t amount); + +// adjust stat in special cases to compensate for double counting (and does not adjust peak values and can decrease the total) +void __mi_stat_adjust_increase(mi_stat_count_t* stat, uint64_t amount); +void __mi_stat_adjust_decrease(mi_stat_count_t* stat, uint64_t amount); +void __mi_stat_adjust_increase_mt(mi_stat_count_t* stat, uint64_t amount); +void __mi_stat_adjust_decrease_mt(mi_stat_count_t* stat, uint64_t amount); + +// counters can just be increased +static inline void __mi_stat_counter_increase_mt(mi_stat_counter_t* stat, uint64_t amount) { + mi_assert_internal(amount<=INT64_MAX); + mi_atomic_volatile_addi64_relaxed(&stat->total, (int64_t)amount); +} + +static inline void __mi_stat_counter_increase(mi_stat_counter_t* stat, uint64_t amount) { + mi_assert_internal(amount<=INT64_MAX); + stat->total += (int64_t)amount; +} + +static inline void __mi_stat_counter_decrease(mi_stat_counter_t* stat, uint64_t amount) { + mi_assert_internal(amount<=INT64_MAX); + stat->total -= (int64_t)amount; +} + +#define mi_heap_stat_counter_increase(heap,stat,amount) __mi_stat_counter_increase_mt( &(heap)->stats.stat, amount) +#define mi_heap_stat_increase(heap,stat,amount) __mi_stat_increase_mt( &(heap)->stats.stat, amount) +#define mi_heap_stat_decrease(heap,stat,amount) __mi_stat_decrease_mt( &(heap)->stats.stat, amount) +#define mi_heap_stat_adjust_increase(heap,stat,amnt) __mi_stat_adjust_increase_mt( &(heap)->stats.stat, amnt) +#define mi_heap_stat_adjust_decrease(heap,stat,amnt) __mi_stat_adjust_decrease_mt( &(heap)->stats.stat, amnt) + +#define mi_subproc_stat_counter_increase(subproc,stat,amount) __mi_stat_counter_increase_mt( &(subproc)->stats.stat, amount) +#define mi_subproc_stat_increase(subproc,stat,amount) __mi_stat_increase_mt( &(subproc)->stats.stat, amount) +#define mi_subproc_stat_decrease(subproc,stat,amount) __mi_stat_decrease_mt( &(subproc)->stats.stat, amount) +#define mi_subproc_stat_adjust_increase(subproc,stat,amount) __mi_stat_adjust_increase_mt( &(subproc)->stats.stat, amount) +#define mi_subproc_stat_adjust_decrease(subproc,stat,amount) __mi_stat_adjust_decrease_mt( &(subproc)->stats.stat, amount) + +#define mi_theap_stat_counter_increase(theap,stat,amount) __mi_stat_counter_increase( &(theap)->stats.stat, amount) +#define mi_theap_stat_counter_decrease(theap,stat,amount) __mi_stat_counter_decrease( &(theap)->stats.stat, amount) +#define mi_theap_stat_increase(theap,stat,amount) __mi_stat_increase( &(theap)->stats.stat, amount) +#define mi_theap_stat_decrease(theap,stat,amount) __mi_stat_decrease( &(theap)->stats.stat, amount) +#define mi_theap_stat_adjust_increase(theap,stat,amnt) __mi_stat_adjust_increase( &(theap)->stats.stat, amnt) +#define mi_theap_stat_adjust_decrease(theap,stat,amnt) __mi_stat_adjust_decrease( &(theap)->stats.stat, amnt) + +#define mi_theapx_stat_counter_increase(heap,theap,stat,amount) if (theap!=NULL) { mi_theap_stat_counter_increase(theap,stat,amount); } else { mi_heap_stat_counter_increase(heap,stat,amount); } +#define mi_theapx_stat_adjust_decrease(heap,theap,stat,amount) if (theap!=NULL) { mi_theap_stat_adjust_decrease(theap,stat,amount); } else { mi_heap_stat_adjust_decrease(heap,stat,amount); } +#define mi_theapx_stat_increase(heap,theap,stat,amount) if (theap!=NULL) { mi_theap_stat_increase(theap,stat,amount); } else { mi_heap_stat_increase(heap,stat,amount); } +#define mi_theapx_stat_decrease(heap,theap,stat,amount) if (theap!=NULL) { mi_theap_stat_decrease(theap,stat,amount); } else { mi_heap_stat_decrease(heap,stat,amount); } + +#endif diff --git a/Include/internal/mimalloc/mimalloc/util/threadlocal.h b/Include/internal/mimalloc/mimalloc/util/threadlocal.h new file mode 100644 index 000000000000000..f0207cdb5e6e5c4 --- /dev/null +++ b/Include/internal/mimalloc/mimalloc/util/threadlocal.h @@ -0,0 +1,23 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_INTERNAL_THREADLOCAL_H +#define MI_INTERNAL_THREADLOCAL_H + +#include "../types.h" + +#define mi_thread_local_key_fast ((mi_thread_local_t)1) + +mi_thread_local_t _mi_thread_local_create(void); +void _mi_thread_local_free( mi_thread_local_t key ); +bool _mi_thread_local_set( mi_thread_local_t key, void* val ); +void* _mi_thread_local_get( mi_thread_local_t key ); +void _mi_thread_locals_init(void); +void _mi_thread_locals_done(void); +void _mi_thread_locals_thread_done(void); + +#endif diff --git a/Include/internal/mimalloc/readme.md b/Include/internal/mimalloc/readme.md new file mode 100644 index 000000000000000..c968f05f00e6b06 --- /dev/null +++ b/Include/internal/mimalloc/readme.md @@ -0,0 +1,12 @@ +# Mimalloc Headers + +- [`mimalloc.h`](mimalloc.h): The main mimalloc API. +- [`mimalloc-override.h`](mimalloc-override.h): Macro-based redirection of standard allocation functions to mimalloc. One way to override all allocations functions is to include this file at the start of each source file (but of course, this only works +if you control all sources). +- [`mimalloc-new-delete.h`](mimalloc-new-delete.h): Global C++ `new` and `delete` overrides. Include this header in a single (!) source file in your project. +- [`mimalloc-stats.h`](mimalloc-stats.h): Statistics types and APIs for allocator statistics. +- [`mimalloc-profile.h`](mimalloc-profile.h): Allocation profiling callbacks, sampling controls, and the built-in pprof profiler API. +- [`mimalloc`](mimalloc): Internal allocator headers. + See in particular [`types.h`](mimalloc/types.h) for the type definitions of all + internal mimalloc data structures. + diff --git a/Include/internal/pycore_mimalloc.h b/Include/internal/pycore_mimalloc.h index d870d01beb702c0..b15fda5d3f6fddc 100644 --- a/Include/internal/pycore_mimalloc.h +++ b/Include/internal/pycore_mimalloc.h @@ -5,7 +5,7 @@ # error "this header requires Py_BUILD_CORE define" #endif -#if defined(MIMALLOC_H) || defined(MIMALLOC_TYPES_H) +#if defined(MIMALLOC_H) || defined(MI_TYPES_H) # error "pycore_mimalloc.h must be included before mimalloc.h" #endif @@ -20,8 +20,26 @@ typedef enum { #include "pycore_pymem.h" #ifdef WITH_MIMALLOC +# define MI_NO_PTHREAD_ATFORK 1 +# ifdef MS_WINDOWS +// Initialize from a CRT initializer and use FLS for thread cleanup instead +// of TLS callbacks, which run during DLL attach before the CRT is set up. +# define MI_WIN_INIT_USE_FLS 1 +# endif # ifdef Py_GIL_DISABLED # define MI_PRIM_THREAD_ID _Py_ThreadId +# define MI_CPYTHON 1 + +struct mi_page_qsbr_s { + uint64_t goal; +}; + +struct mi_heap_qsbr_s { + struct _qsbr_shared *shared; +}; +# else +// Keep full pages in their owning thread's allocation statistics. +# define MI_DEFAULT_PAGE_FULL_RETAIN -1 # endif # define MI_DEBUG_UNINIT PYMEM_CLEANBYTE # define MI_DEBUG_FREED PYMEM_DEADBYTE @@ -52,18 +70,17 @@ extern "C++" { #ifdef Py_GIL_DISABLED struct _mimalloc_interp_state { - // When exiting, threads place any segments with live blocks in this - // shared pool for other threads to claim and reuse. - mi_abandoned_pool_t abandoned_pool; + mi_heap_t *heaps[_Py_MIMALLOC_HEAP_COUNT]; }; struct _mimalloc_thread_state { - mi_heap_t *current_object_heap; - mi_heap_t heaps[_Py_MIMALLOC_HEAP_COUNT]; - mi_tld_t tld; + mi_theap_t *current_object_heap; + mi_theap_t *heaps[_Py_MIMALLOC_HEAP_COUNT]; int initialized; - struct llist_node page_list; }; + +extern int _PyMem_InitMimalloc(PyInterpreterState *interp); +extern void _PyMem_FiniMimalloc(PyInterpreterState *interp); #endif #endif // Py_INTERNAL_MIMALLOC_H diff --git a/Include/internal/pycore_object_alloc.h b/Include/internal/pycore_object_alloc.h index 8cc7a444bc93e71..415b2d9b9a1e6d4 100644 --- a/Include/internal/pycore_object_alloc.h +++ b/Include/internal/pycore_object_alloc.h @@ -14,18 +14,18 @@ extern "C" { #endif #ifdef Py_GIL_DISABLED -static inline mi_heap_t * +static inline mi_theap_t * _PyObject_GetAllocationHeap(_PyThreadStateImpl *tstate, PyTypeObject *tp) { struct _mimalloc_thread_state *m = &tstate->mimalloc; if (_PyType_HasFeature(tp, Py_TPFLAGS_PREHEADER)) { - return &m->heaps[_Py_MIMALLOC_HEAP_GC_PRE]; + return m->heaps[_Py_MIMALLOC_HEAP_GC_PRE]; } else if (_PyType_IS_GC(tp)) { - return &m->heaps[_Py_MIMALLOC_HEAP_GC]; + return m->heaps[_Py_MIMALLOC_HEAP_GC]; } else { - return &m->heaps[_Py_MIMALLOC_HEAP_OBJECT]; + return m->heaps[_Py_MIMALLOC_HEAP_OBJECT]; } } #endif @@ -45,7 +45,7 @@ _PyObject_MallocWithType(PyTypeObject *tp, size_t size) #endif void *mem = PyObject_Malloc(size); #ifdef Py_GIL_DISABLED - m->current_object_heap = &m->heaps[_Py_MIMALLOC_HEAP_OBJECT]; + m->current_object_heap = m->heaps[_Py_MIMALLOC_HEAP_OBJECT]; #endif return mem; } @@ -60,7 +60,7 @@ _PyObject_ReallocWithType(PyTypeObject *tp, void *ptr, size_t size) #endif void *mem = PyObject_Realloc(ptr, size); #ifdef Py_GIL_DISABLED - m->current_object_heap = &m->heaps[_Py_MIMALLOC_HEAP_OBJECT]; + m->current_object_heap = m->heaps[_Py_MIMALLOC_HEAP_OBJECT]; #endif return mem; } diff --git a/InternalDocs/qsbr.md b/InternalDocs/qsbr.md index d511396fdab6452..5c9f61fc2849846 100644 --- a/InternalDocs/qsbr.md +++ b/InternalDocs/qsbr.md @@ -136,6 +136,47 @@ by slightly delaying when memory can be reclaimed; the size-based thresholds above bound that extra memory. +### Mimalloc Integration + +Each interpreter owns four mimalloc heaps: one for `PyMem` allocations and +three for objects with different header layouts. Only the object heaps enable +QSBR. Each thread state retains a `mi_theap_t` handle for each heap. Clearing a +thread state abandons its pages without invalidating live objects; heap +visitation includes both owned and abandoned pages, so they remain visible to +the garbage collector. + +CPython supplies four typed hooks: `mi_heap_qsbr_init` associates a heap with its +interpreter's QSBR state, `mi_page_qsbr_clear` and `mi_page_qsbr_is_clear` manage +a page's goal, and `mi_page_qsbr_should_delay` establishes a goal if needed and +checks cached read-sequence progress. These hooks may run without an attached +Python thread state and while allocator locks are held. They must not poll the +thread registry. `_PyMem_ProcessDelayed` polls outside those locks, then drains +each object heap's thread-local delayed-page queue. + +Mimalloc owns the delayed-page queues and page transitions. A delayed empty +page can still supply allocations of its existing size class. If it becomes +empty again, its previous goal is cleared and a new grace period is required. +Queues are FIFO; an incomplete goal at the head may delay later pages. During +stop-the-world collection, CPython also sweeps abandoned pages using the same +cached QSBR progress. + +The object's reference-count fields and any pre-header remain untouched by +debug fills, using the heap's `debug_fill_preserve` setting. At interpreter +shutdown, empty heaps can be destroyed, but live allocations may still be +referenced elsewhere, including by a shared interned-string dictionary. +Retained object heaps are disconnected from the interpreter's QSBR state and +conservatively keep their pages rather than consulting a deleted domain. + +CPython disables mimalloc's automatic `pthread_atfork` handler and invokes its +child cleanup from `PyOS_AfterFork_Child`, after a coordinated fork. Exec-only +subprocess forks do not stop other allocator users and must not perform that +cleanup or use the allocator before exec. + +The regular GIL build does not enable QSBR and retains full pages in their +owning thread, as in mimalloc v2. Its allocation statistics visit only the +current thread's pages, so early abandonment would make those counts incomplete. + + ## Limitations Determining the `rd_seq` requires scanning over all thread states. This operation diff --git a/Lib/test/test_free_threading/test_gc.py b/Lib/test/test_free_threading/test_gc.py index 30282a111345f86..67b9078ea9f8470 100644 --- a/Lib/test/test_free_threading/test_gc.py +++ b/Lib/test/test_free_threading/test_gc.py @@ -17,6 +17,29 @@ class MyObj: @threading_helper.requires_working_threading() class TestGC(TestCase): + def test_collect_cycles_after_thread_exit(self): + class Slotted: + __slots__ = ("cycle", "__weakref__") + + refs = [] + + def worker(): + for cls in (MyObj, Slotted): + for _ in range(200): + obj = cls() + obj.cycle = obj + refs.append(weakref.ref(obj)) + obj = Slotted() + obj.cycle = (obj,) * 100_000 + refs.append(weakref.ref(obj)) + + with support.disable_gc(): + with threading_helper.start_threads([Thread(target=worker)]): + pass + self.assertTrue(all(ref() is not None for ref in refs)) + gc.collect() + self.assertTrue(all(ref() is None for ref in refs)) + def test_get_objects(self): event = threading.Event() diff --git a/Lib/test/test_free_threading/test_interpreters.py b/Lib/test/test_free_threading/test_interpreters.py index a9a432fae16b1e1..b68014d214b9679 100644 --- a/Lib/test/test_free_threading/test_interpreters.py +++ b/Lib/test/test_free_threading/test_interpreters.py @@ -9,6 +9,22 @@ class InterpreterTeardownTests(unittest.TestCase): + def test_shared_interned_strings_survive_teardown(self): + import_helper.import_module('_testcapi') + script = textwrap.dedent(""" + import _testcapi + import gc + import sys + + for i in range(10): + code = f'import sys; sys.intern("retained_{i}_" + "x" * 5000)' + assert _testcapi.run_in_subinterp(code) == 0 + gc.collect() + expected = f"retained_{i}_" + "x" * 5000 + assert sys.intern(expected) == expected + """) + script_helper.assert_python_ok('-c', script) + def test_destroy_subinterpreter_does_not_abort(self): # gh-153176: destroy_interpreter(basic=True) used to call # PyThreadState_Clear() on a non-current thread state, which on a diff --git a/Makefile.pre.in b/Makefile.pre.in index 308fb964a0cd57e..e2fbf6be83eab37 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -374,12 +374,9 @@ IO_OBJS= \ MIMALLOC_HEADERS= \ $(srcdir)/Include/internal/pycore_mimalloc.h \ - $(srcdir)/Include/internal/mimalloc/mimalloc.h \ - $(srcdir)/Include/internal/mimalloc/mimalloc/atomic.h \ - $(srcdir)/Include/internal/mimalloc/mimalloc/internal.h \ - $(srcdir)/Include/internal/mimalloc/mimalloc/prim.h \ - $(srcdir)/Include/internal/mimalloc/mimalloc/track.h \ - $(srcdir)/Include/internal/mimalloc/mimalloc/types.h + $(wildcard $(srcdir)/Include/internal/mimalloc/*.h \ + $(srcdir)/Include/internal/mimalloc/mimalloc/*.h \ + $(srcdir)/Include/internal/mimalloc/mimalloc/*/*.h) ########################################################################## @@ -2076,26 +2073,11 @@ Objects/unicodeobject.o: $(srcdir)/Objects/unicodeobject.c $(UNICODE_DEPS) Objects/dictobject.o: $(srcdir)/Objects/stringlib/eq.h Objects/setobject.o: $(srcdir)/Objects/stringlib/eq.h -Objects/obmalloc.o: $(srcdir)/Objects/mimalloc/alloc.c \ - $(srcdir)/Objects/mimalloc/alloc-aligned.c \ - $(srcdir)/Objects/mimalloc/alloc-posix.c \ - $(srcdir)/Objects/mimalloc/arena.c \ - $(srcdir)/Objects/mimalloc/bitmap.c \ - $(srcdir)/Objects/mimalloc/heap.c \ - $(srcdir)/Objects/mimalloc/init.c \ - $(srcdir)/Objects/mimalloc/options.c \ - $(srcdir)/Objects/mimalloc/os.c \ - $(srcdir)/Objects/mimalloc/page.c \ - $(srcdir)/Objects/mimalloc/random.c \ - $(srcdir)/Objects/mimalloc/segment.c \ - $(srcdir)/Objects/mimalloc/segment-map.c \ - $(srcdir)/Objects/mimalloc/stats.c \ - $(srcdir)/Objects/mimalloc/prim/prim.c \ - $(srcdir)/Objects/mimalloc/prim/osx/prim.c \ - $(srcdir)/Objects/mimalloc/prim/unix/prim.c \ - $(srcdir)/Objects/mimalloc/prim/wasi/prim.c - -Objects/mimalloc/page.o: $(srcdir)/Objects/mimalloc/page-queue.c +Objects/obmalloc.o: $(wildcard $(srcdir)/Objects/mimalloc/*.c \ + $(srcdir)/Objects/mimalloc/*/*.c \ + $(srcdir)/Objects/mimalloc/*/*/*.c \ + $(srcdir)/Objects/mimalloc/*/*.h \ + $(srcdir)/Objects/mimalloc/*/*/*.h) # Regenerate various files from Python/bytecodes.c @@ -2924,12 +2906,12 @@ inclinstall: $(INSTALL_DATA) $$i $(DESTDIR)$(INCLUDEPY)/internal; \ done @if test "$(INSTALL_MIMALLOC)" = "yes"; then \ - echo $(INSTALL_DATA) $(srcdir)/Include/internal/mimalloc/mimalloc.h $(DESTDIR)$(INCLUDEPY)/internal/mimalloc/mimalloc.h; \ - $(INSTALL_DATA) $(srcdir)/Include/internal/mimalloc/mimalloc.h $(DESTDIR)$(INCLUDEPY)/internal/mimalloc/mimalloc.h; \ - for i in $(srcdir)/Include/internal/mimalloc/mimalloc/*.h; \ + for i in $(filter-out $(srcdir)/Include/internal/pycore_mimalloc.h,$(MIMALLOC_HEADERS)); \ do \ - echo $(INSTALL_DATA) $$i $(INCLUDEPY)/internal/mimalloc/mimalloc; \ - $(INSTALL_DATA) $$i $(DESTDIR)$(INCLUDEPY)/internal/mimalloc/mimalloc; \ + dest=$(DESTDIR)$(INCLUDEPY)/$${i#$(srcdir)/Include/}; \ + $(INSTALL) -d -m $(DIRMODE) "$$(dirname "$$dest")"; \ + echo $(INSTALL_DATA) $$i $$dest; \ + $(INSTALL_DATA) $$i $$dest; \ done; \ fi echo $(INSTALL_DATA) pyconfig.h $(DESTDIR)$(CONFINCLUDEPY)/pyconfig.h diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 301afe1fa27be68..905b61f0383a927 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -760,6 +760,10 @@ PyOS_AfterFork_Child(void) PyStatus status; _PyRuntimeState *runtime = &_PyRuntime; +#ifdef WITH_MIMALLOC + _mi_fork_child(); +#endif + // re-creates runtime->interpreters.mutex (HEAD_UNLOCK) status = _PyRuntimeState_ReInitThreads(runtime); if (_PyStatus_EXCEPTION(status)) { diff --git a/Objects/mimalloc/LICENSE b/Objects/mimalloc/LICENSE new file mode 100644 index 000000000000000..53315ebee557ac2 --- /dev/null +++ b/Objects/mimalloc/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018-2025 Microsoft Corporation, Daan Leijen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Objects/mimalloc/alloc-aligned.c b/Objects/mimalloc/alloc-aligned.c deleted file mode 100644 index 4c15f4043ec117f..000000000000000 --- a/Objects/mimalloc/alloc-aligned.c +++ /dev/null @@ -1,298 +0,0 @@ -/* ---------------------------------------------------------------------------- -Copyright (c) 2018-2021, Microsoft Research, Daan Leijen -This is free software; you can redistribute it and/or modify it under the -terms of the MIT license. A copy of the license can be found in the file -"LICENSE" at the root of this distribution. ------------------------------------------------------------------------------*/ - -#include "mimalloc.h" -#include "mimalloc/internal.h" -#include "mimalloc/prim.h" // mi_prim_get_default_heap - -#include // memset - -// ------------------------------------------------------ -// Aligned Allocation -// ------------------------------------------------------ - -// Fallback primitive aligned allocation -- split out for better codegen -static mi_decl_noinline void* mi_heap_malloc_zero_aligned_at_fallback(mi_heap_t* const heap, const size_t size, const size_t alignment, const size_t offset, const bool zero) mi_attr_noexcept -{ - mi_assert_internal(size <= PTRDIFF_MAX); - mi_assert_internal(alignment != 0 && _mi_is_power_of_two(alignment)); - - const uintptr_t align_mask = alignment - 1; // for any x, `(x & align_mask) == (x % alignment)` - const size_t padsize = size + MI_PADDING_SIZE; - - // use regular allocation if it is guaranteed to fit the alignment constraints - if (offset==0 && alignment<=padsize && padsize<=MI_MAX_ALIGN_GUARANTEE && (padsize&align_mask)==0) { - void* p = _mi_heap_malloc_zero(heap, size, zero); - mi_assert_internal(p == NULL || ((uintptr_t)p % alignment) == 0); - return p; - } - - void* p; - size_t oversize; - if mi_unlikely(alignment > MI_ALIGNMENT_MAX) { - // use OS allocation for very large alignment and allocate inside a huge page (dedicated segment with 1 page) - // This can support alignments >= MI_SEGMENT_SIZE by ensuring the object can be aligned at a point in the - // first (and single) page such that the segment info is `MI_SEGMENT_SIZE` bytes before it (so it can be found by aligning the pointer down) - if mi_unlikely(offset != 0) { - // todo: cannot support offset alignment for very large alignments yet - #if MI_DEBUG > 0 - _mi_error_message(EOVERFLOW, "aligned allocation with a very large alignment cannot be used with an alignment offset (size %zu, alignment %zu, offset %zu)\n", size, alignment, offset); - #endif - return NULL; - } - oversize = (size <= MI_SMALL_SIZE_MAX ? MI_SMALL_SIZE_MAX + 1 /* ensure we use generic malloc path */ : size); - p = _mi_heap_malloc_zero_ex(heap, oversize, false, alignment); // the page block size should be large enough to align in the single huge page block - // zero afterwards as only the area from the aligned_p may be committed! - if (p == NULL) return NULL; - } - else { - // otherwise over-allocate - oversize = size + alignment - 1; - p = _mi_heap_malloc_zero(heap, oversize, zero); - if (p == NULL) return NULL; - } - - // .. and align within the allocation - const uintptr_t poffset = ((uintptr_t)p + offset) & align_mask; - const uintptr_t adjust = (poffset == 0 ? 0 : alignment - poffset); - mi_assert_internal(adjust < alignment); - void* aligned_p = (void*)((uintptr_t)p + adjust); - if (aligned_p != p) { - mi_page_t* page = _mi_ptr_page(p); - mi_page_set_has_aligned(page, true); - _mi_padding_shrink(page, (mi_block_t*)p, adjust + size); - } - // todo: expand padding if overallocated ? - - mi_assert_internal(mi_page_usable_block_size(_mi_ptr_page(p)) >= adjust + size); - mi_assert_internal(p == _mi_page_ptr_unalign(_mi_ptr_segment(aligned_p), _mi_ptr_page(aligned_p), aligned_p)); - mi_assert_internal(((uintptr_t)aligned_p + offset) % alignment == 0); - mi_assert_internal(mi_usable_size(aligned_p)>=size); - mi_assert_internal(mi_usable_size(p) == mi_usable_size(aligned_p)+adjust); - - // now zero the block if needed - if (alignment > MI_ALIGNMENT_MAX) { - // for the tracker, on huge aligned allocations only from the start of the large block is defined - mi_track_mem_undefined(aligned_p, size); - if (zero) { - _mi_memzero_aligned(aligned_p, mi_usable_size(aligned_p)); - } - } - - if (p != aligned_p) { - mi_track_align(p,aligned_p,adjust,mi_usable_size(aligned_p)); - } - return aligned_p; -} - -// Primitive aligned allocation -static void* mi_heap_malloc_zero_aligned_at(mi_heap_t* const heap, const size_t size, const size_t alignment, const size_t offset, const bool zero) mi_attr_noexcept -{ - // note: we don't require `size > offset`, we just guarantee that the address at offset is aligned regardless of the allocated size. - if mi_unlikely(alignment == 0 || !_mi_is_power_of_two(alignment)) { // require power-of-two (see ) - #if MI_DEBUG > 0 - _mi_error_message(EOVERFLOW, "aligned allocation requires the alignment to be a power-of-two (size %zu, alignment %zu)\n", size, alignment); - #endif - return NULL; - } - - if mi_unlikely(size > PTRDIFF_MAX) { // we don't allocate more than PTRDIFF_MAX (see ) - #if MI_DEBUG > 0 - _mi_error_message(EOVERFLOW, "aligned allocation request is too large (size %zu, alignment %zu)\n", size, alignment); - #endif - return NULL; - } - const uintptr_t align_mask = alignment-1; // for any x, `(x & align_mask) == (x % alignment)` - const size_t padsize = size + MI_PADDING_SIZE; // note: cannot overflow due to earlier size > PTRDIFF_MAX check - - // try first if there happens to be a small block available with just the right alignment - if mi_likely(padsize <= MI_SMALL_SIZE_MAX && alignment <= padsize) { - mi_page_t* page = _mi_heap_get_free_small_page(heap, padsize); - const bool is_aligned = (((uintptr_t)page->free+offset) & align_mask)==0; - if mi_likely(page->free != NULL && is_aligned) - { - #if MI_STAT>1 - mi_heap_stat_increase(heap, malloc, size); - #endif - void* p = _mi_page_malloc(heap, page, padsize, zero); // TODO: inline _mi_page_malloc - mi_assert_internal(p != NULL); - mi_assert_internal(((uintptr_t)p + offset) % alignment == 0); - mi_track_malloc(p,size,zero); - return p; - } - } - // fallback - return mi_heap_malloc_zero_aligned_at_fallback(heap, size, alignment, offset, zero); -} - - -// ------------------------------------------------------ -// Optimized mi_heap_malloc_aligned / mi_malloc_aligned -// ------------------------------------------------------ - -mi_decl_nodiscard mi_decl_restrict void* mi_heap_malloc_aligned_at(mi_heap_t* heap, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { - return mi_heap_malloc_zero_aligned_at(heap, size, alignment, offset, false); -} - -mi_decl_nodiscard mi_decl_restrict void* mi_heap_malloc_aligned(mi_heap_t* heap, size_t size, size_t alignment) mi_attr_noexcept { - if mi_unlikely(alignment == 0 || !_mi_is_power_of_two(alignment)) return NULL; - #if !MI_PADDING - // without padding, any small sized allocation is naturally aligned (see also `_mi_segment_page_start`) - if mi_likely(_mi_is_power_of_two(size) && size >= alignment && size <= MI_SMALL_SIZE_MAX) - #else - // with padding, we can only guarantee this for fixed alignments - if mi_likely((alignment == sizeof(void*) || (alignment == MI_MAX_ALIGN_SIZE && size > (MI_MAX_ALIGN_SIZE/2))) - && size <= MI_SMALL_SIZE_MAX) - #endif - { - // fast path for common alignment and size - return mi_heap_malloc_small(heap, size); - } - else { - return mi_heap_malloc_aligned_at(heap, size, alignment, 0); - } -} - -// ensure a definition is emitted -#if defined(__cplusplus) -static void* _mi_heap_malloc_aligned = (void*)&mi_heap_malloc_aligned; -#endif - -// ------------------------------------------------------ -// Aligned Allocation -// ------------------------------------------------------ - -mi_decl_nodiscard mi_decl_restrict void* mi_heap_zalloc_aligned_at(mi_heap_t* heap, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { - return mi_heap_malloc_zero_aligned_at(heap, size, alignment, offset, true); -} - -mi_decl_nodiscard mi_decl_restrict void* mi_heap_zalloc_aligned(mi_heap_t* heap, size_t size, size_t alignment) mi_attr_noexcept { - return mi_heap_zalloc_aligned_at(heap, size, alignment, 0); -} - -mi_decl_nodiscard mi_decl_restrict void* mi_heap_calloc_aligned_at(mi_heap_t* heap, size_t count, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { - size_t total; - if (mi_count_size_overflow(count, size, &total)) return NULL; - return mi_heap_zalloc_aligned_at(heap, total, alignment, offset); -} - -mi_decl_nodiscard mi_decl_restrict void* mi_heap_calloc_aligned(mi_heap_t* heap, size_t count, size_t size, size_t alignment) mi_attr_noexcept { - return mi_heap_calloc_aligned_at(heap,count,size,alignment,0); -} - -mi_decl_nodiscard mi_decl_restrict void* mi_malloc_aligned_at(size_t size, size_t alignment, size_t offset) mi_attr_noexcept { - return mi_heap_malloc_aligned_at(mi_prim_get_default_heap(), size, alignment, offset); -} - -mi_decl_nodiscard mi_decl_restrict void* mi_malloc_aligned(size_t size, size_t alignment) mi_attr_noexcept { - return mi_heap_malloc_aligned(mi_prim_get_default_heap(), size, alignment); -} - -mi_decl_nodiscard mi_decl_restrict void* mi_zalloc_aligned_at(size_t size, size_t alignment, size_t offset) mi_attr_noexcept { - return mi_heap_zalloc_aligned_at(mi_prim_get_default_heap(), size, alignment, offset); -} - -mi_decl_nodiscard mi_decl_restrict void* mi_zalloc_aligned(size_t size, size_t alignment) mi_attr_noexcept { - return mi_heap_zalloc_aligned(mi_prim_get_default_heap(), size, alignment); -} - -mi_decl_nodiscard mi_decl_restrict void* mi_calloc_aligned_at(size_t count, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { - return mi_heap_calloc_aligned_at(mi_prim_get_default_heap(), count, size, alignment, offset); -} - -mi_decl_nodiscard mi_decl_restrict void* mi_calloc_aligned(size_t count, size_t size, size_t alignment) mi_attr_noexcept { - return mi_heap_calloc_aligned(mi_prim_get_default_heap(), count, size, alignment); -} - - -// ------------------------------------------------------ -// Aligned re-allocation -// ------------------------------------------------------ - -static void* mi_heap_realloc_zero_aligned_at(mi_heap_t* heap, void* p, size_t newsize, size_t alignment, size_t offset, bool zero) mi_attr_noexcept { - mi_assert(alignment > 0); - if (alignment <= sizeof(uintptr_t)) return _mi_heap_realloc_zero(heap,p,newsize,zero); - if (p == NULL) return mi_heap_malloc_zero_aligned_at(heap,newsize,alignment,offset,zero); - size_t size = mi_usable_size(p); - if (newsize <= size && newsize >= (size - (size / 2)) - && (((uintptr_t)p + offset) % alignment) == 0) { - return p; // reallocation still fits, is aligned and not more than 50% waste - } - else { - // note: we don't zero allocate upfront so we only zero initialize the expanded part - void* newp = mi_heap_malloc_aligned_at(heap,newsize,alignment,offset); - if (newp != NULL) { - if (zero && newsize > size) { - // also set last word in the previous allocation to zero to ensure any padding is zero-initialized - size_t start = (size >= sizeof(intptr_t) ? size - sizeof(intptr_t) : 0); - _mi_memzero((uint8_t*)newp + start, newsize - start); - } - _mi_memcpy_aligned(newp, p, (newsize > size ? size : newsize)); - mi_free(p); // only free if successful - } - return newp; - } -} - -static void* mi_heap_realloc_zero_aligned(mi_heap_t* heap, void* p, size_t newsize, size_t alignment, bool zero) mi_attr_noexcept { - mi_assert(alignment > 0); - if (alignment <= sizeof(uintptr_t)) return _mi_heap_realloc_zero(heap,p,newsize,zero); - size_t offset = ((uintptr_t)p % alignment); // use offset of previous allocation (p can be NULL) - return mi_heap_realloc_zero_aligned_at(heap,p,newsize,alignment,offset,zero); -} - -mi_decl_nodiscard void* mi_heap_realloc_aligned_at(mi_heap_t* heap, void* p, size_t newsize, size_t alignment, size_t offset) mi_attr_noexcept { - return mi_heap_realloc_zero_aligned_at(heap,p,newsize,alignment,offset,false); -} - -mi_decl_nodiscard void* mi_heap_realloc_aligned(mi_heap_t* heap, void* p, size_t newsize, size_t alignment) mi_attr_noexcept { - return mi_heap_realloc_zero_aligned(heap,p,newsize,alignment,false); -} - -mi_decl_nodiscard void* mi_heap_rezalloc_aligned_at(mi_heap_t* heap, void* p, size_t newsize, size_t alignment, size_t offset) mi_attr_noexcept { - return mi_heap_realloc_zero_aligned_at(heap, p, newsize, alignment, offset, true); -} - -mi_decl_nodiscard void* mi_heap_rezalloc_aligned(mi_heap_t* heap, void* p, size_t newsize, size_t alignment) mi_attr_noexcept { - return mi_heap_realloc_zero_aligned(heap, p, newsize, alignment, true); -} - -mi_decl_nodiscard void* mi_heap_recalloc_aligned_at(mi_heap_t* heap, void* p, size_t newcount, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { - size_t total; - if (mi_count_size_overflow(newcount, size, &total)) return NULL; - return mi_heap_rezalloc_aligned_at(heap, p, total, alignment, offset); -} - -mi_decl_nodiscard void* mi_heap_recalloc_aligned(mi_heap_t* heap, void* p, size_t newcount, size_t size, size_t alignment) mi_attr_noexcept { - size_t total; - if (mi_count_size_overflow(newcount, size, &total)) return NULL; - return mi_heap_rezalloc_aligned(heap, p, total, alignment); -} - -mi_decl_nodiscard void* mi_realloc_aligned_at(void* p, size_t newsize, size_t alignment, size_t offset) mi_attr_noexcept { - return mi_heap_realloc_aligned_at(mi_prim_get_default_heap(), p, newsize, alignment, offset); -} - -mi_decl_nodiscard void* mi_realloc_aligned(void* p, size_t newsize, size_t alignment) mi_attr_noexcept { - return mi_heap_realloc_aligned(mi_prim_get_default_heap(), p, newsize, alignment); -} - -mi_decl_nodiscard void* mi_rezalloc_aligned_at(void* p, size_t newsize, size_t alignment, size_t offset) mi_attr_noexcept { - return mi_heap_rezalloc_aligned_at(mi_prim_get_default_heap(), p, newsize, alignment, offset); -} - -mi_decl_nodiscard void* mi_rezalloc_aligned(void* p, size_t newsize, size_t alignment) mi_attr_noexcept { - return mi_heap_rezalloc_aligned(mi_prim_get_default_heap(), p, newsize, alignment); -} - -mi_decl_nodiscard void* mi_recalloc_aligned_at(void* p, size_t newcount, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { - return mi_heap_recalloc_aligned_at(mi_prim_get_default_heap(), p, newcount, size, alignment, offset); -} - -mi_decl_nodiscard void* mi_recalloc_aligned(void* p, size_t newcount, size_t size, size_t alignment) mi_attr_noexcept { - return mi_heap_recalloc_aligned(mi_prim_get_default_heap(), p, newcount, size, alignment); -} diff --git a/Objects/mimalloc/alloc.c b/Objects/mimalloc/alloc.c deleted file mode 100644 index 44c84cf1931717b..000000000000000 --- a/Objects/mimalloc/alloc.c +++ /dev/null @@ -1,1074 +0,0 @@ -/* ---------------------------------------------------------------------------- -Copyright (c) 2018-2022, Microsoft Research, Daan Leijen -This is free software; you can redistribute it and/or modify it under the -terms of the MIT license. A copy of the license can be found in the file -"LICENSE" at the root of this distribution. ------------------------------------------------------------------------------*/ -#ifndef _DEFAULT_SOURCE -#define _DEFAULT_SOURCE // for realpath() on Linux -#endif - -#include "mimalloc.h" -#include "mimalloc/internal.h" -#include "mimalloc/atomic.h" -#include "mimalloc/prim.h" // _mi_prim_thread_id() - -#include // memset, strlen (for mi_strdup) -#include // malloc, abort - -#define _ZSt15get_new_handlerv _Py__ZSt15get_new_handlerv - -#define MI_IN_ALLOC_C -#include "alloc-override.c" -#undef MI_IN_ALLOC_C - -// ------------------------------------------------------ -// Allocation -// ------------------------------------------------------ - -#if (MI_DEBUG>0) -static inline void mi_debug_fill(mi_page_t* page, mi_block_t* block, int c, size_t size) { - size_t offset = (size_t)page->debug_offset; - if (offset < size) { - memset((char*)block + offset, c, size - offset); - } -} -#endif - -// Fast allocation in a page: just pop from the free list. -// Fall back to generic allocation only if the list is empty. -extern inline void* _mi_page_malloc(mi_heap_t* heap, mi_page_t* page, size_t size, bool zero) mi_attr_noexcept { - mi_assert_internal(page->xblock_size==0||mi_page_block_size(page) >= size); - mi_block_t* const block = page->free; - if mi_unlikely(block == NULL) { - return _mi_malloc_generic(heap, size, zero, 0); - } - mi_assert_internal(block != NULL && _mi_ptr_page(block) == page); - // pop from the free list - page->used++; - page->free = mi_block_next(page, block); - mi_assert_internal(page->free == NULL || _mi_ptr_page(page->free) == page); - #if MI_DEBUG>3 - if (page->free_is_zero) { - mi_assert_expensive(mi_mem_is_zero(block+1,size - sizeof(*block))); - } - #endif - - // allow use of the block internally - // note: when tracking we need to avoid ever touching the MI_PADDING since - // that is tracked by valgrind etc. as non-accessible (through the red-zone, see `mimalloc/track.h`) - mi_track_mem_undefined(block, mi_page_usable_block_size(page)); - - // zero the block? note: we need to zero the full block size (issue #63) - if mi_unlikely(zero) { - mi_assert_internal(page->xblock_size != 0); // do not call with zero'ing for huge blocks (see _mi_malloc_generic) - mi_assert_internal(page->xblock_size >= MI_PADDING_SIZE); - if (page->free_is_zero) { - block->next = 0; - mi_track_mem_defined(block, page->xblock_size - MI_PADDING_SIZE); - } - else { - _mi_memzero_aligned(block, page->xblock_size - MI_PADDING_SIZE); - } - } - -#if (MI_DEBUG>0) && !MI_TRACK_ENABLED && !MI_TSAN - if (!zero && !mi_page_is_huge(page)) { - mi_debug_fill(page, block, MI_DEBUG_UNINIT, mi_page_usable_block_size(page)); - } -#elif (MI_SECURE!=0) - if (!zero) { block->next = 0; } // don't leak internal data -#endif - -#if (MI_STAT>0) - const size_t bsize = mi_page_usable_block_size(page); - if (bsize <= MI_MEDIUM_OBJ_SIZE_MAX) { - mi_heap_stat_increase(heap, normal, bsize); - mi_heap_stat_counter_increase(heap, normal_count, 1); -#if (MI_STAT>1) - const size_t bin = _mi_bin(bsize); - mi_heap_stat_increase(heap, normal_bins[bin], 1); -#endif - } -#endif - -#if MI_PADDING // && !MI_TRACK_ENABLED - mi_padding_t* const padding = (mi_padding_t*)((uint8_t*)block + mi_page_usable_block_size(page)); - ptrdiff_t delta = ((uint8_t*)padding - (uint8_t*)block - (size - MI_PADDING_SIZE)); - #if (MI_DEBUG>=2) - mi_assert_internal(delta >= 0 && mi_page_usable_block_size(page) >= (size - MI_PADDING_SIZE + delta)); - #endif - mi_track_mem_defined(padding,sizeof(mi_padding_t)); // note: re-enable since mi_page_usable_block_size may set noaccess - padding->canary = (uint32_t)(mi_ptr_encode(page,block,page->keys)); - padding->delta = (uint32_t)(delta); - #if MI_PADDING_CHECK - if (!mi_page_is_huge(page)) { - uint8_t* fill = (uint8_t*)padding - delta; - const size_t maxpad = (delta > MI_MAX_ALIGN_SIZE ? MI_MAX_ALIGN_SIZE : delta); // set at most N initial padding bytes - for (size_t i = 0; i < maxpad; i++) { fill[i] = MI_DEBUG_PADDING; } - } - #endif -#endif - - return block; -} - -static inline mi_decl_restrict void* mi_heap_malloc_small_zero(mi_heap_t* heap, size_t size, bool zero) mi_attr_noexcept { - mi_assert(heap != NULL); - #if MI_DEBUG - const uintptr_t tid = _mi_thread_id(); - mi_assert(heap->thread_id == 0 || heap->thread_id == tid); // heaps are thread local - #endif - mi_assert(size <= MI_SMALL_SIZE_MAX); - #if (MI_PADDING) - if (size == 0) { size = sizeof(void*); } - #endif - mi_page_t* page = _mi_heap_get_free_small_page(heap, size + MI_PADDING_SIZE); - void* const p = _mi_page_malloc(heap, page, size + MI_PADDING_SIZE, zero); - mi_track_malloc(p,size,zero); - #if MI_STAT>1 - if (p != NULL) { - if (!mi_heap_is_initialized(heap)) { heap = mi_prim_get_default_heap(); } - mi_heap_stat_increase(heap, malloc, mi_usable_size(p)); - } - #endif - #if MI_DEBUG>3 - if (p != NULL && zero) { - mi_assert_expensive(mi_mem_is_zero(p, size)); - } - #endif - return p; -} - -// allocate a small block -mi_decl_nodiscard extern inline mi_decl_restrict void* mi_heap_malloc_small(mi_heap_t* heap, size_t size) mi_attr_noexcept { - return mi_heap_malloc_small_zero(heap, size, false); -} - -mi_decl_nodiscard extern inline mi_decl_restrict void* mi_malloc_small(size_t size) mi_attr_noexcept { - return mi_heap_malloc_small(mi_prim_get_default_heap(), size); -} - -// The main allocation function -extern inline void* _mi_heap_malloc_zero_ex(mi_heap_t* heap, size_t size, bool zero, size_t huge_alignment) mi_attr_noexcept { - if mi_likely(size <= MI_SMALL_SIZE_MAX) { - mi_assert_internal(huge_alignment == 0); - return mi_heap_malloc_small_zero(heap, size, zero); - } - else { - mi_assert(heap!=NULL); - mi_assert(heap->thread_id == 0 || heap->thread_id == _mi_thread_id()); // heaps are thread local - void* const p = _mi_malloc_generic(heap, size + MI_PADDING_SIZE, zero, huge_alignment); // note: size can overflow but it is detected in malloc_generic - mi_track_malloc(p,size,zero); - #if MI_STAT>1 - if (p != NULL) { - if (!mi_heap_is_initialized(heap)) { heap = mi_prim_get_default_heap(); } - mi_heap_stat_increase(heap, malloc, mi_usable_size(p)); - } - #endif - #if MI_DEBUG>3 - if (p != NULL && zero) { - mi_assert_expensive(mi_mem_is_zero(p, size)); - } - #endif - return p; - } -} - -extern inline void* _mi_heap_malloc_zero(mi_heap_t* heap, size_t size, bool zero) mi_attr_noexcept { - return _mi_heap_malloc_zero_ex(heap, size, zero, 0); -} - -mi_decl_nodiscard extern inline mi_decl_restrict void* mi_heap_malloc(mi_heap_t* heap, size_t size) mi_attr_noexcept { - return _mi_heap_malloc_zero(heap, size, false); -} - -mi_decl_nodiscard extern inline mi_decl_restrict void* mi_malloc(size_t size) mi_attr_noexcept { - return mi_heap_malloc(mi_prim_get_default_heap(), size); -} - -// zero initialized small block -mi_decl_nodiscard mi_decl_restrict void* mi_zalloc_small(size_t size) mi_attr_noexcept { - return mi_heap_malloc_small_zero(mi_prim_get_default_heap(), size, true); -} - -mi_decl_nodiscard extern inline mi_decl_restrict void* mi_heap_zalloc(mi_heap_t* heap, size_t size) mi_attr_noexcept { - return _mi_heap_malloc_zero(heap, size, true); -} - -mi_decl_nodiscard mi_decl_restrict void* mi_zalloc(size_t size) mi_attr_noexcept { - return mi_heap_zalloc(mi_prim_get_default_heap(),size); -} - - -// ------------------------------------------------------ -// Check for double free in secure and debug mode -// This is somewhat expensive so only enabled for secure mode 4 -// ------------------------------------------------------ - -#if (MI_ENCODE_FREELIST && (MI_SECURE>=4 || MI_DEBUG!=0)) -// linear check if the free list contains a specific element -static bool mi_list_contains(const mi_page_t* page, const mi_block_t* list, const mi_block_t* elem) { - while (list != NULL) { - if (elem==list) return true; - list = mi_block_next(page, list); - } - return false; -} - -static mi_decl_noinline bool mi_check_is_double_freex(const mi_page_t* page, const mi_block_t* block) { - // The decoded value is in the same page (or NULL). - // Walk the free lists to verify positively if it is already freed - if (mi_list_contains(page, page->free, block) || - mi_list_contains(page, page->local_free, block) || - mi_list_contains(page, mi_page_thread_free(page), block)) - { - _mi_error_message(EAGAIN, "double free detected of block %p with size %zu\n", block, mi_page_block_size(page)); - return true; - } - return false; -} - -#define mi_track_page(page,access) { size_t psize; void* pstart = _mi_page_start(_mi_page_segment(page),page,&psize); mi_track_mem_##access( pstart, psize); } - -static inline bool mi_check_is_double_free(const mi_page_t* page, const mi_block_t* block) { - bool is_double_free = false; - mi_block_t* n = mi_block_nextx(page, block, page->keys); // pretend it is freed, and get the decoded first field - if (((uintptr_t)n & (MI_INTPTR_SIZE-1))==0 && // quick check: aligned pointer? - (n==NULL || mi_is_in_same_page(block, n))) // quick check: in same page or NULL? - { - // Suspicious: decoded value a in block is in the same page (or NULL) -- maybe a double free? - // (continue in separate function to improve code generation) - is_double_free = mi_check_is_double_freex(page, block); - } - return is_double_free; -} -#else -static inline bool mi_check_is_double_free(const mi_page_t* page, const mi_block_t* block) { - MI_UNUSED(page); - MI_UNUSED(block); - return false; -} -#endif - -// --------------------------------------------------------------------------- -// Check for heap block overflow by setting up padding at the end of the block -// --------------------------------------------------------------------------- - -#if MI_PADDING // && !MI_TRACK_ENABLED -static bool mi_page_decode_padding(const mi_page_t* page, const mi_block_t* block, size_t* delta, size_t* bsize) { - *bsize = mi_page_usable_block_size(page); - const mi_padding_t* const padding = (mi_padding_t*)((uint8_t*)block + *bsize); - mi_track_mem_defined(padding,sizeof(mi_padding_t)); - *delta = padding->delta; - uint32_t canary = padding->canary; - uintptr_t keys[2]; - keys[0] = page->keys[0]; - keys[1] = page->keys[1]; - bool ok = ((uint32_t)mi_ptr_encode(page,block,keys) == canary && *delta <= *bsize); - mi_track_mem_noaccess(padding,sizeof(mi_padding_t)); - return ok; -} - -// Return the exact usable size of a block. -static size_t mi_page_usable_size_of(const mi_page_t* page, const mi_block_t* block) { - size_t bsize; - size_t delta; - bool ok = mi_page_decode_padding(page, block, &delta, &bsize); - mi_assert_internal(ok); mi_assert_internal(delta <= bsize); - return (ok ? bsize - delta : 0); -} - -// When a non-thread-local block is freed, it becomes part of the thread delayed free -// list that is freed later by the owning heap. If the exact usable size is too small to -// contain the pointer for the delayed list, then shrink the padding (by decreasing delta) -// so it will later not trigger an overflow error in `mi_free_block`. -void _mi_padding_shrink(const mi_page_t* page, const mi_block_t* block, const size_t min_size) { - size_t bsize; - size_t delta; - bool ok = mi_page_decode_padding(page, block, &delta, &bsize); - mi_assert_internal(ok); - if (!ok || (bsize - delta) >= min_size) return; // usually already enough space - mi_assert_internal(bsize >= min_size); - if (bsize < min_size) return; // should never happen - size_t new_delta = (bsize - min_size); - mi_assert_internal(new_delta < bsize); - mi_padding_t* padding = (mi_padding_t*)((uint8_t*)block + bsize); - mi_track_mem_defined(padding,sizeof(mi_padding_t)); - padding->delta = (uint32_t)new_delta; - mi_track_mem_noaccess(padding,sizeof(mi_padding_t)); -} -#else -static size_t mi_page_usable_size_of(const mi_page_t* page, const mi_block_t* block) { - MI_UNUSED(block); - return mi_page_usable_block_size(page); -} - -void _mi_padding_shrink(const mi_page_t* page, const mi_block_t* block, const size_t min_size) { - MI_UNUSED(page); - MI_UNUSED(block); - MI_UNUSED(min_size); -} -#endif - -#if MI_PADDING && MI_PADDING_CHECK - -static bool mi_verify_padding(const mi_page_t* page, const mi_block_t* block, size_t* size, size_t* wrong) { - size_t bsize; - size_t delta; - bool ok = mi_page_decode_padding(page, block, &delta, &bsize); - *size = *wrong = bsize; - if (!ok) return false; - mi_assert_internal(bsize >= delta); - *size = bsize - delta; - if (!mi_page_is_huge(page)) { - uint8_t* fill = (uint8_t*)block + bsize - delta; - const size_t maxpad = (delta > MI_MAX_ALIGN_SIZE ? MI_MAX_ALIGN_SIZE : delta); // check at most the first N padding bytes - mi_track_mem_defined(fill, maxpad); - for (size_t i = 0; i < maxpad; i++) { - if (fill[i] != MI_DEBUG_PADDING) { - *wrong = bsize - delta + i; - ok = false; - break; - } - } - mi_track_mem_noaccess(fill, maxpad); - } - return ok; -} - -static void mi_check_padding(const mi_page_t* page, const mi_block_t* block) { - size_t size; - size_t wrong; - if (!mi_verify_padding(page,block,&size,&wrong)) { - _mi_error_message(EFAULT, "buffer overflow in heap block %p of size %zu: write after %zu bytes\n", block, size, wrong ); - } -} - -#else - -static void mi_check_padding(const mi_page_t* page, const mi_block_t* block) { - MI_UNUSED(page); - MI_UNUSED(block); -} - -#endif - -// only maintain stats for smaller objects if requested -#if (MI_STAT>0) -static void mi_stat_free(const mi_page_t* page, const mi_block_t* block) { - #if (MI_STAT < 2) - MI_UNUSED(block); - #endif - mi_heap_t* const heap = mi_heap_get_default(); - const size_t bsize = mi_page_usable_block_size(page); - #if (MI_STAT>1) - const size_t usize = mi_page_usable_size_of(page, block); - mi_heap_stat_decrease(heap, malloc, usize); - #endif - if (bsize <= MI_MEDIUM_OBJ_SIZE_MAX) { - mi_heap_stat_decrease(heap, normal, bsize); - #if (MI_STAT > 1) - mi_heap_stat_decrease(heap, normal_bins[_mi_bin(bsize)], 1); - #endif - } - else if (bsize <= MI_LARGE_OBJ_SIZE_MAX) { - mi_heap_stat_decrease(heap, large, bsize); - } - else { - mi_heap_stat_decrease(heap, huge, bsize); - } -} -#else -static void mi_stat_free(const mi_page_t* page, const mi_block_t* block) { - MI_UNUSED(page); MI_UNUSED(block); -} -#endif - -#if MI_HUGE_PAGE_ABANDON -#if (MI_STAT>0) -// maintain stats for huge objects -static void mi_stat_huge_free(const mi_page_t* page) { - mi_heap_t* const heap = mi_heap_get_default(); - const size_t bsize = mi_page_block_size(page); // to match stats in `page.c:mi_page_huge_alloc` - if (bsize <= MI_LARGE_OBJ_SIZE_MAX) { - mi_heap_stat_decrease(heap, large, bsize); - } - else { - mi_heap_stat_decrease(heap, huge, bsize); - } -} -#else -static void mi_stat_huge_free(const mi_page_t* page) { - MI_UNUSED(page); -} -#endif -#endif - -// ------------------------------------------------------ -// Free -// ------------------------------------------------------ - -// multi-threaded free (or free in huge block if compiled with MI_HUGE_PAGE_ABANDON) -static mi_decl_noinline void _mi_free_block_mt(mi_page_t* page, mi_block_t* block) -{ - // The padding check may access the non-thread-owned page for the key values. - // that is safe as these are constant and the page won't be freed (as the block is not freed yet). - mi_check_padding(page, block); - _mi_padding_shrink(page, block, sizeof(mi_block_t)); // for small size, ensure we can fit the delayed thread pointers without triggering overflow detection - - // huge page segments are always abandoned and can be freed immediately - mi_segment_t* segment = _mi_page_segment(page); - if (segment->kind == MI_SEGMENT_HUGE) { - #if MI_HUGE_PAGE_ABANDON - // huge page segments are always abandoned and can be freed immediately - mi_stat_huge_free(page); - _mi_segment_huge_page_free(segment, page, block); - return; - #else - // huge pages are special as they occupy the entire segment - // as these are large we reset the memory occupied by the page so it is available to other threads - // (as the owning thread needs to actually free the memory later). - _mi_segment_huge_page_reset(segment, page, block); - #endif - } - - #if (MI_DEBUG>0) && !MI_TRACK_ENABLED && !MI_TSAN // note: when tracking, cannot use mi_usable_size with multi-threading - if (segment->kind != MI_SEGMENT_HUGE) { // not for huge segments as we just reset the content - mi_debug_fill(page, block, MI_DEBUG_FREED, mi_usable_size(block)); - } - #endif - - // Try to put the block on either the page-local thread free list, or the heap delayed free list. - mi_thread_free_t tfreex; - bool use_delayed; - mi_thread_free_t tfree = mi_atomic_load_relaxed(&page->xthread_free); - do { - use_delayed = (mi_tf_delayed(tfree) == MI_USE_DELAYED_FREE); - if mi_unlikely(use_delayed) { - // unlikely: this only happens on the first concurrent free in a page that is in the full list - tfreex = mi_tf_set_delayed(tfree,MI_DELAYED_FREEING); - } - else { - // usual: directly add to page thread_free list - mi_block_set_next(page, block, mi_tf_block(tfree)); - tfreex = mi_tf_set_block(tfree,block); - } - } while (!mi_atomic_cas_weak_release(&page->xthread_free, &tfree, tfreex)); - - if mi_unlikely(use_delayed) { - // racy read on `heap`, but ok because MI_DELAYED_FREEING is set (see `mi_heap_delete` and `mi_heap_collect_abandon`) - mi_heap_t* const heap = (mi_heap_t*)(mi_atomic_load_acquire(&page->xheap)); //mi_page_heap(page); - mi_assert_internal(heap != NULL); - if (heap != NULL) { - // add to the delayed free list of this heap. (do this atomically as the lock only protects heap memory validity) - mi_block_t* dfree = mi_atomic_load_ptr_relaxed(mi_block_t, &heap->thread_delayed_free); - do { - mi_block_set_nextx(heap,block,dfree, heap->keys); - } while (!mi_atomic_cas_ptr_weak_release(mi_block_t,&heap->thread_delayed_free, &dfree, block)); - } - - // and reset the MI_DELAYED_FREEING flag - tfree = mi_atomic_load_relaxed(&page->xthread_free); - do { - tfreex = tfree; - mi_assert_internal(mi_tf_delayed(tfree) == MI_DELAYED_FREEING); - tfreex = mi_tf_set_delayed(tfree,MI_NO_DELAYED_FREE); - } while (!mi_atomic_cas_weak_release(&page->xthread_free, &tfree, tfreex)); - } -} - -// regular free -static inline void _mi_free_block(mi_page_t* page, bool local, mi_block_t* block) -{ - // and push it on the free list - //const size_t bsize = mi_page_block_size(page); - if mi_likely(local) { - // owning thread can free a block directly - if mi_unlikely(mi_check_is_double_free(page, block)) return; - mi_check_padding(page, block); - #if (MI_DEBUG>0) && !MI_TRACK_ENABLED && !MI_TSAN - if (!mi_page_is_huge(page)) { // huge page content may be already decommitted - mi_debug_fill(page, block, MI_DEBUG_FREED, mi_page_block_size(page)); - } - #endif - mi_block_set_next(page, block, page->local_free); - page->local_free = block; - page->used--; - if mi_unlikely(mi_page_all_free(page)) { - _mi_page_retire(page); - } - else if mi_unlikely(mi_page_is_in_full(page)) { - _mi_page_unfull(page); - } - } - else { - _mi_free_block_mt(page,block); - } -} - - -// Adjust a block that was allocated aligned, to the actual start of the block in the page. -mi_block_t* _mi_page_ptr_unalign(const mi_segment_t* segment, const mi_page_t* page, const void* p) { - mi_assert_internal(page!=NULL && p!=NULL); - const size_t diff = (uint8_t*)p - _mi_page_start(segment, page, NULL); - const size_t adjust = (diff % mi_page_block_size(page)); - return (mi_block_t*)((uintptr_t)p - adjust); -} - - -void mi_decl_noinline _mi_free_generic(const mi_segment_t* segment, mi_page_t* page, bool is_local, void* p) mi_attr_noexcept { - mi_block_t* const block = (mi_page_has_aligned(page) ? _mi_page_ptr_unalign(segment, page, p) : (mi_block_t*)p); - mi_stat_free(page, block); // stat_free may access the padding - mi_track_free_size(block, mi_page_usable_size_of(page,block)); - _mi_free_block(page, is_local, block); -} - -// Get the segment data belonging to a pointer -// This is just a single `and` in assembly but does further checks in debug mode -// (and secure mode) if this was a valid pointer. -static inline mi_segment_t* mi_checked_ptr_segment(const void* p, const char* msg) -{ - MI_UNUSED(msg); - mi_assert(p != NULL); - -#if (MI_DEBUG>0) - if mi_unlikely(((uintptr_t)p & (MI_INTPTR_SIZE - 1)) != 0) { - _mi_error_message(EINVAL, "%s: invalid (unaligned) pointer: %p\n", msg, p); - return NULL; - } -#endif - - mi_segment_t* const segment = _mi_ptr_segment(p); - mi_assert_internal(segment != NULL); - -#if 0 && (MI_DEBUG>0) - if mi_unlikely(!mi_is_in_heap_region(p)) { - #if (MI_INTPTR_SIZE == 8 && defined(__linux__)) - if (((uintptr_t)p >> 40) != 0x7F) { // linux tends to align large blocks above 0x7F000000000 (issue #640) - #else - { - #endif - _mi_warning_message("%s: pointer might not point to a valid heap region: %p\n" - "(this may still be a valid very large allocation (over 64MiB))\n", msg, p); - if mi_likely(_mi_ptr_cookie(segment) == segment->cookie) { - _mi_warning_message("(yes, the previous pointer %p was valid after all)\n", p); - } - } - } -#endif -#if (MI_DEBUG>0 || MI_SECURE>=4) - if mi_unlikely(_mi_ptr_cookie(segment) != segment->cookie) { - _mi_error_message(EINVAL, "%s: pointer does not point to a valid heap space: %p\n", msg, p); - return NULL; - } -#endif - - return segment; -} - -// Free a block -// fast path written carefully to prevent spilling on the stack -void mi_free(void* p) mi_attr_noexcept -{ - if mi_unlikely(p == NULL) return; - mi_segment_t* const segment = mi_checked_ptr_segment(p,"mi_free"); - const bool is_local= (_mi_prim_thread_id() == mi_atomic_load_relaxed(&segment->thread_id)); - mi_page_t* const page = _mi_segment_page_of(segment, p); - - if mi_likely(is_local) { // thread-local free? - if mi_likely(page->flags.full_aligned == 0) // and it is not a full page (full pages need to move from the full bin), nor has aligned blocks (aligned blocks need to be unaligned) - { - mi_block_t* const block = (mi_block_t*)p; - if mi_unlikely(mi_check_is_double_free(page, block)) return; - mi_check_padding(page, block); - mi_stat_free(page, block); - #if (MI_DEBUG>0) && !MI_TRACK_ENABLED && !MI_TSAN - mi_debug_fill(page, block, MI_DEBUG_FREED, mi_page_block_size(page)); - #endif - mi_track_free_size(p, mi_page_usable_size_of(page,block)); // faster then mi_usable_size as we already know the page and that p is unaligned - mi_block_set_next(page, block, page->local_free); - page->local_free = block; - if mi_unlikely(--page->used == 0) { // using this expression generates better code than: page->used--; if (mi_page_all_free(page)) - _mi_page_retire(page); - } - } - else { - // page is full or contains (inner) aligned blocks; use generic path - _mi_free_generic(segment, page, true, p); - } - } - else { - // not thread-local; use generic path - _mi_free_generic(segment, page, false, p); - } -} - -// return true if successful -bool _mi_free_delayed_block(mi_block_t* block) { - // get segment and page - const mi_segment_t* const segment = _mi_ptr_segment(block); - mi_assert_internal(_mi_ptr_cookie(segment) == segment->cookie); -#ifndef Py_GIL_DISABLED - // The GC traverses heaps of other threads, which can trigger this assert. - mi_assert_internal(_mi_thread_id() == segment->thread_id); -#endif - mi_page_t* const page = _mi_segment_page_of(segment, block); - - // Clear the no-delayed flag so delayed freeing is used again for this page. - // This must be done before collecting the free lists on this page -- otherwise - // some blocks may end up in the page `thread_free` list with no blocks in the - // heap `thread_delayed_free` list which may cause the page to be never freed! - // (it would only be freed if we happen to scan it in `mi_page_queue_find_free_ex`) - if (!_mi_page_try_use_delayed_free(page, MI_USE_DELAYED_FREE, false /* dont overwrite never delayed */)) { - return false; - } - - // collect all other non-local frees to ensure up-to-date `used` count - _mi_page_free_collect(page, false); - - // and free the block (possibly freeing the page as well since used is updated) - _mi_free_block(page, true, block); - return true; -} - -// Bytes available in a block -mi_decl_noinline static size_t mi_page_usable_aligned_size_of(const mi_segment_t* segment, const mi_page_t* page, const void* p) mi_attr_noexcept { - const mi_block_t* block = _mi_page_ptr_unalign(segment, page, p); - const size_t size = mi_page_usable_size_of(page, block); - const ptrdiff_t adjust = (uint8_t*)p - (uint8_t*)block; - mi_assert_internal(adjust >= 0 && (size_t)adjust <= size); - return (size - adjust); -} - -static inline size_t _mi_usable_size(const void* p, const char* msg) mi_attr_noexcept { - if (p == NULL) return 0; - const mi_segment_t* const segment = mi_checked_ptr_segment(p, msg); - const mi_page_t* const page = _mi_segment_page_of(segment, p); - if mi_likely(!mi_page_has_aligned(page)) { - const mi_block_t* block = (const mi_block_t*)p; - return mi_page_usable_size_of(page, block); - } - else { - // split out to separate routine for improved code generation - return mi_page_usable_aligned_size_of(segment, page, p); - } -} - -mi_decl_nodiscard size_t mi_usable_size(const void* p) mi_attr_noexcept { - return _mi_usable_size(p, "mi_usable_size"); -} - - -// ------------------------------------------------------ -// Allocation extensions -// ------------------------------------------------------ - -void mi_free_size(void* p, size_t size) mi_attr_noexcept { - MI_UNUSED_RELEASE(size); - mi_assert(p == NULL || size <= _mi_usable_size(p,"mi_free_size")); - mi_free(p); -} - -void mi_free_size_aligned(void* p, size_t size, size_t alignment) mi_attr_noexcept { - MI_UNUSED_RELEASE(alignment); - mi_assert(((uintptr_t)p % alignment) == 0); - mi_free_size(p,size); -} - -void mi_free_aligned(void* p, size_t alignment) mi_attr_noexcept { - MI_UNUSED_RELEASE(alignment); - mi_assert(((uintptr_t)p % alignment) == 0); - mi_free(p); -} - -mi_decl_nodiscard extern inline mi_decl_restrict void* mi_heap_calloc(mi_heap_t* heap, size_t count, size_t size) mi_attr_noexcept { - size_t total; - if (mi_count_size_overflow(count,size,&total)) return NULL; - return mi_heap_zalloc(heap,total); -} - -mi_decl_nodiscard mi_decl_restrict void* mi_calloc(size_t count, size_t size) mi_attr_noexcept { - return mi_heap_calloc(mi_prim_get_default_heap(),count,size); -} - -// Uninitialized `calloc` -mi_decl_nodiscard extern mi_decl_restrict void* mi_heap_mallocn(mi_heap_t* heap, size_t count, size_t size) mi_attr_noexcept { - size_t total; - if (mi_count_size_overflow(count, size, &total)) return NULL; - return mi_heap_malloc(heap, total); -} - -mi_decl_nodiscard mi_decl_restrict void* mi_mallocn(size_t count, size_t size) mi_attr_noexcept { - return mi_heap_mallocn(mi_prim_get_default_heap(),count,size); -} - -// Expand (or shrink) in place (or fail) -void* mi_expand(void* p, size_t newsize) mi_attr_noexcept { - #if MI_PADDING - // we do not shrink/expand with padding enabled - MI_UNUSED(p); MI_UNUSED(newsize); - return NULL; - #else - if (p == NULL) return NULL; - const size_t size = _mi_usable_size(p,"mi_expand"); - if (newsize > size) return NULL; - return p; // it fits - #endif -} - -void* _mi_heap_realloc_zero(mi_heap_t* heap, void* p, size_t newsize, bool zero) mi_attr_noexcept { - // if p == NULL then behave as malloc. - // else if size == 0 then reallocate to a zero-sized block (and don't return NULL, just as mi_malloc(0)). - // (this means that returning NULL always indicates an error, and `p` will not have been freed in that case.) - const size_t size = _mi_usable_size(p,"mi_realloc"); // also works if p == NULL (with size 0) - if mi_unlikely(newsize <= size && newsize >= (size / 2) && newsize > 0) { // note: newsize must be > 0 or otherwise we return NULL for realloc(NULL,0) - mi_assert_internal(p!=NULL); - // todo: do not track as the usable size is still the same in the free; adjust potential padding? - // mi_track_resize(p,size,newsize) - // if (newsize < size) { mi_track_mem_noaccess((uint8_t*)p + newsize, size - newsize); } - return p; // reallocation still fits and not more than 50% waste - } - void* newp = mi_heap_malloc(heap,newsize); - if mi_likely(newp != NULL) { - if (zero && newsize > size) { - // also set last word in the previous allocation to zero to ensure any padding is zero-initialized - const size_t start = (size >= sizeof(intptr_t) ? size - sizeof(intptr_t) : 0); - _mi_memzero((uint8_t*)newp + start, newsize - start); - } - else if (newsize == 0) { - ((uint8_t*)newp)[0] = 0; // work around for applications that expect zero-reallocation to be zero initialized (issue #725) - } - if mi_likely(p != NULL) { - const size_t copysize = (newsize > size ? size : newsize); - mi_track_mem_defined(p,copysize); // _mi_useable_size may be too large for byte precise memory tracking.. - _mi_memcpy(newp, p, copysize); - mi_free(p); // only free the original pointer if successful - } - } - return newp; -} - -mi_decl_nodiscard void* mi_heap_realloc(mi_heap_t* heap, void* p, size_t newsize) mi_attr_noexcept { - return _mi_heap_realloc_zero(heap, p, newsize, false); -} - -mi_decl_nodiscard void* mi_heap_reallocn(mi_heap_t* heap, void* p, size_t count, size_t size) mi_attr_noexcept { - size_t total; - if (mi_count_size_overflow(count, size, &total)) return NULL; - return mi_heap_realloc(heap, p, total); -} - - -// Reallocate but free `p` on errors -mi_decl_nodiscard void* mi_heap_reallocf(mi_heap_t* heap, void* p, size_t newsize) mi_attr_noexcept { - void* newp = mi_heap_realloc(heap, p, newsize); - if (newp==NULL && p!=NULL) mi_free(p); - return newp; -} - -mi_decl_nodiscard void* mi_heap_rezalloc(mi_heap_t* heap, void* p, size_t newsize) mi_attr_noexcept { - return _mi_heap_realloc_zero(heap, p, newsize, true); -} - -mi_decl_nodiscard void* mi_heap_recalloc(mi_heap_t* heap, void* p, size_t count, size_t size) mi_attr_noexcept { - size_t total; - if (mi_count_size_overflow(count, size, &total)) return NULL; - return mi_heap_rezalloc(heap, p, total); -} - - -mi_decl_nodiscard void* mi_realloc(void* p, size_t newsize) mi_attr_noexcept { - return mi_heap_realloc(mi_prim_get_default_heap(),p,newsize); -} - -mi_decl_nodiscard void* mi_reallocn(void* p, size_t count, size_t size) mi_attr_noexcept { - return mi_heap_reallocn(mi_prim_get_default_heap(),p,count,size); -} - -// Reallocate but free `p` on errors -mi_decl_nodiscard void* mi_reallocf(void* p, size_t newsize) mi_attr_noexcept { - return mi_heap_reallocf(mi_prim_get_default_heap(),p,newsize); -} - -mi_decl_nodiscard void* mi_rezalloc(void* p, size_t newsize) mi_attr_noexcept { - return mi_heap_rezalloc(mi_prim_get_default_heap(), p, newsize); -} - -mi_decl_nodiscard void* mi_recalloc(void* p, size_t count, size_t size) mi_attr_noexcept { - return mi_heap_recalloc(mi_prim_get_default_heap(), p, count, size); -} - - - -// ------------------------------------------------------ -// strdup, strndup, and realpath -// ------------------------------------------------------ - -// `strdup` using mi_malloc -mi_decl_nodiscard mi_decl_restrict char* mi_heap_strdup(mi_heap_t* heap, const char* s) mi_attr_noexcept { - if (s == NULL) return NULL; - size_t n = strlen(s); - char* t = (char*)mi_heap_malloc(heap,n+1); - if (t == NULL) return NULL; - _mi_memcpy(t, s, n); - t[n] = 0; - return t; -} - -mi_decl_nodiscard mi_decl_restrict char* mi_strdup(const char* s) mi_attr_noexcept { - return mi_heap_strdup(mi_prim_get_default_heap(), s); -} - -// `strndup` using mi_malloc -mi_decl_nodiscard mi_decl_restrict char* mi_heap_strndup(mi_heap_t* heap, const char* s, size_t n) mi_attr_noexcept { - if (s == NULL) return NULL; - const char* end = (const char*)memchr(s, 0, n); // find end of string in the first `n` characters (returns NULL if not found) - const size_t m = (end != NULL ? (size_t)(end - s) : n); // `m` is the minimum of `n` or the end-of-string - mi_assert_internal(m <= n); - char* t = (char*)mi_heap_malloc(heap, m+1); - if (t == NULL) return NULL; - _mi_memcpy(t, s, m); - t[m] = 0; - return t; -} - -mi_decl_nodiscard mi_decl_restrict char* mi_strndup(const char* s, size_t n) mi_attr_noexcept { - return mi_heap_strndup(mi_prim_get_default_heap(),s,n); -} - -#ifndef __wasi__ -// `realpath` using mi_malloc -#ifdef _WIN32 -#ifndef PATH_MAX -#define PATH_MAX MAX_PATH -#endif -#include -mi_decl_nodiscard mi_decl_restrict char* mi_heap_realpath(mi_heap_t* heap, const char* fname, char* resolved_name) mi_attr_noexcept { - // todo: use GetFullPathNameW to allow longer file names - char buf[PATH_MAX]; - DWORD res = GetFullPathNameA(fname, PATH_MAX, (resolved_name == NULL ? buf : resolved_name), NULL); - if (res == 0) { - errno = GetLastError(); return NULL; - } - else if (res > PATH_MAX) { - errno = EINVAL; return NULL; - } - else if (resolved_name != NULL) { - return resolved_name; - } - else { - return mi_heap_strndup(heap, buf, PATH_MAX); - } -} -#else -/* -#include // pathconf -static size_t mi_path_max(void) { - static size_t path_max = 0; - if (path_max <= 0) { - long m = pathconf("/",_PC_PATH_MAX); - if (m <= 0) path_max = 4096; // guess - else if (m < 256) path_max = 256; // at least 256 - else path_max = m; - } - return path_max; -} -*/ -char* mi_heap_realpath(mi_heap_t* heap, const char* fname, char* resolved_name) mi_attr_noexcept { - if (resolved_name != NULL) { - return realpath(fname,resolved_name); - } - else { - char* rname = realpath(fname, NULL); - if (rname == NULL) return NULL; - char* result = mi_heap_strdup(heap, rname); - free(rname); // use regular free! (which may be redirected to our free but that's ok) - return result; - } - /* - const size_t n = mi_path_max(); - char* buf = (char*)mi_malloc(n+1); - if (buf == NULL) { - errno = ENOMEM; - return NULL; - } - char* rname = realpath(fname,buf); - char* result = mi_heap_strndup(heap,rname,n); // ok if `rname==NULL` - mi_free(buf); - return result; - } - */ -} -#endif - -mi_decl_nodiscard mi_decl_restrict char* mi_realpath(const char* fname, char* resolved_name) mi_attr_noexcept { - return mi_heap_realpath(mi_prim_get_default_heap(),fname,resolved_name); -} -#endif - -/*------------------------------------------------------- -C++ new and new_aligned -The standard requires calling into `get_new_handler` and -throwing the bad_alloc exception on failure. If we compile -with a C++ compiler we can implement this precisely. If we -use a C compiler we cannot throw a `bad_alloc` exception -but we call `exit` instead (i.e. not returning). --------------------------------------------------------*/ - -#ifdef __cplusplus -#include -static bool mi_try_new_handler(bool nothrow) { - #if defined(_MSC_VER) || (__cplusplus >= 201103L) - std::new_handler h = std::get_new_handler(); - #else - std::new_handler h = std::set_new_handler(); - std::set_new_handler(h); - #endif - if (h==NULL) { - _mi_error_message(ENOMEM, "out of memory in 'new'"); - if (!nothrow) { - throw std::bad_alloc(); - } - return false; - } - else { - h(); - return true; - } -} -#else -typedef void (*std_new_handler_t)(void); - -#if (defined(__GNUC__) || (defined(__clang__) && !defined(_MSC_VER))) // exclude clang-cl, see issue #631 -std_new_handler_t __attribute__((weak)) _ZSt15get_new_handlerv(void) { - return NULL; -} -static std_new_handler_t mi_get_new_handler(void) { - return _ZSt15get_new_handlerv(); -} -#else -// note: on windows we could dynamically link to `?get_new_handler@std@@YAP6AXXZXZ`. -static std_new_handler_t mi_get_new_handler() { - return NULL; -} -#endif - -static bool mi_try_new_handler(bool nothrow) { - std_new_handler_t h = mi_get_new_handler(); - if (h==NULL) { - _mi_error_message(ENOMEM, "out of memory in 'new'"); - if (!nothrow) { - abort(); // cannot throw in plain C, use abort - } - return false; - } - else { - h(); - return true; - } -} -#endif - -mi_decl_export mi_decl_noinline void* mi_heap_try_new(mi_heap_t* heap, size_t size, bool nothrow ) { - void* p = NULL; - while(p == NULL && mi_try_new_handler(nothrow)) { - p = mi_heap_malloc(heap,size); - } - return p; -} - -static mi_decl_noinline void* mi_try_new(size_t size, bool nothrow) { - return mi_heap_try_new(mi_prim_get_default_heap(), size, nothrow); -} - - -mi_decl_nodiscard mi_decl_restrict void* mi_heap_alloc_new(mi_heap_t* heap, size_t size) { - void* p = mi_heap_malloc(heap,size); - if mi_unlikely(p == NULL) return mi_heap_try_new(heap, size, false); - return p; -} - -mi_decl_nodiscard mi_decl_restrict void* mi_new(size_t size) { - return mi_heap_alloc_new(mi_prim_get_default_heap(), size); -} - - -mi_decl_nodiscard mi_decl_restrict void* mi_heap_alloc_new_n(mi_heap_t* heap, size_t count, size_t size) { - size_t total; - if mi_unlikely(mi_count_size_overflow(count, size, &total)) { - mi_try_new_handler(false); // on overflow we invoke the try_new_handler once to potentially throw std::bad_alloc - return NULL; - } - else { - return mi_heap_alloc_new(heap,total); - } -} - -mi_decl_nodiscard mi_decl_restrict void* mi_new_n(size_t count, size_t size) { - return mi_heap_alloc_new_n(mi_prim_get_default_heap(), size, count); -} - - -mi_decl_nodiscard mi_decl_restrict void* mi_new_nothrow(size_t size) mi_attr_noexcept { - void* p = mi_malloc(size); - if mi_unlikely(p == NULL) return mi_try_new(size, true); - return p; -} - -mi_decl_nodiscard mi_decl_restrict void* mi_new_aligned(size_t size, size_t alignment) { - void* p; - do { - p = mi_malloc_aligned(size, alignment); - } - while(p == NULL && mi_try_new_handler(false)); - return p; -} - -mi_decl_nodiscard mi_decl_restrict void* mi_new_aligned_nothrow(size_t size, size_t alignment) mi_attr_noexcept { - void* p; - do { - p = mi_malloc_aligned(size, alignment); - } - while(p == NULL && mi_try_new_handler(true)); - return p; -} - -mi_decl_nodiscard void* mi_new_realloc(void* p, size_t newsize) { - void* q; - do { - q = mi_realloc(p, newsize); - } while (q == NULL && mi_try_new_handler(false)); - return q; -} - -mi_decl_nodiscard void* mi_new_reallocn(void* p, size_t newcount, size_t size) { - size_t total; - if mi_unlikely(mi_count_size_overflow(newcount, size, &total)) { - mi_try_new_handler(false); // on overflow we invoke the try_new_handler once to potentially throw std::bad_alloc - return NULL; - } - else { - return mi_new_realloc(p, total); - } -} - -// ------------------------------------------------------ -// ensure explicit external inline definitions are emitted! -// ------------------------------------------------------ - -#ifdef __cplusplus -void* _mi_externs[] = { - (void*)&_mi_page_malloc, - (void*)&_mi_heap_malloc_zero, - (void*)&_mi_heap_malloc_zero_ex, - (void*)&mi_malloc, - (void*)&mi_malloc_small, - (void*)&mi_zalloc_small, - (void*)&mi_heap_malloc, - (void*)&mi_heap_zalloc, - (void*)&mi_heap_malloc_small, - // (void*)&mi_heap_alloc_new, - // (void*)&mi_heap_alloc_new_n -}; -#endif diff --git a/Objects/mimalloc/alloc/alloc-aligned.c b/Objects/mimalloc/alloc/alloc-aligned.c new file mode 100644 index 000000000000000..7895bee0851b0a5 --- /dev/null +++ b/Objects/mimalloc/alloc/alloc-aligned.c @@ -0,0 +1,453 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +#include "mimalloc.h" +#include "mimalloc/types.h" +#include "mimalloc/prim/prim-tls.h" // _mi_theap_default +#include "mimalloc/alloc/alloc.h" +#include "mimalloc/alloc/free.h" +#include "mimalloc/alloc/page.h" +#include "mimalloc/profile/sample-guarded.h" + + +// ------------------------------------------------------ +// Aligned Allocation +// ------------------------------------------------------ + +static bool mi_malloc_is_naturally_aligned( size_t size, size_t alignment ) { + // certain blocks are always allocated at a certain natural alignment. + // (see `alloc/page.h:mi_natural_alignment_of_size`, asserted in `arena/arena-page.c:mi_arenas_page_alloc_fresh`). + mi_assert_internal(mi_alignment_is_valid(alignment)); + if (alignment > size) return false; + const size_t bsize = mi_good_size(size); + const bool ok = (alignment <= mi_natural_alignment_of_size(bsize)); + if (ok) { mi_assert_internal((bsize & (alignment-1)) == 0); } // since the natural alignment always divides `bsize` + return ok; +} + +#if 0 +static mi_decl_noinline mi_decl_restrict void* mi_theap_malloc_guarded_aligned(mi_theap_t* theap, size_t size, size_t alignment, bool zero, mi_page_t** ppage) mi_attr_noexcept { + // use over allocation for guarded blocksl + #if MI_THEAP_INITASNULL + if mi_unlikely(theap==NULL) { theap = _mi_theap_empty_get(); } + #endif + mi_assert_internal(alignment > 0 && alignment < MI_PAGE_MAX_OVERALLOC_ALIGN); + if mi_unlikely(alignment >= MI_PAGE_MAX_OVERALLOC_ALIGN || size > (MI_MAX_ALLOC_SIZE - MI_PADDING_SIZE - alignment)) { + _mi_error_message(EOVERFLOW, "(guarded) aligned allocation request is too large (size %zu, alignment %zu)\n", size, alignment); + return NULL; + } + const size_t oversize = size + alignment - 1; + void* const base = _mi_theap_malloc_guarded(theap, oversize, zero, ppage); + if (base==NULL) return NULL; + void* const p = mi_align_up_ptr(base, alignment); + mi_track_align(base, p, (uint8_t*)p - (uint8_t*)base, size); + mi_assert_internal(mi_usable_size(p) >= size); + mi_assert_internal(mi_is_aligned(p, alignment)); + return p; +} +#endif + + +// Fallback aligned allocation that over-allocates -- split out for better codegen +static mi_decl_noinline void* mi_theap_malloc_zero_aligned_at_overalloc(mi_theap_t* const theap, const size_t size, const size_t alignment, const size_t offset, const bool zero, mi_page_t** ppage) mi_attr_noexcept +{ + mi_assert_internal(size <= (MI_MAX_ALLOC_SIZE - MI_PADDING_SIZE)); + mi_assert_internal(mi_alignment_is_valid(alignment)); + + void* p; + size_t oversize; + mi_page_t* page; + if mi_unlikely(alignment > MI_PAGE_MAX_OVERALLOC_ALIGN) { + // use OS allocation for large alignments and allocate inside a singleton page (not in an arena) + // This can support alignments >= MI_PAGE_ALIGN by ensuring the object can be aligned + // in the first (and single) page such that the page info is `MI_PAGE_ALIGN` bytes before it (and can be found in the _mi_page_map). + if mi_unlikely(offset != 0) { + // todo: cannot support offset alignment for very large alignments yet + _mi_error_message(EOVERFLOW, "aligned allocation with a large alignment cannot be used with an alignment offset (size %zu, alignment %zu, offset %zu)\n", size, alignment, offset); + return NULL; + } + oversize = (size <= MI_SMALL_SIZE_MAX ? MI_SMALL_SIZE_MAX + 1 /* ensure we use generic malloc path */ : size); + // note: no guarded as alignment > 0 + p = _mi_theap_malloc_zero(theap, oversize, zero, alignment, &page); // the page block size should be large enough to align in the single huge page block + if (p == NULL) return NULL; + } + else { + // otherwise over-allocate + mi_assert_internal(size <= (MI_MAX_ALLOC_SIZE - MI_PADDING_SIZE) && alignment <= MI_PAGE_MAX_OVERALLOC_ALIGN); + mi_assert_internal(size < SIZE_MAX - alignment); // `oversize` cannot overflow + oversize = (size < MI_MAX_ALIGN_SIZE ? MI_MAX_ALIGN_SIZE : size) + alignment - 1; // adjust for size <= 16; with size 0 and alignment 64k, we would allocate a 64k block and pointing just beyond that. + p = _mi_theap_malloc_zero(theap, oversize, zero, 0, &page); + if (p == NULL) return NULL; + } + mi_assert_internal(page == mi_ptr_page(p)); + if (ppage!=NULL) { *ppage = page; } + + // .. and align within the allocation + const uintptr_t align_mask = alignment - 1; // for any x, `(x & align_mask) == (x % alignment)` + const uintptr_t poffset = ((uintptr_t)p + offset) & align_mask; + const uintptr_t adjust = (poffset == 0 ? 0 : alignment - poffset); + mi_assert_internal(adjust < alignment); + void* aligned_p = (void*)((uintptr_t)p + adjust); + + // note: after the above allocation, the page may be abandoned now (as it became full, see `alloc/alloc-generic.c:_mi_malloc_generic`) + // and we no longer own it. We should be careful to only read constant fields in the page, + // or use safe atomic access as in `mi_page_set_has_interior_pointers`. + // (we can access the page though since the just allocated pointer keeps it alive) + if (aligned_p != p) { + mi_page_set_has_interior_pointers(page, true); + + #if MI_GUARDED || MI_PROFILE + // set tag to aligned so mi_usable_size works with guard pages + // note: we could have gotten a profiled or guarded block but that is still ok + // and in such case the MI_BLOCK_TAG_ALIGNED is just not at the start of the real heap block + if (adjust >= sizeof(mi_block_t)) { + mi_block_t* const block = (mi_block_t*)p; + block->next = MI_BLOCK_TAG_ALIGNED; + } + #endif + mi_block_t* const base = _mi_page_ptr_unalign(page, p); + _mi_padding_shrink(page, base, adjust + size); + } + // todo: expand padding if overallocated ? + + mi_assert_internal(mi_page_usable_block_size(page) >= adjust + size); + mi_assert_internal(((uintptr_t)aligned_p + offset) % alignment == 0); + mi_assert_internal(mi_usable_size(aligned_p)>=size); + mi_assert_internal(mi_usable_size(p) == mi_usable_size(aligned_p)+adjust); + #if MI_DEBUG > 1 + mi_page_t* const apage = mi_ptr_page(aligned_p); + mi_block_t* unalign_p = _mi_page_ptr_unalign(apage, aligned_p); + mi_assert_internal(p == (void*)unalign_p || mi_block_ptr_is_sampled(unalign_p, aligned_p)); + #endif + + if (p != aligned_p) { + mi_track_align(p,aligned_p,adjust,mi_usable_size(aligned_p)); + #if MI_GUARDED || MI_PROFILE + mi_track_mem_defined(p, sizeof(mi_block_t)); + #endif + } + return aligned_p; +} + +// Generic primitive aligned allocation -- split out for better codegen +static mi_decl_noinline void* mi_theap_malloc_zero_aligned_at_generic(mi_theap_t* const theap, const size_t size, const size_t alignment, const size_t offset, const bool zero, mi_page_t** ppage) mi_attr_noexcept +{ + mi_assert_internal(mi_alignment_is_valid(alignment)); + // we don't allocate more than MI_MAX_ALLOC_SIZE (see ) + if mi_unlikely(size > (MI_MAX_ALLOC_SIZE - MI_PADDING_SIZE)) { + _mi_error_message(EINVAL, "aligned allocation request is too large (size %zu, alignment %zu)\n", size, alignment); + return NULL; + } + + // use regular allocation if it is guaranteed to fit the alignment constraints. + // this is important to try as the fast path in `mi_theap_malloc_zero_aligned` only works when there exist + // a page with the right block size, and if we always use the over-alloc fallback that would never happen. + #if MI_THEAP_INITASNULL + if mi_likely(theap!=NULL) + #endif + { + #if MI_SAMPLE // only try if we would not take a sample + if mi_likely(!mi_theap_should_sample(theap,size)) + #endif + { + if (offset == 0 && mi_malloc_is_naturally_aligned(size,alignment)) + { + mi_page_t* page = NULL; + void* p = _mi_theap_malloc_zero(theap, size, zero, 0,&page); + if (ppage!=NULL) { *ppage = page; } + const bool is_aligned_or_null = (((uintptr_t)p) & (alignment-1))==0; + if mi_likely(is_aligned_or_null) { + return p; + } + else { + // this should never happen if the `mi_malloc_is_naturally_aligned` check is correct.. + // (but it can happen if this allocation just so happens to be sampled but we checked for that) + mi_assert_internal(false); // p==NULL || mi_block_ptr_is_profiled_or_guarded(_mi_page_ptr_unalign(page,p),p)); + mi_free(p); + } + } + } + } + + // fall back to over-allocation + return mi_theap_malloc_zero_aligned_at_overalloc(theap,size,alignment,offset,zero,ppage); +} + + +static mi_decl_cold mi_decl_noinline void* mi_error_bad_alignment(size_t size, size_t alignment, size_t offset) { + _mi_error_message(EINVAL, "aligned allocation requires the alignment to be a power-of-two (size %zu, alignment %zu, offset %zu)\n", size, alignment, offset); + return NULL; +} + +// Primitive aligned allocation +static inline void* mi_theap_malloc_zero_aligned_at(mi_theap_t* const theap, size_t size, const size_t alignment, const size_t offset, const bool zero, mi_page_t** ppage) mi_attr_noexcept +{ + // note: we don't require `size > offset`, we just guarantee that the address at offset is aligned regardless of the allocated size. + if mi_unlikely(!mi_alignment_is_valid(alignment)) { // require power-of-two and multiple of void* (see ) + return mi_error_bad_alignment(size, alignment, offset); + } + + // Use a naturally aligned size class instead of over-allocating and adjusting the pointer. + // Keep the requested size when padding is enabled for byte-precise overflow checks. + if (MI_PADDING_SIZE == 0 && offset == 0 && size < alignment && alignment <= MI_PAGE_MAX_START_BLOCK_ALIGN2) { + size = alignment; + } + + // try first if there happens to be a small block available with just the right alignment + // since most small power-of-2 blocks (under MI_PAGE_MAX_BLOCK_START_ALIGN2) are already + // naturally aligned this can be often the case. + if mi_likely(size <= MI_SMALL_SIZE_MAX && alignment <= size) + { + #if MI_THEAP_INITASNULL + if mi_likely(theap!=NULL) + #endif + { + #if MI_SAMPLE // check if we shouldn't take a sample + if mi_likely(!mi_theap_should_sample(theap,size)) + #endif + { + const uintptr_t align_mask = alignment-1; // for any x, `(x & align_mask) == (x % alignment)` + const size_t padsize = size + MI_PADDING_SIZE; + mi_page_t* page = mi_theap_get_free_small_page(theap, padsize, false); + if mi_likely(page->free != NULL) { + const bool is_aligned = (((uintptr_t)page->free + offset) & align_mask)==0; + if mi_likely(is_aligned) + { + if (ppage!=NULL) { *ppage = page; } + void* p = _mi_page_malloc_zero(theap, page, padsize, zero); // theap!=NULL + mi_assert_internal(p != NULL); + mi_assert_internal(((uintptr_t)p + offset) % alignment == 0); + mi_track_malloc(p, size, zero); + return p; + } + } + } + } + } + + // fallback to generic aligned allocation + return mi_theap_malloc_zero_aligned_at_generic(theap, size, alignment, offset, zero, ppage); +} + + +// ------------------------------------------------------ +// Internal mi_theap_malloc_aligned / mi_malloc_aligned +// ------------------------------------------------------ + +static mi_decl_restrict void* mi_theap_malloc_aligned_at(mi_theap_t* theap, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_malloc_zero_aligned_at(theap, size, alignment, offset, false, NULL); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_theap_malloc_aligned(mi_theap_t* theap, size_t size, size_t alignment) mi_attr_noexcept { + return mi_theap_malloc_aligned_at(theap, size, alignment, 0); +} + +static mi_decl_restrict void* mi_theap_zalloc_aligned_at(mi_theap_t* theap, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_malloc_zero_aligned_at(theap, size, alignment, offset, true, NULL); +} + +mi_decl_restrict void* mi_theap_zalloc_aligned(mi_theap_t* theap, size_t size, size_t alignment) mi_attr_noexcept { + return mi_theap_zalloc_aligned_at(theap, size, alignment, 0); +} + +static mi_decl_restrict void* mi_theap_calloc_aligned_at(mi_theap_t* theap, size_t count, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { + size_t total; + if (mi_count_size_overflow(count, size, &total)) return NULL; + return mi_theap_zalloc_aligned_at(theap, total, alignment, offset); +} + +static mi_decl_restrict void* mi_theap_calloc_aligned(mi_theap_t* theap, size_t count, size_t size, size_t alignment) mi_attr_noexcept { + return mi_theap_calloc_aligned_at(theap, count, size, alignment, 0); +} + + +// ------------------------------------------------------ +// Aligned Allocation +// ------------------------------------------------------ + +mi_decl_nodiscard mi_decl_restrict void* mi_malloc_aligned_at(size_t size, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_malloc_aligned_at(_mi_theap_default(), size, alignment, offset); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_malloc_aligned(size_t size, size_t alignment) mi_attr_noexcept { + return mi_theap_malloc_aligned(_mi_theap_default(), size, alignment); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_umalloc_aligned(size_t size, size_t alignment, size_t* pblock_size) mi_attr_noexcept { + mi_page_t* page; + void* p = mi_theap_malloc_zero_aligned_at(_mi_theap_default(), size, alignment, 0, false, &page); + if (p!=NULL && pblock_size!=NULL) { *pblock_size = mi_page_block_size(page); } + return p; +} + +mi_decl_nodiscard mi_decl_restrict void* mi_zalloc_aligned_at(size_t size, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_zalloc_aligned_at(_mi_theap_default(), size, alignment, offset); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_zalloc_aligned(size_t size, size_t alignment) mi_attr_noexcept { + return mi_theap_zalloc_aligned(_mi_theap_default(), size, alignment); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_uzalloc_aligned(size_t size, size_t alignment, size_t* pblock_size) mi_attr_noexcept { + mi_page_t* page; + void* p = mi_theap_malloc_zero_aligned_at(_mi_theap_default(), size, alignment, 0, true, &page); + if (p!=NULL && pblock_size!=NULL) { *pblock_size = mi_page_block_size(page); } + return p; +} + +mi_decl_nodiscard mi_decl_restrict void* mi_calloc_aligned_at(size_t count, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_calloc_aligned_at(_mi_theap_default(), count, size, alignment, offset); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_calloc_aligned(size_t count, size_t size, size_t alignment) mi_attr_noexcept { + return mi_theap_calloc_aligned(_mi_theap_default(), count, size, alignment); +} + + +mi_decl_nodiscard mi_decl_restrict void* mi_heap_malloc_aligned_at(mi_heap_t* heap, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_malloc_aligned_at(_mi_heap_theap(heap), size, alignment, offset); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_heap_malloc_aligned(mi_heap_t* heap, size_t size, size_t alignment) mi_attr_noexcept { + return mi_theap_malloc_aligned(_mi_heap_theap(heap), size, alignment); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_heap_zalloc_aligned_at(mi_heap_t* heap, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_zalloc_aligned_at(_mi_heap_theap(heap), size, alignment, offset); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_heap_zalloc_aligned(mi_heap_t* heap, size_t size, size_t alignment) mi_attr_noexcept { + return mi_theap_zalloc_aligned(_mi_heap_theap(heap), size, alignment); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_heap_calloc_aligned_at(mi_heap_t* heap, size_t count, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_calloc_aligned_at(_mi_heap_theap(heap), count, size, alignment, offset); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_heap_calloc_aligned(mi_heap_t* heap, size_t count, size_t size, size_t alignment) mi_attr_noexcept { + return mi_theap_calloc_aligned(_mi_heap_theap(heap), count, size, alignment); +} + + +// ------------------------------------------------------ +// Aligned re-allocation +// ------------------------------------------------------ + +static void* mi_theap_realloc_zero_aligned_at(mi_theap_t* theap, void* p, size_t newsize, size_t alignment, size_t offset, bool zero) mi_attr_noexcept { + mi_assert(mi_alignment_is_valid(alignment)); + if mi_unlikely(!mi_alignment_is_valid(alignment)) { // require power-of-two (see ) + return mi_error_bad_alignment(newsize,alignment,offset); + } + if (alignment <= sizeof(uintptr_t) && offset==0) return _mi_theap_realloc_zero(theap,p,newsize,zero); + if (p == NULL) return mi_theap_malloc_zero_aligned_at(theap,newsize,alignment,offset,zero,NULL); + const size_t size = mi_usable_size(p); + if (newsize <= size && newsize >= (size - (size / 2)) && (((uintptr_t)p + offset) & (alignment-1)) == 0) { + return p; // reallocation still fits, is aligned and not more than 50% waste + } + else { + // note: we don't zero allocate upfront so we only zero initialize the expanded part (at the cost of calling mi_usable_size) + mi_page_t* newpage; + void* const newp = mi_theap_malloc_zero_aligned_at(theap,newsize,alignment,offset,false/*zero?*/,&newpage); + if (newp != NULL) { + const size_t copy_size = (newsize > size ? size : newsize); + const size_t zero_start = (copy_size >= sizeof(intptr_t) ? copy_size - sizeof(intptr_t) : 0); // also set last word in the previous allocation to zero to ensure any padding is zero-initialized + const size_t usable = _mi_page_usable_size(newpage,newp); + mi_assert_internal(usable >= newsize); // use usable for zero'ing, issue #763 + if (zero && usable > zero_start) { + // also set last word in the previous allocation to zero to ensure any padding is zero-initialized + mi_memzero((uint8_t*)newp + zero_start, usable - zero_start); + } + mi_memcpy(newp, p, copy_size); // cannot be aligned due to abitrary offset... (todo: require offset to be a multiple of sizeof(void*)?) + mi_free(p); // only free if successful + } + return newp; + } +} + +static void* mi_theap_realloc_zero_aligned(mi_theap_t* theap, void* p, size_t newsize, size_t alignment, bool zero) mi_attr_noexcept { + mi_assert(alignment > 0); + if (alignment <= sizeof(uintptr_t)) return _mi_theap_realloc_zero(theap,p,newsize,zero); + return mi_theap_realloc_zero_aligned_at(theap,p,newsize,alignment,0,zero); +} + +static void* mi_theap_realloc_aligned_at(mi_theap_t* theap, void* p, size_t newsize, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_realloc_zero_aligned_at(theap,p,newsize,alignment,offset,false); +} + +static void* mi_theap_realloc_aligned(mi_theap_t* theap, void* p, size_t newsize, size_t alignment) mi_attr_noexcept { + return mi_theap_realloc_zero_aligned(theap,p,newsize,alignment,false); +} + +static void* mi_theap_rezalloc_aligned_at(mi_theap_t* theap, void* p, size_t newsize, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_realloc_zero_aligned_at(theap, p, newsize, alignment, offset, true); +} + +static void* mi_theap_rezalloc_aligned(mi_theap_t* theap, void* p, size_t newsize, size_t alignment) mi_attr_noexcept { + return mi_theap_realloc_zero_aligned(theap, p, newsize, alignment, true); +} + +static void* mi_theap_recalloc_aligned_at(mi_theap_t* theap, void* p, size_t newcount, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { + size_t total; + if (mi_count_size_overflow(newcount, size, &total)) return NULL; + return mi_theap_rezalloc_aligned_at(theap, p, total, alignment, offset); +} + +static void* mi_theap_recalloc_aligned(mi_theap_t* theap, void* p, size_t newcount, size_t size, size_t alignment) mi_attr_noexcept { + size_t total; + if (mi_count_size_overflow(newcount, size, &total)) return NULL; + return mi_theap_rezalloc_aligned(theap, p, total, alignment); +} + + +mi_decl_nodiscard void* mi_realloc_aligned_at(void* p, size_t newsize, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_realloc_aligned_at(_mi_theap_default(), p, newsize, alignment, offset); +} + +mi_decl_nodiscard void* mi_realloc_aligned(void* p, size_t newsize, size_t alignment) mi_attr_noexcept { + return mi_theap_realloc_aligned(_mi_theap_default(), p, newsize, alignment); +} + +mi_decl_nodiscard void* mi_rezalloc_aligned_at(void* p, size_t newsize, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_rezalloc_aligned_at(_mi_theap_default(), p, newsize, alignment, offset); +} + +mi_decl_nodiscard void* mi_rezalloc_aligned(void* p, size_t newsize, size_t alignment) mi_attr_noexcept { + return mi_theap_rezalloc_aligned(_mi_theap_default(), p, newsize, alignment); +} + +mi_decl_nodiscard void* mi_recalloc_aligned_at(void* p, size_t newcount, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_recalloc_aligned_at(_mi_theap_default(), p, newcount, size, alignment, offset); +} + +mi_decl_nodiscard void* mi_recalloc_aligned(void* p, size_t newcount, size_t size, size_t alignment) mi_attr_noexcept { + return mi_theap_recalloc_aligned(_mi_theap_default(), p, newcount, size, alignment); +} + + +mi_decl_nodiscard void* mi_heap_realloc_aligned_at(mi_heap_t* heap, void* p, size_t newsize, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_realloc_aligned_at(_mi_heap_theap(heap), p, newsize, alignment, offset); +} + +mi_decl_nodiscard void* mi_heap_realloc_aligned(mi_heap_t* heap, void* p, size_t newsize, size_t alignment) mi_attr_noexcept { + return mi_theap_realloc_aligned(_mi_heap_theap(heap), p, newsize, alignment); +} + +mi_decl_nodiscard void* mi_heap_rezalloc_aligned_at(mi_heap_t* heap, void* p, size_t newsize, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_rezalloc_aligned_at(_mi_heap_theap(heap), p, newsize, alignment, offset); +} + +mi_decl_nodiscard void* mi_heap_rezalloc_aligned(mi_heap_t* heap, void* p, size_t newsize, size_t alignment) mi_attr_noexcept { + return mi_theap_rezalloc_aligned(_mi_heap_theap(heap), p, newsize, alignment); +} + +mi_decl_nodiscard void* mi_heap_recalloc_aligned_at(mi_heap_t* heap, void* p, size_t newcount, size_t size, size_t alignment, size_t offset) mi_attr_noexcept { + return mi_theap_recalloc_aligned_at(_mi_heap_theap(heap), p, newcount, size, alignment, offset); +} + +mi_decl_nodiscard void* mi_heap_recalloc_aligned(mi_heap_t* heap, void* p, size_t newcount, size_t size, size_t alignment) mi_attr_noexcept { + return mi_theap_recalloc_aligned(_mi_heap_theap(heap), p, newcount, size, alignment); +} diff --git a/Objects/mimalloc/alloc/alloc-generic.c b/Objects/mimalloc/alloc/alloc-generic.c new file mode 100644 index 000000000000000..57123e2feea1815 --- /dev/null +++ b/Objects/mimalloc/alloc/alloc-generic.c @@ -0,0 +1,494 @@ +/*---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +/* ----------------------------------------------------------- + The "slow" allocation path (`mi_malloc_generic`), including + finding suitable pages with free blocks. +----------------------------------------------------------- */ + +#include "mimalloc.h" +#include "mimalloc/types.h" +#include "mimalloc/prim/prim-tls.h" +#include "mimalloc/util/stats.h" +#include "mimalloc/alloc/alloc.h" +#include "mimalloc/alloc/page.h" +#include "mimalloc/profile/sample-profile.h" +#include "page-queue.h" +#include "page.h" + +// returns `true` if after collection `mi_page_immediate_available` is true. +static inline bool mi_page_free_quick_collect(mi_page_t* page) { + if mi_likely(page->free != NULL) return true; + if (page->local_free == NULL) return false; + // move local_free to free + page->free = page->local_free; + page->local_free = NULL; + page->free_is_zero = false; + _mi_page_update_sample_countdown(page); + return true; +} + + +/* ----------------------------------------------------------- + Find pages with free blocks +-------------------------------------------------------------*/ + +#define MI_XCOLLECT_SCORE_MAX (256) // saturation limit for `xcollect_score` +#define MI_XCOLLECT_SCORE_SHIFT (5) // retain bonus = score >> shift (max 8) +#define MI_XCOLLECT_SCORE_INC (8) // a revived page saves a full abandon/reclaim round-trip + + +// Find a page with free blocks of `page->block_size`. +static mi_decl_noinline mi_page_t* mi_page_queue_find_free_ex(mi_theap_t* theap, mi_page_queue_t* pq, bool first_try) +{ + // search through the pages in "next fit" order + size_t count = 0; + long candidate_limit = 0; // we reset this on the first candidate to limit the search + long page_full_retain = (pq->block_size > MI_SMALL_MAX_OBJ_SIZE ? 0 : theap->page_full_retain); // only retain small pages + if (page_full_retain >= 0) { + // adaptively retain more full pages when cross-thread frees keep reviving them (producer/consumer patterns) + page_full_retain += (long)(pq->xcollect_score >> MI_XCOLLECT_SCORE_SHIFT); + } + mi_page_t* page_candidate = NULL; // a page with free space + mi_page_t* page = pq->first; + mi_page_t* const last = pq->last; + + while (page!=NULL) + { + mi_page_t* next = page->next; // remember next (as this page can move to another queue) + count++; + candidate_limit--; + + // search up to N pages for a best candidate + + // is the local free list non-empty? + bool immediate_available = mi_page_immediate_available(page); + if (!immediate_available) { + // collect freed blocks by us and other threads to we get a proper use count + const bool collected_xfree = _mi_page_free_collect(page, false); + immediate_available = mi_page_immediate_available(page); + if (collected_xfree && immediate_available) { + // a cross-thread free revived this page: retaining such pages avoids an abandon/reclaim round-trip + pq->xcollect_score = mi_min(pq->xcollect_score + MI_XCOLLECT_SCORE_INC, MI_XCOLLECT_SCORE_MAX); + } + else if (pq->xcollect_score > 0) { + pq->xcollect_score--; + } + } + + // if the page is completely full, move it to the `mi_pages_full` + // queue so we don't visit long-lived pages too often. + if (!immediate_available && !mi_page_is_expandable(page)) { + page_full_retain--; + if (page_full_retain < 0) { + mi_assert_internal(!mi_page_is_in_full(page) && !mi_page_immediate_available(page)); + _mi_page_to_full(page, pq); + } + else if (page!=last && pq->last!=page) { + // avoid revisiting this page for a while + mi_page_queue_move_to_back(theap, pq, page); + } + } + else { + // the page has free space, make it a candidate + // we prefer non-expandable pages with high usage as candidates (to reduce commit, and increase chances of free-ing up pages) + if (page_candidate == NULL) { + page_candidate = page; + candidate_limit = _mi_option_get_fast(mi_option_page_max_candidates); + } + else if (mi_page_all_free(page_candidate)) { + _mi_page_free(page_candidate, pq); + page_candidate = page; + } + // prefer to reuse fuller pages (in the hope the less used page gets freed) + else if (mi_page_used(page) >= mi_page_used(page_candidate) && !mi_page_is_mostly_used(page)) { // && !mi_page_is_expandable(page)) { + page_candidate = page; + } + // if we find a non-expandable candidate, or searched for N pages, return with the best candidate + if (immediate_available || candidate_limit <= 0) { + mi_assert_internal(page_candidate!=NULL); + break; + } + } + + #if 0 + // first-fit algorithm without candidates + // If the page contains free blocks, we are done + if (mi_page_immediate_available(page) || mi_page_is_expandable(page)) { + break; // pick this one + } + + // If the page is completely full, move it to the `mi_pages_full` + // queue so we don't visit long-lived pages too often. + mi_assert_internal(!mi_page_is_in_full(page) && !mi_page_immediate_available(page)); + _mi_page_to_full(page, pq); + #endif + if (page==last) { page=NULL; } // don't revisit earlier pages that moved to the back + else { page = next; } + } // for each page + + mi_theap_stat_counter_increase(theap, page_searches, count); + mi_theap_stat_counter_increase(theap, page_searches_count, 1); + + // set the page to the best candidate + if (page_candidate != NULL) { + page = page_candidate; + } + if (page != NULL) { + if (!mi_page_immediate_available(page)) { + mi_assert_internal(mi_page_is_expandable(page)); + if (!_mi_page_extend_free(theap, page)) { + page = NULL; // failed to extend + } + } + mi_assert_internal(page == NULL || mi_page_immediate_available(page)); + } + + if (page == NULL) { + _mi_theap_collect_retired(theap, false); // perhaps make a page available + page = _mi_page_fresh(theap, pq); + mi_assert_internal(page == NULL || mi_page_immediate_available(page)); + if (page == NULL && first_try) { + // out-of-memory _or_ an abandoned page with free blocks was reclaimed, try once again + page = mi_page_queue_find_free_ex(theap, pq, false); + mi_assert_internal(page == NULL || mi_page_immediate_available(page)); + } + } + else { + mi_assert_internal(page == NULL || mi_page_immediate_available(page)); + // move the page to the front of the queue + mi_page_queue_move_to_front(theap, pq, page); + page->retire_expire = 0; + _mi_page_qsbr_clear(page); + // _mi_theap_collect_retired(theap, false); // update retire counts; note: increases rss on MemoryLoad bench so don't do this + } + mi_assert_internal(page == NULL || mi_page_immediate_available(page)); + + + return page; +} + +// Look for a page with free blocks of `size` but don't try to search or allocate +static inline mi_page_t* mi_page_queue_lookup_free_first(mi_theap_t* theap, mi_page_queue_t* pq) { + mi_assert_internal(!mi_page_queue_is_huge(pq)); + mi_page_t* page = pq->first; + if mi_likely(page!=NULL && mi_page_free_quick_collect(page)) { + // fast path + #if (MI_SECURE>=2) // in secure mode, we extend half the time to increase randomness + if (page->capacity < page->reserved && ((_mi_theap_random_next(theap) & 1) == 1)) { + (void)_mi_page_extend_free(theap, page); // ok if this fails + mi_assert_internal(mi_page_immediate_available(page)); + } + #else + MI_UNUSED(theap); + #endif + page->retire_expire = 0; + _mi_page_qsbr_clear(page); + mi_assert_internal(mi_page_immediate_available(page)); + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_assert_internal(mi_ptr_page(mi_page_start(page))==page); + return page; + } + else { + return NULL; + } +} + +// Find a page with free blocks of `size`. +static inline mi_page_t* mi_page_queue_find_free(mi_theap_t* theap, mi_page_queue_t* pq) { + // mi_page_queue_t* pq = mi_page_queue(theap, size); + mi_assert_internal(!mi_page_queue_is_huge(pq)); + + // check the first page: we even do this with candidate search or otherwise we re-search every time + mi_page_t* page = mi_page_queue_lookup_free_first(theap,pq); + if (page==NULL) { + page = mi_page_queue_find_free_ex(theap, pq, true); + if (page==NULL) return NULL; + } + mi_assert_internal(mi_page_immediate_available(page)); + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_assert_internal(mi_ptr_page(mi_page_start(page))==page); + return page; +} + +// Huge pages contain just one block, and the segment contains just that page. +// Huge pages are also use if the requested alignment is very large (> MI_BLOCK_ALIGNMENT_MAX) +// so their size is not always `> MI_LARGE_OBJ_SIZE_MAX`. +static mi_page_t* mi_huge_page_alloc(mi_theap_t* theap, size_t size, size_t page_alignment, mi_page_queue_t* pq) { + _mi_theap_qsbr_collect_delayed(theap); + const size_t block_size = _mi_os_good_alloc_size(size); + // mi_assert_internal(mi_bin(block_size) == MI_BIN_HUGE || page_alignment > 0); + #if MI_HUGE_PAGE_ABANDON + #error todo. + #else + // mi_page_queue_t* pq = mi_page_queue(theap, MI_LARGE_MAX_OBJ_SIZE+1); // always in the huge queue regardless of the block size + mi_assert_internal(mi_page_queue_is_huge(pq)); + #endif + mi_page_t* page = _mi_page_fresh_alloc(theap, pq, block_size, page_alignment); + if (page != NULL) { + mi_assert_internal(mi_page_block_size(page) >= size); + mi_assert_internal(mi_page_immediate_available(page)); + mi_assert_internal(mi_page_is_huge(page)); + mi_assert_internal(mi_page_is_singleton(page)); + #if MI_HUGE_PAGE_ABANDON + mi_assert_internal(mi_page_is_abandoned(page)); + mi_page_set_theap(page, NULL); + #endif + // mi_theap_stat_increase(theap, malloc_huge, mi_page_block_size(page)); + // mi_theap_stat_counter_increase(theap, malloc_huge_count, 1); + } + return page; +} + +// Allocate a page +// Note: in debug mode the size includes MI_PADDING_SIZE and might have overflowed. +static mi_page_t* mi_find_page(mi_theap_t* theap, size_t size, size_t huge_alignment) mi_attr_noexcept { + const size_t req_size = size - MI_PADDING_SIZE; // correct for padding_size in case of an overflow on `size` + if mi_unlikely(req_size > MI_MAX_ALLOC_SIZE) { // TODO: remove as we check in generic_fallback ? + _mi_error_message(EOVERFLOW, "allocation request is too large (%zu bytes)\n", req_size); + return NULL; + } + mi_page_queue_t* pq = mi_page_queue(theap, (huge_alignment > 0 ? MI_LARGE_MAX_OBJ_SIZE+1 : size)); + mi_page_t* page; + // huge allocation? + if mi_unlikely(mi_page_queue_is_huge(pq) || req_size > MI_MAX_ALLOC_SIZE) { + page = mi_huge_page_alloc(theap,size,huge_alignment,pq); + } + else { + // otherwise find a page with free blocks in our size segregated queues + #if MI_PADDING + mi_assert_internal(size >= MI_PADDING_SIZE); + #endif + page = mi_page_queue_find_free(theap,pq); + } + if (page==NULL) return NULL; + mi_assert_internal(mi_page_block_size(page) >= size); + mi_assert_internal(mi_page_immediate_available(page)); + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_assert_internal(mi_ptr_page(mi_page_start(page))==page); + return page; +} + + + +/* ----------------------------------------------------------- + Users can register a deferred free function called + when the `free` list is empty. Since the `local_free` + is separate this is deterministically called after + a certain number of allocations. +----------------------------------------------------------- */ + +// The program should only install a single deferred free handler before doing allocation. +static _Atomic(void*) deferred_free; // is `mi_deferred_free_fun*` (but some platforms don't support atomic function pointers) +static _Atomic(void*) deferred_arg; + +void _mi_deferred_free(mi_theap_t* theap, bool force) { + theap->heartbeat++; + mi_deferred_free_fun* const fun = (mi_deferred_free_fun*)mi_atomic_load_ptr_acquire(void,&deferred_free); + if (fun != NULL && !theap->tld->recurse) { + theap->tld->recurse = true; + void* const arg = mi_atomic_load_ptr_acquire(void,&deferred_arg); + fun(force, theap->heartbeat, arg); + theap->tld->recurse = false; + } +} + +void mi_register_deferred_free(mi_deferred_free_fun* fn, void* arg) mi_attr_noexcept { + mi_atomic_store_ptr_release(void,&deferred_arg, arg); + mi_atomic_store_ptr_release(void,&deferred_free, (void*)fn); +} + + +/* ----------------------------------------------------------- + Admin +----------------------------------------------------------- */ +static mi_theap_t* mi_theap_init(mi_theap_t* theap) { + if mi_likely(mi_theap_is_initialized(theap)) return theap; + if (theap==&_mi_theap_empty_wrong) { + // we were unable to allocate a theap for a first-class heap + return NULL; + } + // otherwise we initialize the thread and its default theap + theap = _mi_thread_init(); + if mi_unlikely(!mi_theap_is_initialized(theap)) { return NULL; } + mi_assert_internal(mi_theap_is_initialized(theap)); + return theap; +} + +static mi_theap_t* mi_malloc_generic_admin(mi_theap_t* theap) +{ + theap = mi_theap_init(theap); + if (theap==NULL) return NULL; + mi_assert_internal(mi_theap_is_initialized(theap)); + + // do administrative tasks every N generic mallocs + if mi_unlikely(theap->generic_count >= 1000) { + theap->generic_collect_count += theap->generic_count; + theap->generic_count = 0; + + // check if the profiler is enabled (unless this theap is permanently excluded, see `mi_theap_profile_disable`) + if (!theap->profile_disabled) { + mi_heap_t* const heap = mi_theap_heap(theap); + mi_profiler_t* prof = mi_atomic_load_ptr_relaxed(mi_profiler_t, &heap->profiler); + const bool prof_enabled = (prof!=NULL && mi_profiler_is_enabled(prof)); + if (prof_enabled && theap->profile_sample_rate==0) { + _mi_theap_set_profile_sample_rate(theap,mi_max(1,prof->initial_sample_rate)); // start profiling + } + else if (!prof_enabled && theap->profile_sample_rate!=0) { + _mi_theap_set_profile_sample_rate(theap,0); // stop profiling + } + } + + // do a full theap collect every once in a while (10000 by default) + const long generic_collect = mi_option_get_clamp(mi_option_generic_collect, 1, 1000000L); + if (theap->generic_collect_count >= generic_collect) { + theap->generic_collect_count = 0; + mi_theap_collect(theap, false /* force? */); + } + // else if (theap->sample_rate != 0) { // update stats more aggressively if we are sampling + // mi_theap_collect(theap, false /* force? */); // TODO: make specialized mi_theap_collect_update_stats ? + // } + else { + // otherwise we do a mini-collect + _mi_deferred_free(theap, false); // call potential deferred free routines + _mi_theap_collect_retired(theap, false); // free retired pages + } + } + return theap; +} + +/* ----------------------------------------------------------- + Generic allocation +----------------------------------------------------------- */ + +static mi_decl_noinline void* mi_malloc_generic_fallback(mi_theap_t* theap, size_t size, bool zero, size_t huge_alignment, mi_page_t** ppage) +{ + // initialize if necessary + theap = mi_malloc_generic_admin(theap); + if (theap==NULL) return NULL; + + // check allocation size + const size_t req_size = size - MI_PADDING_SIZE; + if mi_unlikely(req_size > MI_MAX_ALLOC_SIZE - MI_PADDING_SIZE) { + _mi_error_message(EOVERFLOW, "allocation request is too large (%zu bytes)\n", req_size); + return NULL; + } + + // take a sample? + bool sample_countdown_is_adjusted = false; + if mi_unlikely(mi_theap_should_sample(theap,req_size)) { + if (huge_alignment==0 && theap->sample_rate!=0) { + return _mi_theap_malloc_sampled(theap,req_size,zero,ppage); + } + mi_assert_internal(!_mi_is_empty_theap(theap)); // cannot write to the empty theap + mi_assert_internal(req_size <= MI_SAMPLE_COUNTDOWN_MAX); + if (theap->sample_rate==0) { + theap->sample_countdown = MI_SAMPLE_COUNTDOWN_MAX; // reset the countdown counter + } + #if MI_SAMPLE==2 + else { + // huge_alignment!=0 so we need to adjust the countdown + mi_assert_internal(huge_alignment!=0); + mi_assert_internal(theap->sample_countdown <= SIZE_MAX - req_size); + sample_countdown_is_adjusted = true; + theap->sample_countdown += req_size; // adjust such that after `_mi_page_malloc_zero` the countdown is correct again + } + #endif + } + + // find (or allocate) a page of the right size + mi_page_t* page = mi_find_page(theap, size, huge_alignment); + if mi_unlikely(page == NULL) { // first time out of memory, try to collect and retry the allocation once more + mi_theap_collect(theap, true /* force? */); + page = mi_find_page(theap, size, huge_alignment); + } + + if mi_unlikely(page == NULL) { // out of memory + if (sample_countdown_is_adjusted) { theap->sample_countdown -= req_size; } // unadjust again if we failed to find a page + _mi_error_message(ENOMEM, "unable to allocate memory (%zu bytes)\n", req_size); + return NULL; + } + + mi_assert_internal(mi_page_immediate_available(page)); + mi_assert_internal(mi_page_block_size(page) >= size); + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_assert_internal(mi_ptr_page(mi_page_start(page))==page); + + // and try again, this time succeeding! (i.e. this should never recurse through _mi_page_malloc_zero) + if (ppage!=NULL) { *ppage = page; } + void* const p = _mi_page_malloc_zero(theap,page,size,zero); + mi_assert_internal(p != NULL); + _mi_page_update_sample_countdown(page); + + // move full pages to the full queue + // this will also call _mi_page_update_stats for huge pages + if (mi_page_block_size(page) > MI_SMALL_MAX_OBJ_SIZE) { + if (mi_page_is_full(page)) { + _mi_page_to_full(page, mi_page_queue_of(page)); + } + } + return p; +} + + +// Generic allocation routine if the fast path (`alloc/alloc.c:mi_page_malloc`) does not succeed. +// Note: in debug mode the size includes MI_PADDING_SIZE and might have overflowed. +// The `huge_alignment` is normally 0 but is set to a multiple of MI_SLICE_SIZE for +// very large requested alignments in which case we use a huge singleton page. +// Note: we put `bool zero, size_t huge_alignment` into one parameter (with zero in bit 0) +// to use 4 parameters which compiles better on msvc for the malloc fast path. +mi_decl_restrict void* _mi_malloc_generic(mi_theap_t* theap, size_t size, size_t zero_huge_alignment, mi_page_t** ppage) mi_attr_noexcept +{ + #if !MI_THEAP_INITASNULL + mi_assert_internal(theap != NULL); + #endif + const bool zero = ((zero_huge_alignment & 1) != 0); + const size_t huge_alignment = (zero_huge_alignment & ~1); + mi_assert_internal(huge_alignment==0 || huge_alignment > MI_PAGE_MAX_OVERALLOC_ALIGN); + mi_page_t* page = NULL; + + // fast path objects that fit in a small page + if mi_likely(mi_theap_is_initialized(theap) && ++theap->generic_count < 1000 && huge_alignment==0) { + const size_t req_size = size - MI_PADDING_SIZE; // correct for padding_size in case of an overflow on `size` + if (req_size < MI_SMALL_MAX_OBJ_SIZE) + { + #if MI_SAMPLE + if mi_likely(!mi_theap_should_sample(theap,req_size)) // ensure we don't need to take a sample + #endif + { + mi_page_queue_t* pq = mi_page_queue(theap, size); + mi_assert_internal(pq!=NULL && !mi_page_queue_is_huge(pq)); + page = mi_page_queue_find_free(theap,pq); + // mi_assert_internal(mi_page_block_size(page) <= MI_SMALL_MAX_OBJ_SIZE); + if (page!=NULL) { + if (ppage!=NULL) { *ppage = page; } + mi_assert_internal(mi_page_immediate_available(page)); // we should never recurse in _mi_page_malloc_zero + void* p = _mi_page_malloc_zero(theap,page,size,zero); // always succeeds + mi_assert_internal(p != NULL); + return p; + } + } + } + } + // otherwise fallback + return mi_malloc_generic_fallback(theap,size,zero,huge_alignment,ppage); +} + +// Generic allocation without sampling +mi_decl_restrict void* _mi_malloc_generic_no_sample(mi_theap_t* theap, size_t size, bool zero, mi_page_t** ppage) mi_attr_noexcept +{ + theap = mi_theap_init(theap); + if (theap==NULL) return NULL; + const size_t sample_rate = theap->sample_rate; + const size_t sample_countdown = theap->sample_countdown; + theap->sample_rate = 0; // prevent a recursive call to mi_theap_malloc_sampled from _mi_malloc_generic + theap->sample_countdown = MI_SAMPLE_COUNTDOWN_MAX; + void* p = _mi_malloc_generic(theap, size, (zero ? 1 : 0), ppage); + theap->sample_rate = sample_rate; + theap->sample_countdown = sample_countdown; + return p; +} diff --git a/Objects/mimalloc/alloc-override.c b/Objects/mimalloc/alloc/alloc-override.c similarity index 60% rename from Objects/mimalloc/alloc-override.c rename to Objects/mimalloc/alloc/alloc-override.c index 873065dc63495c8..87d449dfeb2e708 100644 --- a/Objects/mimalloc/alloc-override.c +++ b/Objects/mimalloc/alloc/alloc-override.c @@ -1,19 +1,39 @@ /* ---------------------------------------------------------------------------- -Copyright (c) 2018-2021, Microsoft Research, Daan Leijen +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen This is free software; you can redistribute it and/or modify it under the terms of the MIT license. A copy of the license can be found in the file "LICENSE" at the root of this distribution. -----------------------------------------------------------------------------*/ +// ------------------------------------------------------ +// Overrides for standard allocation functions +// ------------------------------------------------------ +#include "mimalloc.h" +#include "mimalloc/types.h" + #if !defined(MI_IN_ALLOC_C) #error "this file should be included from 'alloc.c' (so aliases can work)" #endif -#if defined(MI_MALLOC_OVERRIDE) && defined(_WIN32) && !(defined(MI_SHARED_LIB) && defined(_DLL)) -#error "It is only possible to override "malloc" on Windows when building as a DLL (and linking the C runtime as a DLL)" -#endif +#include "mimalloc/track.h" + +// ------------------------------------------------------ +// Defined here instead of alloc-posix.c so they can be aliased below, +// (but still outside of MI_MALLOC_OVERRIDE as they are part of the API.) +// ------------------------------------------------------ + +mi_decl_nodiscard size_t mi_malloc_size(const void* p) mi_attr_noexcept { + if (!mi_any_heap_contains(p)) return 0; + return mi_usable_size(p); +} + +mi_decl_nodiscard size_t mi_malloc_usable_size(const void *p) mi_attr_noexcept { + if (!mi_any_heap_contains(p)) return 0; + return mi_usable_size(p); +} + -#if defined(MI_MALLOC_OVERRIDE) && !(defined(_WIN32)) +#if defined(MI_MALLOC_OVERRIDE) && !defined(_DLL) #if defined(__APPLE__) #include @@ -23,7 +43,7 @@ mi_decl_externc size_t malloc_good_size(size_t size); #endif // helper definition for C override of C++ new -typedef struct mi_nothrow_s { int _tag; } mi_nothrow_t; +typedef void* mi_nothrow_t; // ------------------------------------------------------ // Override system malloc @@ -51,7 +71,6 @@ typedef struct mi_nothrow_s { int _tag; } mi_nothrow_t; #define MI_FORWARD02(fun,x,y) { fun(x,y); } #endif - #if defined(__APPLE__) && defined(MI_SHARED_LIB_EXPORT) && defined(MI_OSX_INTERPOSE) // define MI_OSX_IS_INTERPOSED as we should not provide forwarding definitions for // functions that are interposed (or the interposing does not work) @@ -64,29 +83,18 @@ typedef struct mi_nothrow_s { int _tag; } mi_nothrow_t; // use interposing so `DYLD_INSERT_LIBRARIES` works without `DYLD_FORCE_FLAT_NAMESPACE=1` // See: - struct mi_interpose_s { - const void* replacement; - const void* target; - }; - #define MI_INTERPOSE_FUN(oldfun,newfun) { (const void*)&newfun, (const void*)&oldfun } - #define MI_INTERPOSE_MI(fun) MI_INTERPOSE_FUN(fun,mi_##fun) - - __attribute__((used)) static struct mi_interpose_s _mi_interposes[] __attribute__((section("__DATA, __interpose"))) = + MI_INTERPOSE_DECLS(_mi_interposes) = { MI_INTERPOSE_MI(malloc), MI_INTERPOSE_MI(calloc), MI_INTERPOSE_MI(realloc), MI_INTERPOSE_MI(strdup), - MI_INTERPOSE_MI(strndup), MI_INTERPOSE_MI(realpath), MI_INTERPOSE_MI(posix_memalign), MI_INTERPOSE_MI(reallocf), MI_INTERPOSE_MI(valloc), MI_INTERPOSE_FUN(malloc_size,mi_malloc_size_checked), MI_INTERPOSE_MI(malloc_good_size), - #if defined(MAC_OS_X_VERSION_10_15) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_15 - MI_INTERPOSE_MI(aligned_alloc), - #endif #ifdef MI_OSX_ZONE // we interpose malloc_default_zone in alloc-override-osx.c so we can use mi_free safely MI_INTERPOSE_MI(free), @@ -97,6 +105,12 @@ typedef struct mi_nothrow_s { int _tag; } mi_nothrow_t; MI_INTERPOSE_FUN(vfree,mi_cfree), #endif }; + #if defined(MAC_OS_X_VERSION_10_7) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_7) + MI_INTERPOSE_DECLS(_mi_interposes_10_7) = { MI_INTERPOSE_MI(strndup) }; + #endif + #if defined(MAC_OS_X_VERSION_10_15) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_15) + MI_INTERPOSE_DECLS(_mi_interposes_10_15) = { MI_INTERPOSE_MI(aligned_alloc) }; + #endif #ifdef __cplusplus extern "C" { @@ -112,7 +126,7 @@ typedef struct mi_nothrow_s { int _tag; } mi_nothrow_t; #ifdef __cplusplus } #endif - __attribute__((used)) static struct mi_interpose_s _mi_cxx_interposes[] __attribute__((section("__DATA, __interpose"))) = + MI_INTERPOSE_DECLS(_mi_cxx_interposes) = { MI_INTERPOSE_FUN(_ZdlPv,mi_free), MI_INTERPOSE_FUN(_ZdaPv,mi_free), @@ -125,17 +139,108 @@ typedef struct mi_nothrow_s { int _tag; } mi_nothrow_t; }; #elif defined(_MSC_VER) - // cannot override malloc unless using a dll. - // we just override new/delete which does work in a static library. + _Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Size) _ACRTIMP _CRTALLOCATOR _CRT_HYBRIDPATCHABLE + void* __cdecl _expand(_Pre_notnull_ void* _Block, _In_ _CRT_GUARDOVERFLOW size_t _Size) { + return mi__expand(_Block, _Size); + } + _Check_return_ _ACRTIMP + size_t __cdecl _msize_base(_Pre_notnull_ void* _Block) _CRT_NOEXCEPT { + return mi_malloc_size(_Block); + } + _Check_return_ _ACRTIMP _CRT_HYBRIDPATCHABLE + size_t __cdecl _msize(_Pre_notnull_ void* _Block) { + return mi_malloc_size(_Block); + } + _ACRTIMP + void __cdecl _free_base(_Pre_maybenull_ _Post_invalid_ void* _Block) { + mi_free(_Block); + } + _ACRTIMP _CRT_HYBRIDPATCHABLE + void __cdecl free(_Pre_maybenull_ _Post_invalid_ void* _Block) { + mi_free(_Block); + } + _Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Size) _ACRTIMP _CRTALLOCATOR _CRT_JIT_INTRINSIC _CRTRESTRICT _CRT_HYBRIDPATCHABLE + void* __cdecl malloc(_In_ _CRT_GUARDOVERFLOW size_t _Size) { + return mi_malloc(_Size); + } + _Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Size) _ACRTIMP _CRTALLOCATOR _CRTRESTRICT + void* __cdecl _malloc_base(_In_ size_t _Size) { + return mi_malloc(_Size); + } + _Success_(return != 0) _Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Size) _ACRTIMP _CRTALLOCATOR _CRTRESTRICT + void* __cdecl _realloc_base(_Pre_maybenull_ _Post_invalid_ void* _Block, _In_ size_t _Size) { + return mi_realloc(_Block, _Size); + } + _Success_(return != 0) _Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Size) _ACRTIMP _CRTALLOCATOR _CRTRESTRICT _CRT_HYBRIDPATCHABLE + void* __cdecl realloc(_Pre_maybenull_ _Post_invalid_ void* _Block, _In_ _CRT_GUARDOVERFLOW size_t _Size) { + return mi_realloc(_Block, _Size); + } + _Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Count * _Size) _ACRTIMP _CRTALLOCATOR _CRTRESTRICT + void* __cdecl _calloc_base(_In_ size_t _Count, _In_ size_t _Size) { + return mi_calloc(_Count, _Size); + } + _Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Count * _Size) _ACRTIMP _CRT_JIT_INTRINSIC _CRTALLOCATOR _CRTRESTRICT + void* __cdecl calloc(_In_ _CRT_GUARDOVERFLOW size_t _Count, _In_ _CRT_GUARDOVERFLOW size_t _Size) { + return mi_calloc(_Count, _Size); + } + _Success_(return != 0) _Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Count * _Size) _ACRTIMP _CRTALLOCATOR _CRTRESTRICT + void* __cdecl _recalloc_base(_Pre_maybenull_ _Post_invalid_ void* _Block, _In_ size_t _Count, _In_ size_t _Size) { + return mi_recalloc(_Block, _Count, _Size); + } + _Success_(return != 0) _Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Count * _Size) _ACRTIMP _CRTALLOCATOR _CRTRESTRICT + void* __cdecl _recalloc(_Pre_maybenull_ _Post_invalid_ void* _Block, _In_ _CRT_GUARDOVERFLOW size_t _Count, _In_ _CRT_GUARDOVERFLOW size_t _Size) { + return mi_recalloc(_Block, _Count, _Size); + } + _ACRTIMP + void __cdecl _aligned_free(_Pre_maybenull_ _Post_invalid_ void* _Block) { + mi_free(_Block); + } + _Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Size) _ACRTIMP _CRTALLOCATOR _CRTRESTRICT + void* __cdecl _aligned_malloc(_In_ _CRT_GUARDOVERFLOW size_t _Size, _In_ size_t _Alignment) { + return mi_malloc_aligned(_Size, _Alignment); + } + _Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Size) _ACRTIMP _CRTALLOCATOR _CRTRESTRICT + void* __cdecl _aligned_offset_malloc(_In_ _CRT_GUARDOVERFLOW size_t _Size, _In_ size_t _Alignment, _In_ size_t _Offset) { + return mi_malloc_aligned_at(_Size, _Alignment, _Offset); + } + _Check_return_ _ACRTIMP + size_t __cdecl _aligned_msize(_Pre_notnull_ void* _Block, _In_ size_t _Alignment, _In_ size_t _Offset) { + MI_UNUSED(_Alignment); MI_UNUSED(_Offset); return mi_malloc_size(_Block); + } + _Success_(return != 0) _Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Size) _ACRTIMP _CRTALLOCATOR _CRTRESTRICT + void* __cdecl _aligned_offset_realloc(_Pre_maybenull_ _Post_invalid_ void* _Block, _In_ _CRT_GUARDOVERFLOW size_t _Size, _In_ size_t _Alignment, _In_ size_t _Offset) { + return mi_realloc_aligned_at(_Block, _Size, _Alignment, _Offset); + } + _Success_(return != 0) _Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Count * _Size) _ACRTIMP _CRTALLOCATOR _CRTRESTRICT + void* __cdecl _aligned_offset_recalloc(_Pre_maybenull_ _Post_invalid_ void* _Block, _In_ _CRT_GUARDOVERFLOW size_t _Count, _In_ _CRT_GUARDOVERFLOW size_t _Size, _In_ size_t _Alignment, _In_ size_t _Offset) { + return mi_recalloc_aligned_at(_Block, _Count, _Size, _Alignment, _Offset); + } + _Success_(return != 0) _Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Size) _ACRTIMP _CRTALLOCATOR _CRTRESTRICT + void* __cdecl _aligned_realloc(_Pre_maybenull_ _Post_invalid_ void* _Block, _In_ _CRT_GUARDOVERFLOW size_t _Size, _In_ size_t _Alignment) { + return mi_realloc_aligned(_Block, _Size, _Alignment); + } + _Success_(return != 0) _Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Count * _Size) _ACRTIMP _CRTALLOCATOR _CRTRESTRICT + void* __cdecl _aligned_recalloc(_Pre_maybenull_ _Post_invalid_ void* _Block, _In_ _CRT_GUARDOVERFLOW size_t _Count, _In_ _CRT_GUARDOVERFLOW size_t _Size, _In_ size_t _Alignment) { + return mi_recalloc_aligned(_Block, _Count, _Size, _Alignment); + } #else - // On all other systems forward to our API + // On all other systems forward allocation primitives to our API mi_decl_export void* malloc(size_t size) MI_FORWARD1(mi_malloc, size) mi_decl_export void* calloc(size_t size, size_t n) MI_FORWARD2(mi_calloc, size, n) mi_decl_export void* realloc(void* p, size_t newsize) MI_FORWARD2(mi_realloc, p, newsize) mi_decl_export void free(void* p) MI_FORWARD0(mi_free, p) + // In principle we do not need to forward `strdup`/`strndup` but on some systems these do not use `malloc` internally (but a more primitive call) + // We only override if `strdup` is not a macro (as on some older libc's, see issue #885) + #if !defined(strdup) + mi_decl_export char* strdup(const char* str) MI_FORWARD1(mi_strdup, str) + #endif + #if !defined(strndup) && (!defined(__APPLE__) || (defined(MAC_OS_X_VERSION_10_7) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_7)) + mi_decl_export char* strndup(const char* str, size_t n) MI_FORWARD2(mi_strndup, str, n) + #endif #endif -#if (defined(__GNUC__) || defined(__clang__)) && !defined(__APPLE__) +// on macOS export the C++ operators too: interposing them does not work, but as weak definitions an exported one wins. +#if (defined(__GNUC__) || defined(__clang__)) #pragma GCC visibility push(default) #endif @@ -192,12 +297,18 @@ typedef struct mi_nothrow_s { int _tag; } mi_nothrow_t; void _ZdaPv(void* p) MI_FORWARD0(mi_free,p) // delete[] void _ZdlPvm(void* p, size_t n) MI_FORWARD02(mi_free_size,p,n) void _ZdaPvm(void* p, size_t n) MI_FORWARD02(mi_free_size,p,n) + void _ZdlPvSt11align_val_t(void* p, size_t al) { mi_free_aligned(p,al); } void _ZdaPvSt11align_val_t(void* p, size_t al) { mi_free_aligned(p,al); } void _ZdlPvmSt11align_val_t(void* p, size_t n, size_t al) { mi_free_size_aligned(p,n,al); } void _ZdaPvmSt11align_val_t(void* p, size_t n, size_t al) { mi_free_size_aligned(p,n,al); } - #if (MI_INTPTR_SIZE==8) + void _ZdlPvRKSt9nothrow_t(void* p, mi_nothrow_t tag) { MI_UNUSED(tag); mi_free(p); } // operator delete(void*, std::nothrow_t const&) + void _ZdaPvRKSt9nothrow_t(void* p, mi_nothrow_t tag) { MI_UNUSED(tag); mi_free(p); } // operator delete[](void*, std::nothrow_t const&) + void _ZdlPvSt11align_val_tRKSt9nothrow_t(void* p, size_t al, mi_nothrow_t tag) { MI_UNUSED(tag); mi_free_aligned(p,al); } // operator delete(void*, std::align_val_t, std::nothrow_t const&) + void _ZdaPvSt11align_val_tRKSt9nothrow_t(void* p, size_t al, mi_nothrow_t tag) { MI_UNUSED(tag); mi_free_aligned(p,al); } // operator delete[](void*, std::align_val_t, std::nothrow_t const&) + + #if (MI_SIZE_SIZE==8) || (MI_SIZE_SIZE==4 && defined(__EMSCRIPTEN__)) // pr #1257 void* _Znwm(size_t n) MI_FORWARD1(mi_new,n) // new 64-bit void* _Znam(size_t n) MI_FORWARD1(mi_new,n) // new[] 64-bit void* _ZnwmRKSt9nothrow_t(size_t n, mi_nothrow_t tag) { MI_UNUSED(tag); return mi_new_nothrow(n); } @@ -206,7 +317,7 @@ typedef struct mi_nothrow_s { int _tag; } mi_nothrow_t; void* _ZnamSt11align_val_t(size_t n, size_t al) MI_FORWARD2(mi_new_aligned, n, al) void* _ZnwmSt11align_val_tRKSt9nothrow_t(size_t n, size_t al, mi_nothrow_t tag) { MI_UNUSED(tag); return mi_new_aligned_nothrow(n,al); } void* _ZnamSt11align_val_tRKSt9nothrow_t(size_t n, size_t al, mi_nothrow_t tag) { MI_UNUSED(tag); return mi_new_aligned_nothrow(n,al); } - #elif (MI_INTPTR_SIZE==4) + #elif (MI_SIZE_SIZE==4) void* _Znwj(size_t n) MI_FORWARD1(mi_new,n) // new 64-bit void* _Znaj(size_t n) MI_FORWARD1(mi_new,n) // new[] 64-bit void* _ZnwjRKSt9nothrow_t(size_t n, mi_nothrow_t tag) { MI_UNUSED(tag); return mi_new_nothrow(n); } @@ -231,11 +342,11 @@ extern "C" { #ifndef MI_OSX_IS_INTERPOSED // Forward Posix/Unix calls as well void* reallocf(void* p, size_t newsize) MI_FORWARD2(mi_reallocf,p,newsize) - size_t malloc_size(const void* p) MI_FORWARD1(mi_usable_size,p) - #if !defined(__ANDROID__) && !defined(__FreeBSD__) - size_t malloc_usable_size(void *p) MI_FORWARD1(mi_usable_size,p) + size_t malloc_size(const void* p) MI_FORWARD1(mi_malloc_size,p) + #if !defined(__ANDROID__) && !defined(__FreeBSD__) && !defined(__DragonFly__) + size_t malloc_usable_size(void *p) MI_FORWARD1(mi_malloc_usable_size,p) #else - size_t malloc_usable_size(const void *p) MI_FORWARD1(mi_usable_size,p) + size_t malloc_usable_size(const void *p) MI_FORWARD1(mi_malloc_usable_size,p) #endif // No forwarding here due to aliasing/name mangling issues @@ -259,10 +370,13 @@ extern "C" { // no forwarding here due to aliasing/name mangling issues void cfree(void* p) { mi_free(p); } void* pvalloc(size_t size) { return mi_pvalloc(size); } -void* reallocarray(void* p, size_t count, size_t size) { return mi_reallocarray(p, count, size); } -int reallocarr(void* p, size_t count, size_t size) { return mi_reallocarr(p, count, size); } void* memalign(size_t alignment, size_t size) { return mi_memalign(alignment, size); } -void* _aligned_malloc(size_t alignment, size_t size) { return mi_aligned_alloc(alignment, size); } +#if !defined(_WIN32) +void* _aligned_malloc(size_t size, size_t alignment) { return mi_malloc_aligned(size,alignment); } +#endif +void* reallocarray(void* p, size_t count, size_t size) { return mi_reallocarray(p, count, size); } +// some systems define reallocarr so mark it as a weak symbol (#751) +mi_decl_weak int reallocarr(void* p, size_t count, size_t size) { return mi_reallocarr(p, count, size); } #if defined(__wasi__) // forward __libc interface (see PR #667) @@ -272,8 +386,8 @@ void* _aligned_malloc(size_t alignment, size_t size) { return mi_aligned_allo void __libc_free(void* p) MI_FORWARD0(mi_free, p) void* __libc_memalign(size_t alignment, size_t size) { return mi_memalign(alignment, size); } -#elif defined(__GLIBC__) && defined(__linux__) - // forward __libc interface (needed for glibc-based Linux distributions) +#elif defined(__linux__) + // forward __libc interface (needed for glibc-based and musl-based Linux distributions) void* __libc_malloc(size_t size) MI_FORWARD1(mi_malloc,size) void* __libc_calloc(size_t count, size_t size) MI_FORWARD2(mi_calloc,count,size) void* __libc_realloc(void* p, size_t size) MI_FORWARD2(mi_realloc,p,size) @@ -290,8 +404,8 @@ void* _aligned_malloc(size_t alignment, size_t size) { return mi_aligned_allo } #endif -#if (defined(__GNUC__) || defined(__clang__)) && !defined(__APPLE__) +#if (defined(__GNUC__) || defined(__clang__)) #pragma GCC visibility pop #endif -#endif // MI_MALLOC_OVERRIDE && !_WIN32 +#endif // MI_MALLOC_OVERRIDE diff --git a/Objects/mimalloc/alloc-posix.c b/Objects/mimalloc/alloc/alloc-posix.c similarity index 67% rename from Objects/mimalloc/alloc-posix.c rename to Objects/mimalloc/alloc/alloc-posix.c index 225752fd8707fe3..4ba1ccb0a10a150 100644 --- a/Objects/mimalloc/alloc-posix.c +++ b/Objects/mimalloc/alloc/alloc-posix.c @@ -10,14 +10,15 @@ terms of the MIT license. A copy of the license can be found in the file // for convenience and used when overriding these functions. // ------------------------------------------------------------------------ #include "mimalloc.h" -#include "mimalloc/internal.h" +#include "mimalloc/types.h" +#include "mimalloc/prim/os.h" +#include "mimalloc/alloc/alloc.h" // ------------------------------------------------------ // Posix & Unix functions definitions // ------------------------------------------------------ #include -#include // memset #include // getenv #ifdef _MSC_VER @@ -31,44 +32,26 @@ terms of the MIT license. A copy of the license can be found in the file #define ENOMEM 12 #endif - -mi_decl_nodiscard size_t mi_malloc_size(const void* p) mi_attr_noexcept { - // if (!mi_is_in_heap_region(p)) return 0; - return mi_usable_size(p); -} - -mi_decl_nodiscard size_t mi_malloc_usable_size(const void *p) mi_attr_noexcept { - // if (!mi_is_in_heap_region(p)) return 0; - return mi_usable_size(p); -} - mi_decl_nodiscard size_t mi_malloc_good_size(size_t size) mi_attr_noexcept { return mi_good_size(size); } -void mi_cfree(void* p) mi_attr_noexcept { - if (mi_is_in_heap_region(p)) { - mi_free(p); - } -} - -int mi_posix_memalign(void** p, size_t alignment, size_t size) mi_attr_noexcept { +int mi_posix_memalign(void** p, size_t alignment, size_t size) { // mi_attr_noexcept (issue #794) // Note: The spec dictates we should not modify `*p` on an error. (issue#27) // if (p == NULL) return EINVAL; - if ((alignment % sizeof(void*)) != 0) return EINVAL; // natural alignment - // it is also required that alignment is a power of 2 and > 0; this is checked in `mi_malloc_aligned` - if (alignment==0 || !_mi_is_power_of_two(alignment)) return EINVAL; // not a power of 2 + // it is required that alignment is a power of 2 and a multiple of sizeof(void*) + if (alignment= SIZE_MAX - psize) return NULL; // overflow - size_t asize = _mi_align_up(size, psize); + size_t asize = mi_align_up(size, psize); return mi_malloc_aligned(asize, psize); } @@ -95,27 +78,43 @@ mi_decl_nodiscard mi_decl_restrict void* mi_aligned_alloc(size_t alignment, size */ // C11 also requires alignment to be a power-of-two (and > 0) which is checked in mi_malloc_aligned void* p = mi_malloc_aligned(size, alignment); - mi_assert_internal(((uintptr_t)p % alignment) == 0); + mi_assert_internal(mi_is_aligned(p,alignment)); return p; } -mi_decl_nodiscard void* mi_reallocarray( void* p, size_t count, size_t size ) mi_attr_noexcept { // BSD - void* newp = mi_reallocn(p,count,size); +mi_decl_nodiscard void* mi_reallocarray( void* p, size_t count, size_t size ) mi_attr_noexcept { // BSD + size_t total; + if mi_unlikely(mi_count_size_overflow(count, size, &total)) { + errno = EOVERFLOW; + return NULL; + } + void* newp = mi_realloc(p,total); if (newp==NULL) { errno = ENOMEM; } return newp; } -mi_decl_nodiscard int mi_reallocarr( void* p, size_t count, size_t size ) mi_attr_noexcept { // NetBSD - mi_assert(p != NULL); - if (p == NULL) { - errno = EINVAL; - return EINVAL; +mi_decl_nodiscard int mi_reallocarr( void* ptrp, size_t count, size_t size ) mi_attr_noexcept { // NetBSD + mi_assert(size != 0); + mi_assert(ptrp != NULL); + if (ptrp == NULL || size == 0) { + return (errno = EINVAL); + } + size_t total; + if mi_unlikely(mi_count_size_overflow(count, size, &total)) { + return (errno = EOVERFLOW); + } + void** op = (void**)ptrp; + if (total == 0) { + mi_free(*op); + *op = NULL; + return 0; + } + else { + void* newp = mi_realloc(*op,total); + if (newp == NULL) { return (errno = ENOMEM); } + *op = newp; + return 0; } - void** op = (void**)p; - void* newp = mi_reallocarray(*op, count, size); - if mi_unlikely(newp == NULL) { return errno; } - *op = newp; - return 0; } void* mi__expand(void* p, size_t newsize) mi_attr_noexcept { // Microsoft @@ -124,14 +123,15 @@ void* mi__expand(void* p, size_t newsize) mi_attr_noexcept { // Microsoft return res; } -mi_decl_nodiscard mi_decl_restrict unsigned short* mi_wcsdup(const unsigned short* s) mi_attr_noexcept { +mi_decl_nodiscard mi_decl_restrict wchar_t* mi_wcsdup(const wchar_t* s) mi_attr_noexcept { if (s==NULL) return NULL; - size_t len; - for(len = 0; s[len] != 0; len++) { } - size_t size = (len+1)*sizeof(unsigned short); - unsigned short* p = (unsigned short*)mi_malloc(size); + size_t wlen; + for(wlen = 0; s[wlen] != 0 && wlen < PTRDIFF_MAX; wlen++) { } // prevent overflow on wlen+1 + size_t size; + if (mi_mul_overflow(wlen+1, sizeof(wchar_t), &size) || size > PTRDIFF_MAX) return NULL; + wchar_t* p = (wchar_t*)mi_malloc(size); if (p != NULL) { - _mi_memcpy(p,s,size); + mi_memcpy(p,s,size); } return p; } @@ -141,36 +141,36 @@ mi_decl_nodiscard mi_decl_restrict unsigned char* mi_mbsdup(const unsigned char* } int mi_dupenv_s(char** buf, size_t* size, const char* name) mi_attr_noexcept { - if (buf==NULL || name==NULL) return EINVAL; if (size != NULL) *size = 0; - char* p = getenv(name); // mscver warning 4996 + if (buf==NULL || name==NULL) return EINVAL; + char* p = getenv(name); if (p==NULL) { *buf = NULL; } else { *buf = mi_strdup(p); if (*buf==NULL) return ENOMEM; - if (size != NULL) *size = _mi_strlen(p); + if (size != NULL) { *size = _mi_strlen(p) + 1; } // cannot overflow as mi_strdup is limited to PTRDIFF_MAX } return 0; } -int mi_wdupenv_s(unsigned short** buf, size_t* size, const unsigned short* name) mi_attr_noexcept { - if (buf==NULL || name==NULL) return EINVAL; +int mi_wdupenv_s(wchar_t** buf, size_t* size, const wchar_t* name) mi_attr_noexcept { if (size != NULL) *size = 0; + if (buf==NULL || name==NULL) return EINVAL; #if !defined(_WIN32) || (defined(WINAPI_FAMILY) && (WINAPI_FAMILY != WINAPI_FAMILY_DESKTOP_APP)) // not supported *buf = NULL; return EINVAL; #else - unsigned short* p = (unsigned short*)_wgetenv((const wchar_t*)name); // msvc warning 4996 + wchar_t* p = (wchar_t*)_wgetenv(name); if (p==NULL) { *buf = NULL; } else { *buf = mi_wcsdup(p); if (*buf==NULL) return ENOMEM; - if (size != NULL) *size = wcslen((const wchar_t*)p); + if (size != NULL) { *size = wcslen(p) + 1; } // cannot overflow as wcsdup is limited to PTRDIFF_MAX } return 0; #endif diff --git a/Objects/mimalloc/alloc/alloc.c b/Objects/mimalloc/alloc/alloc.c new file mode 100644 index 000000000000000..53623cad9b06ea6 --- /dev/null +++ b/Objects/mimalloc/alloc/alloc.c @@ -0,0 +1,1005 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#ifndef _DEFAULT_SOURCE +#define _DEFAULT_SOURCE // for realpath() on Linux +#endif + +#include // malloc, abort, realpath + +#include "mimalloc.h" +#include "mimalloc/types.h" +#include "mimalloc/prim/prim-tls.h" // _mi_prim_thread_id() +#include "mimalloc/util/stats.h" +#include "mimalloc/alloc/alloc.h" +#include "mimalloc/alloc/free.h" +#include "mimalloc/profile/sample-profile.h" + +#define MI_IN_ALLOC_C +#include "alloc-override.c" +#include "free.c" +#undef MI_IN_ALLOC_C + + +// ------------------------------------------------------ +// Core allocation functions. +// Every allocation goes through `mi_page_malloc_zero` +// ------------------------------------------------------ + +#if MI_PADDING +static mi_decl_noinline void mi_page_block_setup_padding(mi_page_t* page, mi_block_t* block, size_t size) mi_attr_noexcept { + const size_t bsize = mi_page_usable_block_size(page); + mi_padding_t* const padding = (mi_padding_t*)((uint8_t*)block + bsize); + const ptrdiff_t delta = ((uint8_t*)padding - (uint8_t*)block - (size - MI_PADDING_SIZE)); + mi_assert_internal(delta >= 0 && bsize >= (size - MI_PADDING_SIZE + delta)); + mi_track_mem_defined(padding,sizeof(mi_padding_t)); // note: re-enable since mi_page_usable_block_size may set noaccess + padding->canary = mi_ptr_encode_canary(page,block,page->keys); + padding->delta = (uint32_t)(delta); + #if MI_PADDING_CHECK_BYTES + if (!mi_page_is_huge(page)) { + uint8_t* fill = (uint8_t*)padding - delta; + const size_t maxpad = (delta > MI_MAX_ALIGN_SIZE ? MI_MAX_ALIGN_SIZE : delta); // set at most N initial padding bytes + for (size_t i = 0; i < maxpad; i++) { fill[i] = MI_DEBUG_PADDING; } + } + #endif +} +#endif + +// C++ new handler support +static void* mi_theap_new_handler(mi_theap_t* theap, size_t size); +static mi_decl_noinline void* mi_malloc_generic_new(mi_theap_t* theap, size_t size, size_t zero_huge_alignment, mi_page_t** ppage) { + void* p = _mi_malloc_generic(theap, size, zero_huge_alignment, ppage); + if (p == NULL) { return mi_theap_new_handler(theap, size - MI_PADDING_SIZE); } + return p; +} +#if defined(_MSC_VER) && !MI_CLANG_CL +// for the msvc compiler to get tail-calls +static mi_decl_noinline void* mi_malloc_generic_msvc(mi_theap_t* theap, size_t size, size_t zero_new_huge_alignment, mi_page_t** ppage) mi_attr_noexcept { + const bool is_new = ((zero_new_huge_alignment & 2) != 0); + const size_t zero_huge_alignment = (zero_new_huge_alignment & ~2); + if (is_new) { return mi_malloc_generic_new(theap, size, zero_huge_alignment, ppage); } + else { return _mi_malloc_generic(theap, size, zero_huge_alignment, ppage); } +} +#endif + +// C++ new calls a new-handler on failure. +#if MI_SAMPLE==2 +static mi_decl_noinline mi_decl_restrict void* mi_theap_malloc_sampled_new(mi_theap_t* theap, size_t req_size, bool zero, mi_page_t** ppage); +#endif + +// Fast allocation in a page: just pop from the free list. +// Fall back to generic allocation only if the list is empty. +// Note: even though there is a lot of checks etc in the source, +// in release mode the (inlined) routine is about 7 to 10 instructions with a single test. +static mi_decl_forceinline mi_decl_restrict void* mi_page_malloc_zero(mi_theap_t* theap, mi_page_t* page, size_t size, size_t sample_countdown, bool zero, bool is_new, mi_page_t** ppage) +{ + if (page->block_size != 0) { // not the empty theap + mi_assert_internal(mi_page_block_size(page) >= size); + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_assert_internal(mi_ptr_page(mi_page_start(page))==page); + mi_assert_internal(mi_page_alloc_count(page) + mi_page_last_used(page) >= mi_page_used(page)); + } + + // check the free list + mi_block_t* const block = page->free; + mi_used_t xused = page->xused; + #if defined(__GNUC__) + __asm("" : : "r"(xused) : ); // load the `xused` field before the test + #endif + xused.used_alloc += 0x10001; + if mi_unlikely(block == NULL) { + #if defined(_MSC_VER) && !MI_CLANG_CL + return mi_malloc_generic_msvc(theap, size, (zero ? 1 : 0) | (is_new ? 2 : 0), ppage); + #else + if (is_new) { return mi_malloc_generic_new(theap, size, (zero ? 1 : 0), ppage); } + else { return _mi_malloc_generic(theap, size, (zero ? 1 : 0), ppage); } + #endif + } + mi_assert_internal(block != NULL && mi_ptr_page(block) == page); + if (ppage != NULL) { *ppage = page; }; + + // pop from the free list + mi_block_t* next = mi_block_next(page,block); + mi_track_mem_undefined(block,sizeof(*block)); + + #if MI_SECURE + if (!zero) block->next = 0; // don't leak internal data + #endif + + // update the page free list and used count + page->free = next; + page->xused = xused; + + mi_assert_internal(page->free == NULL || mi_ptr_page(page->free) == page); + mi_assert_internal(page->block_size < MI_MAX_ALIGN_SIZE || mi_is_aligned(block, MI_MAX_ALIGN_SIZE)); + + #if MI_SAMPLE==2 + const size_t req_size = size - MI_PADDING_SIZE; + mi_assert_internal(theap->sample_countdown == sample_countdown); // we pass it to improve codegen + mi_assert_internal(theap->sample_countdown >= req_size); + theap->sample_countdown = sample_countdown - req_size; + #else + MI_UNUSED(sample_countdown); + #endif + + #if MI_STATS>=2 + mi_theap_stat_counter_increase(theap,malloc_requested,size - MI_PADDING_SIZE); + #endif + + #if MI_DEBUG>3 + if (page->free_is_zero && size > sizeof(*block)) { + mi_assert_expensive(mi_mem_is_zero(block+1,size - sizeof(*block))); + } + #endif + + // allow use of the block internally + // note: when tracking we need to avoid ever touching the MI_PADDING since + // that is tracked by valgrind etc. as non-accessible (through the red-zone, see `mimalloc/track.h`) + const size_t bsize = mi_page_usable_block_size(page); + mi_track_mem_undefined(block, bsize); + + // track per-block statistics + mi_assert_internal(mi_page_alloc_count(page) + mi_page_last_used(page) >= mi_page_used(page)); + + // in debug mode initialize with 0xD0 + #if (MI_DEBUG>0) && !MI_TRACK_ENABLED && !MI_TSAN + if mi_likely(!zero && !mi_page_is_huge(page)) { mi_debug_fill(page, block, MI_DEBUG_UNINIT, bsize); } + #endif + + // zero the block? note: we need to zero the full block size (issue #63) + if mi_unlikely(zero) { + if (!page->free_is_zero) { + mi_assert_internal(bsize%MI_SIZE_SIZE == 0); + #if !MI_PADDING + return // use tail-call + #endif + mi_memzero_block(block,bsize); + } + else { + block->next = 0; + mi_track_mem_defined(block, bsize); + } + } + + // setup padding (must come after zero'ing) + #if MI_PADDING // && !MI_TRACK_ENABLED + mi_page_block_setup_padding(page,block,size); + #endif + + return block; +} + +// extra entries for improved efficiency in `alloc/alloc-aligned.c` (and in `alloc/alloc-generic.c:mi_malloc_generic`. +extern mi_decl_restrict void* _mi_page_malloc_zero(mi_theap_t* theap, mi_page_t* page, size_t size, bool zero) mi_attr_noexcept { + return mi_page_malloc_zero(theap, page, size, theap->sample_countdown, zero, false, NULL); +} + + +// Internal small size allocation; assumes the `theap` is non-NULL. +// This looks up the page directly from a the direct page entries in the theap. +// The size can be in words (as a wsize); we assume this will be inlined for best code for `mi_wmalloc_small` and similar functions. +static mi_decl_forceinline mi_decl_restrict void* mi_theap_nonnull_xmalloc_small_zero(mi_theap_t* theap, size_t xsize, bool is_wsize, bool zero, bool is_new, mi_page_t** ppage) +{ + size_t size = (is_wsize ? xsize * MI_SIZE_SIZE : xsize); + mi_assert(theap != NULL); + mi_assert(size <= MI_SMALL_SIZE_MAX); + #if MI_DEBUG + mi_assert(mi_theap_matches_thread(theap)); // theaps are thread local + #endif + #if (MI_PADDING) // || MI_GUARDED) + if mi_unlikely(xsize == 0) { xsize = (is_wsize ? 1 : MI_SIZE_SIZE); size += MI_SIZE_SIZE; } + #endif + + // we only sample if fine-grained sampling is enabled (otherwise we sample in mi_malloc_generic) + #if MI_SAMPLE==2 + if mi_unlikely(mi_theap_should_sample(theap,size)) { + if (is_new) { return mi_theap_malloc_sampled_new(theap,size,zero,ppage); } + else { return _mi_theap_malloc_sampled(theap,size,zero,ppage); } + } + #endif + + // get page in constant time + mi_page_t* page = mi_theap_get_free_small_page(theap, xsize + (is_wsize ? MI_PADDING_WSIZE : MI_PADDING_SIZE), is_wsize); + + // and allocate + void* const p = mi_page_malloc_zero(theap, page, size + MI_PADDING_SIZE, theap->sample_countdown, zero, is_new, ppage); + mi_track_malloc(p,size,zero); + + #if MI_DEBUG>3 + if (p != NULL && zero) { + mi_assert_expensive(mi_mem_is_zero(p, size)); + } + #endif + return p; +} + +// Internal generic allocation. +// Goes to `alloc/alloc-generic.c:_mi_malloc_generic` which handles large and aligned allocations. +// Every small allocation eventually goes through this generic allocation path if it cannot be satisfied from the small page cache. +static mi_decl_forceinline void* mi_theap_malloc_generic(mi_theap_t* theap, size_t size, bool zero, size_t huge_alignment, mi_page_t** ppage) mi_attr_noexcept +{ + #if !MI_THEAP_INITASNULL + mi_assert(theap!=NULL); + #endif + mi_assert(mi_theap_matches_thread(theap)); // theaps are thread local + mi_assert((huge_alignment & 1)==0); + void* const p = _mi_malloc_generic(theap, size + MI_PADDING_SIZE, (zero ? 1 : 0) | huge_alignment, ppage); // note: size can overflow but it is detected in malloc_generic + mi_track_malloc(p, size, zero); + + #if MI_DEBUG>3 + if (p != NULL && zero) { + mi_assert_expensive(mi_mem_is_zero(p, size)); + } + #endif + return p; +} + +// ------------------------------------------------------------------------ +// Small allocations: mi_malloc_small and variants (zalloc, wmalloc, wzalloc) +// ------------------------------------------------------------------------ + +// internal small allocation where the theap can be NULL on some platforms (Windows and macOS) +static mi_decl_forceinline mi_decl_restrict void* mi_theap_xmalloc_small_zero(mi_theap_t* theap, size_t xsize, bool is_wsize, bool zero, mi_page_t** ppage) mi_attr_noexcept { + #if !MI_THEAP_INITASNULL + return mi_theap_nonnull_xmalloc_small_zero(theap, xsize, is_wsize, zero, false, ppage); + #else + if (theap!=NULL) { + return mi_theap_nonnull_xmalloc_small_zero(theap, xsize, is_wsize, zero, false, ppage); + } + else { + return mi_theap_malloc_generic(theap, (is_wsize ? xsize * MI_SIZE_SIZE : xsize), zero, 0, ppage); // tailcall + } + #endif +} + +// Allocate a small block from a theap; these routines assume a non-NULL theap. +mi_decl_nodiscard extern inline mi_decl_restrict void* mi_theap_malloc_small(mi_theap_t* theap, size_t size) mi_attr_noexcept { + mi_assert(theap!=NULL); + return mi_theap_nonnull_xmalloc_small_zero(theap, size, false, false, false, NULL); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_theap_wmalloc_small(mi_theap_t* theap, size_t wsize) mi_attr_noexcept { + mi_assert(theap!=NULL); + return mi_theap_nonnull_xmalloc_small_zero(theap, wsize, true, false, false, NULL); +} + +mi_decl_nodiscard extern inline mi_decl_restrict void* mi_theap_zalloc_small(mi_theap_t* theap, size_t size) mi_attr_noexcept { + mi_assert(theap!=NULL); + return mi_theap_nonnull_xmalloc_small_zero(theap, size, false, true, false, NULL); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_theap_wzalloc_small(mi_theap_t* theap, size_t wsize) mi_attr_noexcept { + mi_assert(theap!=NULL); + return mi_theap_nonnull_xmalloc_small_zero(theap, wsize, true, true, false, NULL); +} + +// Regular small allocation functions that use the default theap. +mi_decl_nodiscard mi_decl_restrict void* mi_malloc_small(size_t size) mi_attr_noexcept { + return mi_theap_xmalloc_small_zero(_mi_theap_default(), size, false, false, NULL); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_zalloc_small(size_t size) mi_attr_noexcept { + return mi_theap_xmalloc_small_zero(_mi_theap_default(), size, false, true, NULL); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_wmalloc_small(size_t wsize) mi_attr_noexcept { + return mi_theap_xmalloc_small_zero(_mi_theap_default(), wsize, true, false, NULL ); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_wzalloc_small(size_t wsize) mi_attr_noexcept { + return mi_theap_xmalloc_small_zero(_mi_theap_default(), wsize, true, true, NULL); +} + + +mi_decl_nodiscard static mi_decl_noinline mi_decl_restrict void* mi_heap_init_malloc_small(mi_heap_t* heap, size_t size) mi_attr_noexcept { + return mi_theap_malloc_small(_mi_heap_theap_get_or_init(heap), size); +} +mi_decl_nodiscard mi_decl_restrict void* mi_heap_malloc_small(mi_heap_t* heap, size_t size) mi_attr_noexcept { + // we could also use: mi_theap_malloc_small_zero_nonnull(_mi_heap_theap(theap), size, false, NULL); } + // but the following prevents using a stack frame. We use this to optimize some select functions only. + mi_theap_t* const theap = _mi_heap_theap_cached(heap); + if mi_likely(theap!=NULL) { return mi_theap_malloc_small(theap, size); } + else { return mi_heap_init_malloc_small(heap, size); } +} + +mi_decl_nodiscard static mi_decl_noinline mi_decl_restrict void* mi_heap_init_zalloc_small(mi_heap_t* heap, size_t size) mi_attr_noexcept { + return mi_theap_zalloc_small(_mi_heap_theap_get_or_init(heap), size); +} +mi_decl_nodiscard mi_decl_restrict void* mi_heap_zalloc_small(mi_heap_t* heap, size_t size) mi_attr_noexcept { + // optimize: return mi_theap_malloc_small_zero_nonnull(_mi_heap_theap(heap), size, true, NULL); + mi_theap_t* const theap = _mi_heap_theap_cached(heap); + if mi_likely(theap!=NULL) { return mi_theap_zalloc_small(theap, size); } + else { return mi_heap_init_zalloc_small(heap, size); } +} + + +// ------------------------------------------------------------------------------ +// Main allocation functions: mi_malloc and variants (zalloc, wmalloc, wzalloc) +// ------------------------------------------------------------------------------ + +mi_decl_nodiscard static mi_decl_forceinline mi_decl_restrict void* mi_theap_nonnull_malloc_zero(mi_theap_t* theap, size_t size, bool zero, size_t huge_alignment, mi_page_t** ppage) mi_attr_noexcept { + mi_assert(theap!=NULL); + // fast path for small objects + if mi_likely(size <= MI_SMALL_SIZE_MAX) { + mi_assert_internal(huge_alignment == 0); + return mi_theap_nonnull_xmalloc_small_zero(theap, size, false, zero, false, ppage); + } + else { + return mi_theap_malloc_generic(theap, size, zero, huge_alignment, ppage); + } +} + +mi_decl_nodiscard extern mi_decl_forceinline mi_decl_restrict void* _mi_theap_malloc_zero(mi_theap_t* theap, size_t size, bool zero, size_t huge_alignment, mi_page_t** ppage) mi_attr_noexcept { + // fast path for small objects + #if MI_THEAP_INITASNULL + if mi_likely(theap!=NULL && size <= MI_SMALL_SIZE_MAX) + #else + if mi_likely(size <= MI_SMALL_SIZE_MAX) + #endif + { + mi_assert_internal(huge_alignment == 0); + return mi_theap_nonnull_xmalloc_small_zero(theap, size, false, zero, false, ppage); + } + else { + return mi_theap_malloc_generic(theap, size, zero, huge_alignment, ppage); + } +} + +mi_decl_nodiscard static inline mi_decl_restrict void* mi_theap_nonnull_malloc(mi_theap_t* theap, size_t size) mi_attr_noexcept { + mi_assert(theap!=NULL); + return mi_theap_nonnull_malloc_zero(theap, size, false, 0, NULL); +} + +mi_decl_nodiscard static inline mi_decl_restrict void* mi_theap_nonnull_zalloc(mi_theap_t* theap, size_t size) mi_attr_noexcept { + mi_assert(theap!=NULL); + return mi_theap_nonnull_malloc_zero(theap, size, true, 0, NULL); +} + +// Main allocation functions +// (note: to reduce variants, we only use nonnull theap variants for small allocations) + +mi_decl_nodiscard extern inline mi_decl_restrict void* mi_theap_malloc(mi_theap_t* theap, size_t size) mi_attr_noexcept { + return _mi_theap_malloc_zero(theap, size, false, 0, NULL); +} + +mi_decl_nodiscard extern inline mi_decl_restrict void* mi_theap_zalloc(mi_theap_t* theap, size_t size) mi_attr_noexcept { + return _mi_theap_malloc_zero(theap, size, true, 0, NULL); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_malloc(size_t size) mi_attr_noexcept { + return mi_theap_malloc(_mi_theap_default(), size); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_zalloc(size_t size) mi_attr_noexcept { + return mi_theap_zalloc(_mi_theap_default(), size); +} + +mi_decl_nodiscard static mi_decl_noinline mi_decl_restrict void* mi_heap_init_malloc(mi_heap_t* heap, size_t size) mi_attr_noexcept { + return mi_theap_nonnull_malloc(_mi_heap_theap_get_or_init(heap), size); +} +mi_decl_nodiscard mi_decl_restrict void* mi_heap_malloc(mi_heap_t* heap, size_t size) mi_attr_noexcept { + // return mi_theap_malloc_zero_nonnull(_mi_heap_theap(heap), size, false, 0, NULL); + mi_theap_t* const theap = _mi_heap_theap_cached(heap); + if mi_likely(theap!=NULL) { return mi_theap_nonnull_malloc(theap, size); } + else { return mi_heap_init_malloc(heap, size); } + +} + +mi_decl_nodiscard static mi_decl_noinline mi_decl_restrict void* mi_heap_init_zalloc(mi_heap_t* heap, size_t size) mi_attr_noexcept { + return mi_theap_nonnull_zalloc(_mi_heap_theap_get_or_init(heap), size); +} +mi_decl_nodiscard mi_decl_restrict void* mi_heap_zalloc(mi_heap_t* heap, size_t size) mi_attr_noexcept { + // optimize: return mi_theap_malloc_zero_nonnull(_mi_heap_theap(heap), size, true, 0, NULL); + mi_theap_t* const theap = _mi_heap_theap_cached(heap); + if mi_likely(theap!=NULL) { return mi_theap_nonnull_zalloc(theap, size); } + else { return mi_heap_init_zalloc(heap, size); } +} + +// ---------------------------------------------------------- +// mi_calloc variants +// ---------------------------------------------------------- + +mi_decl_nodiscard extern inline mi_decl_restrict void* mi_theap_calloc(mi_theap_t* theap, size_t count, size_t size) mi_attr_noexcept { + size_t total; + if (mi_count_size_overflow(count,size,&total)) return NULL; + return mi_theap_zalloc(theap,total); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_calloc(size_t count, size_t size) mi_attr_noexcept { + return mi_theap_calloc(_mi_theap_default(), count, size); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_heap_calloc(mi_heap_t* heap, size_t count, size_t size) mi_attr_noexcept { + return mi_theap_calloc(_mi_heap_theap(heap), count, size); +} + +// ---------------------------------------------------------- +// Usable block sizes +// ---------------------------------------------------------- + +// Return usable size +static void* mi_ublock_size( void* p, mi_page_t* page, size_t* pblock_size ) { + mi_assert_internal(page == mi_ptr_page(p)); + if (pblock_size!=NULL) { + if (p!=NULL) { *pblock_size = mi_page_block_size(page); } + } + return p; +} + +mi_decl_nodiscard mi_decl_restrict void* mi_umalloc_small(size_t size, size_t* pblock_size) mi_attr_noexcept { + mi_page_t* page; + void* p = mi_theap_xmalloc_small_zero(_mi_theap_default(), size, false, false, &page); + return mi_ublock_size(p,page,pblock_size); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_uzalloc_small(size_t size, size_t* pblock_size) mi_attr_noexcept { + mi_page_t* page; + void* p = mi_theap_xmalloc_small_zero(_mi_theap_default(), size, false, true, &page); + return mi_ublock_size(p,page,pblock_size); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_umalloc(size_t size, size_t* pblock_size) mi_attr_noexcept { + mi_page_t* page; + void* p = _mi_theap_malloc_zero(_mi_theap_default(), size, false, 0, &page); + return mi_ublock_size(p,page,pblock_size); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_uzalloc(size_t size, size_t* pblock_size) mi_attr_noexcept { + mi_page_t* page; + void* p = _mi_theap_malloc_zero(_mi_theap_default(), size, true, 0, &page); + return mi_ublock_size(p,page,pblock_size); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_ucalloc(size_t count, size_t size, size_t* pblock_size) mi_attr_noexcept { + size_t total; + if (mi_count_size_overflow(count,size,&total)) return NULL; + return mi_uzalloc(total, pblock_size); +} + +// ---------------------------------------------------------- +// Uninitialized `calloc` +// ---------------------------------------------------------- + +mi_decl_nodiscard mi_decl_restrict void* mi_mallocn(size_t count, size_t size) mi_attr_noexcept { + size_t total; + if (mi_count_size_overflow(count, size, &total)) return NULL; + return mi_malloc(total); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_heap_mallocn(mi_heap_t* heap, size_t count, size_t size) mi_attr_noexcept { + size_t total; + if (mi_count_size_overflow(count, size, &total)) return NULL; + return mi_heap_malloc(heap, total); +} + +// ---------------------------------------------------------- +// Reallocation +// ---------------------------------------------------------- + +// Expand (or shrink) in place (or fail) +void* mi_expand(void* p, size_t newsize) mi_attr_noexcept { + #if MI_PADDING + // we do not shrink/expand with padding enabled + MI_UNUSED(p); MI_UNUSED(newsize); + return NULL; + #else + if (p == NULL) return NULL; + const mi_page_t* const page = mi_ptr_page_validate(p,"mi_expand"); + const size_t size = _mi_page_usable_size(page,p); + if (newsize > size) return NULL; + return p; // it fits + #endif +} + +static mi_decl_forceinline void* mi_theap_realloc_zero_ex(mi_theap_t* theap, void* p, size_t newsize, bool zero, size_t* pblock_size_pre, size_t* pblock_size_post) mi_attr_noexcept { + // if p == NULL then behave as malloc. + // else if size == 0 then reallocate to a zero-sized block (and don't return NULL, just as mi_malloc(0)). + // (this means that returning NULL always indicates an error, and `p` will not have been freed in that case.) + const mi_page_t* page; + size_t size; + if (p==NULL) { + page = NULL; + size = 0; + if (pblock_size_pre!=NULL) { *pblock_size_pre = 0; } + } + else { + page = mi_ptr_page_validate(p,"mi_realloc"); + if mi_unlikely(page==NULL) { // invalid pointer + if (pblock_size_pre!=NULL) { *pblock_size_pre = 0; } + if (pblock_size_post!=NULL) { *pblock_size_post = 0; } + return NULL; + } + size = _mi_page_usable_size(page,p); + if (pblock_size_pre!=NULL) { *pblock_size_pre = mi_page_block_size(page); } + } + // check if we can reuse the existing block + if mi_unlikely(newsize<=size && newsize>=(size/2) && newsize>0) { // note: newsize must be > 0 or otherwise we return NULL for realloc(NULL,0) + mi_assert_internal(page!=NULL); // note: page!=NULL (since if p==NULL, we have size=0 and size>=newsize>0 + #if MI_THEAP_INITASNULL + if (theap!=NULL) + #endif + { + if (mi_page_heap(page)==mi_theap_heap_peek(theap)) { // and within the same heap + mi_assert_internal(p!=NULL); + // todo: do not track as the usable size is still the same in the free; adjust potential padding? + // mi_track_resize(p,size,newsize) + // if (newsize < size) { mi_track_mem_noaccess((uint8_t*)p + newsize, size - newsize); } + if (pblock_size_post!=NULL) { *pblock_size_post = mi_page_block_size(page); } + return p; // reallocation still fits and not more than 50% waste + } + } + } + // note: we don't zero allocate upfront so we only zero initialize the expanded part + mi_page_t* newpage; // use block_size for zero-ing, issue #763 + void* const newp = _mi_theap_malloc_zero(theap,newsize,false /* no zero */,0,&newpage); + if mi_likely(newp != NULL) { + if (pblock_size_post!=NULL) { *pblock_size_post = mi_page_block_size(newpage); } + const size_t copy_size = (newsize > size ? size : newsize); + const size_t zero_start = mi_align_down( (copy_size >= sizeof(intptr_t) ? copy_size - sizeof(intptr_t) : 0), sizeof(intptr_t)); // also set last word in the previous allocation to zero to ensure any padding is zero-initialized + const size_t usable = _mi_page_usable_size(newpage,newp); + mi_assert_internal(usable >= newsize); + if (zero && usable > zero_start) { + mi_memzero_aligned((uint8_t*)newp + zero_start, usable - zero_start); + } + else if (newsize == 0) { + ((uint8_t*)newp)[0] = 0; // work around for applications that expect zero-reallocation to be zero initialized (issue #725) + } + if mi_likely(p != NULL) { + mi_track_mem_defined(p,copy_size); // _mi_useable_size may be too large for byte precise memory tracking.. + mi_memcpy_aligned(newp, p, copy_size); + mi_free(p); // only free the original pointer if successful + } + } + return newp; +} + +void* _mi_theap_realloc_zero(mi_theap_t* theap, void* p, size_t newsize, bool zero) mi_attr_noexcept { + return mi_theap_realloc_zero_ex(theap,p,newsize,zero,NULL,NULL); +} + +// for theap_realloc/rezalloc we allow theap==NULL to reduce variants +mi_decl_nodiscard void* mi_theap_realloc(mi_theap_t* theap, void* p, size_t newsize) mi_attr_noexcept { + if (p==NULL) { + return mi_theap_malloc(theap,newsize); + } + else { + return _mi_theap_realloc_zero(theap, p, newsize, false); + } +} + +mi_decl_nodiscard void* mi_theap_rezalloc(mi_theap_t* theap, void* p, size_t newsize) mi_attr_noexcept { + if (p==NULL) { + return mi_theap_zalloc(theap,newsize); + } + else { + return _mi_theap_realloc_zero(theap, p, newsize, true); + } +} + +static void* mi_theap_recalloc(mi_theap_t* theap, void* p, size_t count, size_t size) mi_attr_noexcept { + size_t total; + if (mi_count_size_overflow(count, size, &total)) return NULL; + return mi_theap_rezalloc(theap, p, total); +} + +mi_decl_nodiscard void* mi_realloc(void* p, size_t newsize) mi_attr_noexcept { + return mi_theap_realloc(_mi_theap_default(),p,newsize); +} + +mi_decl_nodiscard void* mi_reallocn(void* p, size_t count, size_t size) mi_attr_noexcept { + size_t total; + if (mi_count_size_overflow(count, size, &total)) return NULL; + return mi_theap_realloc(_mi_theap_default(),p,total); +} + +mi_decl_nodiscard void* mi_urealloc(void* p, size_t newsize, size_t* pblock_size_pre, size_t* pblock_size_post) mi_attr_noexcept { + return mi_theap_realloc_zero_ex(_mi_theap_default(),p,newsize, false, pblock_size_pre, pblock_size_post); +} + +// Reallocate but free `p` on errors +static void* mi_theap_reallocf(mi_theap_t* xtheap, void* p, size_t newsize) mi_attr_noexcept { + void* newp = mi_theap_realloc(xtheap, p, newsize); + if (newp==NULL && p!=NULL) mi_free(p); + return newp; +} + +mi_decl_nodiscard void* mi_reallocf(void* p, size_t newsize) mi_attr_noexcept { + return mi_theap_reallocf(_mi_theap_default(),p,newsize); +} + +mi_decl_nodiscard void* mi_rezalloc(void* p, size_t newsize) mi_attr_noexcept { + return mi_theap_rezalloc(_mi_theap_default(), p, newsize); +} + +mi_decl_nodiscard void* mi_recalloc(void* p, size_t count, size_t size) mi_attr_noexcept { + return mi_theap_recalloc(_mi_theap_default(), p, count, size); +} + +mi_decl_nodiscard void* mi_heap_realloc(mi_heap_t* heap, void* p, size_t newsize) mi_attr_noexcept { + return mi_theap_realloc(_mi_heap_theap(heap), p, newsize); +} + +mi_decl_nodiscard void* mi_heap_reallocn(mi_heap_t* heap, void* p, size_t count, size_t size) mi_attr_noexcept { + size_t total; + if (mi_count_size_overflow(count, size, &total)) return NULL; + return mi_theap_realloc(_mi_heap_theap(heap), p, total); +} + +// Reallocate but free `p` on errors +mi_decl_nodiscard void* mi_heap_reallocf(mi_heap_t* heap, void* p, size_t newsize) mi_attr_noexcept { + return mi_theap_reallocf(_mi_heap_theap(heap), p, newsize); +} + +mi_decl_nodiscard void* mi_heap_rezalloc(mi_heap_t* heap, void* p, size_t newsize) mi_attr_noexcept { + return mi_theap_rezalloc(_mi_heap_theap(heap), p, newsize); +} + +mi_decl_nodiscard void* mi_heap_recalloc(mi_heap_t* heap, void* p, size_t count, size_t size) mi_attr_noexcept { + return mi_theap_recalloc(_mi_heap_theap(heap), p, count, size); +} + + +// ------------------------------------------------------ +// strdup, strndup, and realpath +// ------------------------------------------------------ + +// `strdup` using mi_malloc +mi_decl_nodiscard static mi_decl_restrict char* mi_theap_strdup(mi_theap_t* xtheap, const char* s) mi_attr_noexcept { + if (s == NULL) return NULL; + size_t len = _mi_strlen(s); + if (len > MI_MAX_ALLOC_SIZE - 1) return NULL; // prevent overflow on len+1 + char* t = (char*)mi_theap_malloc(xtheap,len+1); + if (t == NULL) return NULL; + mi_memcpy(t, s, len); + t[len] = 0; + return t; +} + +mi_decl_nodiscard mi_decl_restrict char* mi_strdup(const char* s) mi_attr_noexcept { + return mi_theap_strdup(_mi_theap_default(), s); +} + +mi_decl_nodiscard mi_decl_restrict char* mi_heap_strdup(mi_heap_t* heap, const char* s) mi_attr_noexcept { + return mi_theap_strdup(_mi_heap_theap(heap), s); +} + +// `strndup` using mi_malloc +mi_decl_nodiscard static mi_decl_restrict char* mi_theap_strndup(mi_theap_t* xtheap, const char* s, size_t n) mi_attr_noexcept { + if (s == NULL) return NULL; + const size_t len = _mi_strnlen(s,n); // len <= n + if (len > MI_MAX_ALLOC_SIZE - 1) return NULL; // prevent overflow on len+1 + char* t = (char*)mi_theap_malloc(xtheap, len+1); + if (t == NULL) return NULL; + mi_memcpy(t, s, len); + t[len] = 0; + return t; +} + +mi_decl_nodiscard mi_decl_restrict char* mi_strndup(const char* s, size_t n) mi_attr_noexcept { + return mi_theap_strndup(_mi_theap_default(),s,n); +} + +mi_decl_nodiscard mi_decl_restrict char* mi_heap_strndup(mi_heap_t* heap, const char* s, size_t n) mi_attr_noexcept { + return mi_theap_strndup(_mi_heap_theap(heap), s, n); +} + +#ifndef __wasi__ +// `realpath` using mi_malloc +#ifdef _WIN32 +#ifndef PATH_MAX +#define PATH_MAX MAX_PATH +#endif + +mi_decl_nodiscard static mi_decl_restrict char* mi_theap_realpath(mi_theap_t* xtheap, const char* fname, char* resolved_name) mi_attr_noexcept { + // todo: use GetFullPathNameW to allow longer file names + if (fname==NULL || *fname==0) { + errno = EINVAL; + return NULL; + } + char buf[PATH_MAX]; + DWORD res = GetFullPathNameA(fname, PATH_MAX, (resolved_name == NULL ? buf : resolved_name), NULL); + if (res == 0) { + DWORD err = GetLastError(); + switch (err) { + case ERROR_LOCK_VIOLATION: + case ERROR_SHARING_VIOLATION: + case ERROR_INVALID_ACCESS: errno = EACCES; break; + case ERROR_INVALID_HANDLE: + case ERROR_INVALID_FUNCTION: errno = EINVAL; break; + case ERROR_PATH_NOT_FOUND: errno = ENOTDIR; break; + case ERROR_FILE_NOT_FOUND: errno = ENOENT; break; + case ERROR_NOT_ENOUGH_MEMORY: errno = ENOMEM; break; + default: errno = EIO; + } + return NULL; + } + else if (res > PATH_MAX) { + errno = ENAMETOOLONG; + return NULL; + } + else if (resolved_name != NULL) { + return resolved_name; + } + else { + return mi_theap_strndup(xtheap, buf, PATH_MAX); + } +} + +#else + +#include // pathconf + +static size_t mi_path_max(void) { + static _Atomic(size_t) path_max = 0; + size_t pmax = mi_atomic_load_acquire(&path_max); + if (pmax == 0) { + long m = 0; + #ifdef _PC_PATH_MAX + m = pathconf("/",_PC_PATH_MAX); + #endif + if (m <= 0) pmax = 4096; // guess + else if (m < 256) pmax = 256; // at least 256 + else if (m > 64*1024) pmax = 64*1024; // at most 64 KiB + else pmax = m; + size_t expected = 0; + mi_atomic_cas_strong_acq_rel(&path_max, &expected, pmax); + } + return pmax; +} + +char* mi_theap_realpath(mi_theap_t* xtheap, const char* fname, char* resolved_name) mi_attr_noexcept { + if (resolved_name != NULL) { + return realpath(fname,resolved_name); + } + else { + /* + char* rname = realpath(fname, NULL); + if (rname == NULL) return NULL; + char* result = mi_heap_strdup(heap, rname); + mi_cfree(rname); // note: may leak the original pointer if allocated internally with the system allocator + // note: with ASAN realpath is intercepted and mi_cfree may leak the returned pointer :-( + return result; + */ + const size_t n = mi_path_max(); + char* const buf = (char*)mi_theap_zalloc(xtheap,n+1); + if (buf == NULL) { + errno = ENOMEM; + return NULL; + } + char* rname = realpath(fname,buf); + char* result = mi_theap_strndup(xtheap,rname,n); // ok if `rname==NULL` + mi_free(buf); + return result; + } +} +#endif + +mi_decl_nodiscard mi_decl_restrict char* mi_realpath(const char* fname, char* resolved_name) mi_attr_noexcept { + return mi_theap_realpath(_mi_theap_default(),fname,resolved_name); +} + +mi_decl_nodiscard mi_decl_restrict char* mi_heap_realpath(mi_heap_t* heap, const char* fname, char* resolved_name) mi_attr_noexcept { + return mi_theap_realpath(_mi_heap_theap(heap), fname, resolved_name); +} +#endif + +/*------------------------------------------------------- +C++ new and new_aligned +The standard requires calling into `get_new_handler` and +throwing the bad_alloc exception on failure. If we compile +with a C++ compiler we can implement this precisely. If we +use a C compiler we cannot throw a `bad_alloc` exception +but we call `abort` instead (i.e. not returning). +Also, the standard requires calling the new handler until +it returns false, but we limit the total calls. +-------------------------------------------------------*/ +#define MI_TRY_NEW_MAX (4) + +#ifdef __cplusplus +#include +static bool mi_try_new_handler(void) { + #if defined(_MSC_VER) || (__cplusplus >= 201103L) + std::new_handler h = std::get_new_handler(); + #else + std::new_handler h = std::set_new_handler(); + std::set_new_handler(h); + #endif + if (h==NULL) { + _mi_error_message(ENOMEM, "out of memory in 'new'"); + #if defined(_CPPUNWIND) || defined(__cpp_exceptions) // exceptions are not always enabled + throw std::bad_alloc(); + #endif + return false; + } + else { + h(); + return true; + } +} +#else +typedef void (*std_new_handler_t)(void); + +#if (defined(__GNUC__) || (defined(__clang__) && !defined(_MSC_VER))) // exclude clang-cl, see issue #631 +std_new_handler_t __attribute__((weak)) _ZSt15get_new_handlerv(void) { + return NULL; +} +static std_new_handler_t mi_get_new_handler(void) { + return _ZSt15get_new_handlerv(); +} +#else +// note: on windows we could dynamically link to `?get_new_handler@std@@YAP6AXXZXZ`. +static std_new_handler_t mi_get_new_handler(void) { + return NULL; +} +#endif + +static bool mi_try_new_handler(void) { + std_new_handler_t h = mi_get_new_handler(); + if (h==NULL) { + _mi_error_message(ENOMEM, "out of memory in 'new'"); + abort(); // cannot throw in plain C, use abort + return false; + } + else { + h(); + return true; + } +} +#endif + +static mi_decl_noinline mi_decl_restrict void* mi_try_new_handler_null(void) { + mi_try_new_handler(); + return NULL; +} + +// called when an allocation fails and the new handler needs to be invoked +static mi_decl_noinline void* mi_theap_new_handler(mi_theap_t* theap, size_t size) { + void* p = NULL; + for(int i = 0; i < MI_TRY_NEW_MAX && p == NULL && mi_try_new_handler(); i++) { + if (size > MI_MAX_ALLOC_SIZE) return NULL; // call try_new_handler at least once + p = mi_theap_malloc(theap,size); + } + return p; +} + +#if MI_SAMPLE==2 +static mi_decl_noinline void* mi_theap_malloc_sampled_new(mi_theap_t* theap, size_t req_size, bool zero, mi_page_t** ppage) { + void* p = _mi_theap_malloc_sampled(theap, req_size, zero, ppage); + if mi_unlikely(p == NULL) { + return mi_theap_new_handler(theap, req_size); + } + return p; +} +#endif + +// Fast tail calling allocation that invokes the new handler on failure (through `mi_malloc_generic_new`) +mi_decl_nodiscard mi_decl_restrict void* mi_theap_alloc_new(mi_theap_t* theap, size_t size) { + // fast path for small objects + #if MI_THEAP_INITASNULL + if mi_likely(theap!=NULL && size <= MI_SMALL_SIZE_MAX) + #else + if mi_likely(size <= MI_SMALL_SIZE_MAX) + #endif + { + return mi_theap_nonnull_xmalloc_small_zero(theap, size, false /* is wsize? */, false /* zero */, true /* is_new */, NULL); + } + else { + return mi_malloc_generic_new(theap, size + MI_PADDING_SIZE, 0, NULL); + } +} + +mi_decl_nodiscard mi_decl_restrict void* mi_new(size_t size) { + return mi_theap_alloc_new(_mi_theap_default(), size); +} + +mi_decl_nodiscard static mi_decl_noinline mi_decl_restrict void* mi_heap_init_new(mi_heap_t* heap, size_t size) { + return mi_theap_alloc_new(_mi_heap_theap_get_or_init(heap), size); +} +mi_decl_nodiscard mi_decl_restrict void* mi_heap_alloc_new(mi_heap_t* heap, size_t size) { + mi_theap_t* const theap = _mi_heap_theap_cached(heap); + if mi_likely(theap!=NULL) { return mi_theap_alloc_new(theap, size); } + else { return mi_heap_init_new(heap, size); } +} + +mi_decl_nodiscard mi_decl_restrict void* mi_theap_alloc_new_n(mi_theap_t* theap, size_t count, size_t size) { + size_t total; + if mi_unlikely(mi_count_size_overflow(count, size, &total)) { + return mi_try_new_handler_null(); // on overflow we tail invoke the try_new_handler once to potentially throw std::bad_alloc + } + else { + return mi_theap_alloc_new(theap,total); + } +} + +mi_decl_nodiscard mi_decl_restrict void* mi_new_n(size_t count, size_t size) { + return mi_theap_alloc_new_n(_mi_theap_default(), count, size); +} + +mi_decl_nodiscard static mi_decl_noinline mi_decl_restrict void* mi_heap_init_new_n(mi_heap_t* heap, size_t count, size_t size) { + return mi_theap_alloc_new_n(_mi_heap_theap_get_or_init(heap), count, size); +} +mi_decl_nodiscard mi_decl_restrict void* mi_heap_alloc_new_n(mi_heap_t* heap, size_t count, size_t size) { + mi_theap_t* const theap = _mi_heap_theap_cached(heap); + if mi_likely(theap!=NULL) { return mi_theap_alloc_new_n(theap, count, size); } + else { return mi_heap_init_new_n(heap, count, size); } +} + +mi_decl_nodiscard mi_decl_restrict void* mi_theap_alloc_new_nothrow(mi_theap_t* theap, size_t size) mi_attr_noexcept { + return mi_theap_malloc(theap,size); +} + +mi_decl_nodiscard mi_decl_restrict void* mi_new_nothrow(size_t size) mi_attr_noexcept { + return mi_malloc(size); +} + +static mi_decl_noinline void* mi_try_new_aligned(size_t size, size_t alignment) { + void* p = NULL; + for(int i = 0; i < MI_TRY_NEW_MAX && p==NULL && mi_try_new_handler(); i++) { + if (!mi_alignment_is_valid(alignment)) return NULL; + p = mi_malloc_aligned(size,alignment); + } + return p; +} + +mi_decl_nodiscard mi_decl_restrict void* mi_new_aligned(size_t size, size_t alignment) { + void* p = mi_malloc_aligned(size, alignment); + if mi_unlikely(p==NULL) return mi_try_new_aligned(size,alignment); + return p; +} + +mi_decl_nodiscard mi_decl_restrict void* mi_new_aligned_nothrow(size_t size, size_t alignment) mi_attr_noexcept { + return mi_malloc_aligned(size, alignment); +} + +static mi_decl_noinline void* mi_try_new_realloc(void* p, size_t newsize) { + void* q = NULL; + for(int i = 0; i < MI_TRY_NEW_MAX && q==NULL && mi_try_new_handler(); i++) { + if (newsize > MI_MAX_ALLOC_SIZE) return NULL; + q = mi_realloc(p,newsize); + } + return q; +} + +mi_decl_nodiscard void* mi_new_realloc(void* p, size_t newsize) { + void* q = mi_realloc(p, newsize); + if (q == NULL) return mi_try_new_realloc(p,newsize); + return q; +} + +mi_decl_nodiscard void* mi_new_reallocn(void* p, size_t newcount, size_t size) { + size_t total; + if mi_unlikely(mi_count_size_overflow(newcount, size, &total)) { + return mi_try_new_handler_null(); + } + else { + return mi_new_realloc(p, total); + } +} + +// ------------------------------------------------------ +// ensure explicit external inline definitions are emitted! +// ------------------------------------------------------ + +#ifdef __cplusplus +void* _mi_externs[] = { + (void*)&_mi_page_malloc_zero, + (void*)&_mi_theap_malloc_zero, + (void*)&mi_theap_malloc, + (void*)&mi_theap_zalloc, + (void*)&mi_theap_malloc_small, + (void*)&mi_theap_zalloc_small, + (void*)&mi_theap_calloc, + (void*)&mi_malloc, + (void*)&mi_malloc_small, + (void*)&mi_zalloc, + (void*)&mi_zalloc_small, + (void*)&mi_heap_malloc, + (void*)&mi_heap_malloc_small, + (void*)&mi_malloc_aligned + // (void*)&mi_theap_alloc_new, + // (void*)&mi_theap_alloc_new_n +}; +#endif diff --git a/Objects/mimalloc/alloc/free.c b/Objects/mimalloc/alloc/free.c new file mode 100644 index 000000000000000..7b59ddf069734c6 --- /dev/null +++ b/Objects/mimalloc/alloc/free.c @@ -0,0 +1,902 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#if !defined(MI_IN_ALLOC_C) +#error "this file should be included from 'alloc.c' (so aliases can work from alloc-override)" +#endif + +#include "mimalloc.h" +#include "mimalloc/types.h" +#include "mimalloc/prim/prim-tls.h" // _mi_prim_thread_id() +#include "mimalloc/util/stats.h" +#include "mimalloc/arena/arena-page.h" +#include "mimalloc/arena/arena.h" // _mi_arena_memid_is_suitable +#include "mimalloc/profile/sample-guarded.h" // unguard +#include "mimalloc/alloc/page.h" +#include "mimalloc/alloc/free.h" +#include "mimalloc/heap/heap.h" + + +// forward declarations +mi_decl_nodiscard static bool mi_check_padding_on_free(const mi_page_t* page, const mi_block_t* block, bool is_guarded, size_t* usable_size); +mi_decl_nodiscard static bool mi_check_double_free(const mi_page_t* page, const mi_block_t* block); +static size_t mi_page_usable_size_of(const mi_page_t* page, const mi_block_t* block, bool was_guarded); + +#if (MI_DEBUG>0) && !MI_TRACK_ENABLED && !MI_TSAN +// Fill a block with a debug pattern, preserving the first `heap->debug_fill_preserve` bytes +// (used by CPython to keep object headers intact that may be read by concurrent lock-free readers) +static inline void mi_debug_fill(const mi_page_t* page, void* block, int c, size_t size) { + const size_t preserve = mi_atomic_load_relaxed(&mi_page_heap(page)->debug_fill_preserve); + if (preserve < size) { + mi_memset((uint8_t*)block + preserve, c, size - preserve); + } +} +#endif + +// ------------------------------------------------------ +// Free +// ------------------------------------------------------ + +// regular free of a (thread local) block pointer +// fast path written carefully to prevent spilling on the stack +static inline void mi_free_block_local(mi_page_t* page, mi_block_t* block, bool was_guarded, bool check_full) +{ + // checks + size_t usable_size; + if mi_unlikely(!mi_check_padding_on_free(page, block, was_guarded, &usable_size)) return; + if mi_unlikely(!mi_check_double_free(page,block)) return; // usually checked with padding + + // mi_stat_free(page, block); + mi_track_free_size(block, usable_size); + + #if (MI_DEBUG>0) && !MI_TRACK_ENABLED && !MI_TSAN + const size_t dbgsize = (usable_size > MI_MiB ? MI_MiB : usable_size); + mi_debug_fill(page, block, MI_DEBUG_FREED, dbgsize); + #endif + + // actual free: push on the local free list fast-path + mi_used_t xused = page->xused; + mi_block_t* lfree = page->local_free; + xused.used_alloc--; + mi_block_set_next(page, block, lfree); + page->xused = xused; + page->local_free = block; + const bool is_empty = (mi_xused_used_count(xused) == 0); + mi_assert_internal(mi_page_alloc_count(page) + mi_page_last_used(page) >= mi_page_used(page)); + if mi_unlikely(is_empty) { // is used count zero ? + _mi_page_qsbr_clear(page); // a new empty interval must not reuse an earlier grace period + if (page->retire_expire==0) { // no need to re-retire retired pages (happens when we alloc/free one block repeatedly in an empty page) + _mi_page_retire(page); + } + } + else if mi_unlikely(check_full && mi_page_is_in_full(page)) { + _mi_page_unfull(page); + } +} + +// Forward declaration for multi-threaded collect +static void mi_decl_noinline mi_free_try_collect_mt(mi_page_t* page, mi_block_t* mt_free, bool allow_reclaim) mi_attr_noexcept; + +// Free a block multi-threaded +#if defined(_MSC_VER) +static mi_decl_noinline /* ensures mi_free has no stack frame */ +#else +static inline +#endif +void mi_free_block_mt(mi_page_t* page, mi_block_t* block, bool was_guarded, bool allow_reclaim) mi_attr_noexcept +{ + size_t usable_size; + if mi_unlikely(!mi_check_padding_on_free(page, block, was_guarded, &usable_size)) return; // checking padding is safe for mt + mi_track_free_size(block, usable_size); + + // _mi_padding_shrink(page, block, sizeof(mi_block_t)); + #if (MI_DEBUG>0) && !MI_TRACK_ENABLED && !MI_TSAN // note: when tracking, cannot use mi_usable_size with multi-threading + if (!was_guarded) { + const size_t dbgsize = (usable_size > MI_MiB ? MI_MiB : usable_size); + mi_debug_fill(page, block, MI_DEBUG_FREED, dbgsize); + } + #endif + + // push atomically on the page thread free list + // mi_theap_t* theap = _mi_page_associated_theap_peek(page); + mi_thread_free_t tf_new; + mi_thread_free_t tf_old = mi_atomic_load_relaxed(&page->xthread_free); + do { + mi_block_set_next(page, block, mi_tf_block(tf_old)); + tf_new = mi_tf_create(block, true); + } while (!mi_atomic_cas_weak_acq_rel(&page->xthread_free, &tf_old, tf_new)); + + // and atomically try to collect the page if it was abandoned + const bool is_newly_owned = mi_tf_is_owned(tf_new) && !mi_tf_is_owned(tf_old); + if (is_newly_owned) { + mi_assert_internal(mi_page_is_abandoned(page)); + // mi_assert_internal(!mi_page_is_abandoned_mapped(page)); + mi_free_try_collect_mt(page, block, allow_reclaim); + } +} + + +// Adjust a block that was allocated aligned, to the actual start of the block in the page. +// note: this can be called from `mi_free_generic_mt` where a non-owning thread accesses the +// `page_woffset` and `block_size` fields; however these are constant and the page won't be +// deallocated (as the block we are freeing keeps it alive) and thus safe to read concurrently. +static inline mi_block_t* mi_page_ptr_unalign_ex(const mi_page_t* page, const void* p, size_t* poffset) { + mi_assert_internal(page!=NULL && p!=NULL); + + const size_t diff = (uint8_t*)p - mi_page_start(page); + const size_t block_size = mi_page_block_size(page); + size_t adjust = diff & (block_size - 1); + if mi_unlikely(!mi_is_power_of_two(block_size)) { + adjust = diff % block_size; + } + if (poffset!=NULL) { *poffset = adjust; } + return (mi_block_t*)((uintptr_t)p - adjust); +} + +mi_block_t* _mi_page_ptr_unalign(const mi_page_t* page, const void* p) { + return mi_page_ptr_unalign_ex(page,p,NULL); +} + +static inline mi_block_t* mi_validate_block_from_ptr( const mi_page_t* page, const void* p ) { + mi_assert(_mi_page_ptr_unalign(page,p) == (mi_block_t*)p); // should never be an interior pointer + #if MI_SECURE > 0 + // in secure mode we always unalign to guard against free-ing interior pointers + return _mi_page_ptr_unalign(page,p); + #else + MI_UNUSED(page); + return (mi_block_t*)p; + #endif +} + +static inline mi_block_t* mi_page_ptr_block_check(mi_page_t* page, void* p, bool* was_guarded) mi_attr_noexcept { + MI_UNUSED(was_guarded); + if mi_likely(!mi_page_has_interior_pointers(page)) { + return mi_validate_block_from_ptr(page,p); + } + else { + size_t offset; + mi_block_t* const block = mi_page_ptr_unalign_ex(page,p,&offset); + #if MI_GUARDED || MI_PROFILE + if (offset >= sizeof(mi_block_t)) { + #if MI_PROFILE + if (block->next == MI_BLOCK_TAG_PROFILED) { + _mi_page_profile_on_free(page,block,p); + } + else + #endif + #if MI_GUARDED + if (block->next == MI_BLOCK_TAG_GUARDED) { + _mi_page_block_unguard(page, block, p); + *was_guarded = true; + } + else + #endif + { /* nothing */ } + } + #endif + return block; + } +} + +// free a local pointer (page parameter comes first for better codegen) +static void mi_decl_noinline mi_free_generic_local(mi_page_t* page, void* p) mi_attr_noexcept { + mi_assert_internal(p!=NULL && page != NULL); + bool was_guarded = false; + mi_block_t* block = mi_page_ptr_block_check(page,p,&was_guarded); + // mi_block_t* const block = (mi_page_has_interior_pointers(page) ? _mi_page_ptr_unalign(page, p) : mi_validate_block_from_ptr(page,p)); + // mi_block_check_profiled(page,block,p); + // const bool was_guarded = mi_block_check_unguard(page, block, p); + mi_free_block_local(page, block, was_guarded, true /* check for a full page */); +} + +// free a pointer owned by another thread (page parameter comes first for better codegen) +static void mi_decl_noinline mi_free_generic_mt(mi_page_t* page, void* p, bool allow_reclaim) mi_attr_noexcept { + mi_assert_internal(p!=NULL && page != NULL); + bool was_guarded = false; + mi_block_t* block = mi_page_ptr_block_check(page,p,&was_guarded); + // mi_block_t* const block = (mi_page_has_interior_pointers(page) ? _mi_page_ptr_unalign(page, p) : mi_validate_block_from_ptr(page,p)); + // mi_block_check_profiled(page,block,p); + // const bool was_guarded = mi_block_check_unguard(page, block, p); + mi_free_block_mt(page, block, was_guarded, allow_reclaim); +} + +// generic free (for runtime integration) +void mi_decl_noinline _mi_free_generic(mi_page_t* page, bool is_local, void* p) mi_attr_noexcept { + if (is_local) mi_free_generic_local(page,p); + else mi_free_generic_mt(page,p,true); +} + + +// Get the page belonging to a pointer +// Does further checks in debug mode to see if this was a valid pointer. +static mi_decl_forceinline bool mi_ptr_page_is_valid_ex(const void* p, const char* msg, bool free_small, bool check_p_for_null, mi_page_t** ppage) +{ + MI_UNUSED_RELEASE(msg); MI_UNUSED(free_small); + #if MI_DEBUG + if mi_unlikely(!mi_is_aligned(p,MI_SIZE_SIZE) && !mi_option_is_enabled(mi_option_guarded_precise)) { + _mi_error_message(EINVAL, "%s: invalid (unaligned) pointer: %p\n", msg, p); + return false; + } + #endif + + mi_page_t* page; + #if MI_PAGE_META_SMALL_IS_ALIGNED + if (free_small) { + const uintptr_t up = mi_align_down((uintptr_t)p,MI_SMALL_PAGE_SIZE); // like this for codegen on gcc + if mi_unlikely(up==0 && check_p_for_null) { return false; } + page = (mi_page_t*)up; + } + else + #endif + { + #if MI_PAGE_META_IS_ALIGNED + { page = mi_aligned_ptr_page0(p); } + #else + { page = mi_ptr_page(p); } + #endif + if mi_unlikely(page==NULL && check_p_for_null) { + #if MI_DEBUG + if (p!=NULL) { _mi_error_message(EINVAL, "%s: invalid pointer: %p\n", msg, p); } + #endif + return false; + } + } + #if MI_DEBUG + mi_page_t* const cpage = mi_checked_ptr_page(p); + if mi_unlikely(cpage==NULL) { _mi_error_message(EINVAL, "%s: invalid pointer: %p\n", msg, p); } + #endif + + #if MI_PAGE_META_IS_ALIGNED + #if MI_PAGE_META_SMALL_IS_ALIGNED + if (free_small) { mi_assert_internal(NULL == mi_atomic_load_ptr_acquire(mi_page_t,&page->self)); } + else + #elif MI_SMALL_PAGE_SIZE == MI_ARENA_SLICE_SIZE && !MI_GUARDED + // for mi_free_small we can avoid a load-acquire (but not when guarded as that may still allocate large blocks) + if (free_small) { mi_assert_internal(page == mi_atomic_load_ptr_acquire(mi_page_t,&page->self)); } + else + #endif + { + // The `self` load can be relaxed here as we free a known pointer and thus it has been synchronized. + // However, the ThreadSanitizer seems to require an acquire load to properly track memory dependencies. + #if MI_TSAN + page = mi_atomic_load_ptr_acquire(mi_page_t,&page->self); + #else + page = mi_atomic_load_ptr_relaxed(mi_page_t,&page->self); + #endif + } + #endif + + mi_assert_internal(page!=NULL); + mi_assert(cpage==page /* page_map lookup should be the same as aligned lookup */ ); + #if !MI_GUARDED + if (free_small) { mi_assert_internal(page->block_size <= MI_SMALL_MAX_OBJ_SIZE); /* free small should only be called on small pages */ } + #endif + *ppage = page; + return true; +} + +static mi_decl_forceinline bool mi_ptr_page_is_valid(const void* p, const char* msg, mi_page_t** ppage) { + return mi_ptr_page_is_valid_ex(p,msg,false /* free_small? */, true /* check_p_for_null */, ppage ); +} + +static mi_decl_forceinline mi_page_t* mi_ptr_page_validate(const void* p, const char* msg) { + mi_page_t* page; + return (mi_ptr_page_is_valid(p,msg,&page) ? page : NULL); +} + +// Free a block +// Fast path written carefully to prevent register spilling on the stack +static mi_decl_forceinline void mi_free_nonnull(void* p, mi_page_t* page, size_t* pblock_size, bool allow_reclaim) +{ + mi_assert_internal(p!=NULL && page!=NULL); + if (pblock_size!=NULL) { *pblock_size = mi_page_block_size(page); } + + const mi_threadid_t ptid = mi_page_xthread_id(page); + const mi_threadid_t xtid = (_mi_prim_thread_id() ^ ptid); + if mi_likely(xtid == 0) { // `tid == mi_page_thread_id(page) && mi_page_flags(page) == 0` + // thread-local, aligned, and not a full page + mi_block_t* const block = mi_validate_block_from_ptr(page,p); + mi_free_block_local(page, block, false /* was guarded */, false /* no need to check if the page is full */); + } + else if (xtid <= MI_PAGE_FLAG_MASK) { // `tid == mi_page_thread_id(page) && mi_page_flags(page) != 0` + // page is local, but is full or contains (inner) aligned blocks; use generic path + mi_free_generic_local(page, p); + } + // free-ing in a page owned by a theap in another thread, or an abandoned page (not belonging to a theap) + else if ((xtid & MI_PAGE_FLAG_MASK) == 0) { // `tid != mi_page_thread_id(page) && mi_page_flags(page) == 0` + // blocks are aligned (and not a full page); push on the thread_free list + mi_block_t* const block = mi_validate_block_from_ptr(page,p); + mi_free_block_mt(page,block,false /* was_guarded */, allow_reclaim); + } + else { + // page is full or contains (inner) aligned blocks; use generic multi-thread path + mi_free_generic_mt(page, p, allow_reclaim); + } +} + +void mi_free(void* p) mi_attr_noexcept { + mi_page_t* page = NULL; + if mi_likely(mi_ptr_page_is_valid(p,"mi_free",&page)) { + mi_free_nonnull(p, page, NULL, true /* allow reclaim? */); + } +} + +void mi_ufree(void* p, size_t* pblock_size) mi_attr_noexcept { + mi_page_t* page = NULL; + if mi_likely(mi_ptr_page_is_valid(p,"mi_ufree",&page)) { + mi_free_nonnull(p, page, pblock_size, true /* allow reclaim? */); + } + else { + if (pblock_size!=NULL) { *pblock_size = 0; } + } +} + +// Free a pointer that is potentially allocated in a different sub-process +void _mi_free_subproc_safe(void* p) mi_attr_noexcept { + mi_page_t* page = NULL; + if mi_likely(mi_ptr_page_is_valid(p,"_mi_free_subproc_safe",&page)) { + // const bool allow_reclaim = (_mi_subproc() == mi_page_subproc(page)); + mi_free_nonnull(p, page, NULL, false /* allow reclaim */); + } +} + +void mi_free_small(void* p) mi_attr_noexcept { + mi_page_t* page = NULL; + if mi_likely(mi_ptr_page_is_valid_ex(p,"mi_free_small",true /* is_small? */,true /*check p for null*/, &page)) { + mi_free_nonnull(p, page, NULL, true /* allow reclaim? */); + } +} + +void mi_free_small_nonnull(void* p) mi_attr_noexcept { + mi_assert(p!=NULL); + mi_page_t* page = NULL; + if mi_likely(mi_ptr_page_is_valid_ex(p,"mi_free_small_nonnull",true /* is_small? */,false /*check p for null*/, &page)) { + mi_free_nonnull(p, page, NULL, true /* allow reclaim? */); + } +} + +// For runtime systems: Free a pointer that is guaranteed to be small, and in a page owned by the current thread. +static mi_decl_forceinline void mi_free_small_local_ex(void* p, bool check_p_for_null) mi_attr_noexcept { + mi_assert(p!=NULL); + mi_page_t* page = NULL; + if mi_likely(mi_ptr_page_is_valid_ex(p,"mi_free_small_local_ex", true /* is_small? */, check_p_for_null /*check p for null*/, &page)) { + mi_assert_internal(mi_page_thread_id(page) == _mi_thread_id()); + if mi_likely(mi_page_flags(page) == 0) { + // thread-local, aligned, and not a full page + mi_block_t* const block = mi_validate_block_from_ptr(page,p); + mi_free_block_local(page, block, false /* was guarded */, false /* no need to check if the page is full */); + return; + } + else { + // page is local, but is full or contains (inner) aligned blocks; use generic path + mi_free_generic_local(page, p); + } + } +} + +void mi_free_small_local(void* p) mi_attr_noexcept { + mi_free_small_local_ex(p, true /* check null */); +} + +void mi_free_small_local_nonnull(void* p) mi_attr_noexcept { + mi_free_small_local_ex(p, false /* check null */); +} + + + +// ------------------------------------------------------ +// Free variants +// ------------------------------------------------------ + +void mi_free_size(void* p, size_t size) mi_attr_noexcept { + MI_UNUSED_RELEASE(size); + #if MI_DEBUG + const mi_page_t* const page = mi_ptr_page_validate(p,"mi_free_size"); + if (page==NULL) return; + mi_assert(p!=NULL); + const mi_block_t* block = _mi_page_ptr_unalign(page, p); + const size_t usable = _mi_page_usable_size(page,p); + if mi_unlikely(size > usable) { + const bool is_guarded = mi_block_ptr_is_guarded(block,p); + if (!is_guarded) { + _mi_error_message(EINVAL, "pointer %p is freed with 'mi_free_size' but the size %zu is greater than the usable size %zu\n", p, size, usable); + mi_free(p); + return; + } + } + else if mi_unlikely(size <= MI_SMALL_SIZE_MAX && !mi_page_is_small(page)) { + _mi_error_message(EINVAL, "pointer %p is freed with 'mi_free_size' with size %zu (<= MI_SMALL_SIZE_MAX), but it was allocated in a non-small page with block size %zu -- maybe a mismatched delete operator?\n", p, size, mi_page_block_size(page)); + mi_free(p); + return; + } + #endif + + // note: only use `mi_free_small` if small allocations are aligned. + // this allows building with `-DMI_OPT_FREE_SMALL=OFF` in case wrong sizes are passed to `mi_free_size`, see issue #1423 + #if MI_PAGE_META_SMALL_IS_ALIGNED + if mi_likely(size <= MI_SMALL_SIZE_MAX) { + mi_free_small(p); + } + else + #endif + { + mi_free(p); + } +} + +void mi_free_size_aligned(void* p, size_t size, size_t alignment) mi_attr_noexcept { + mi_assert(((uintptr_t)p % alignment) == 0); + // If the alignment is smaller than the `size`, then for `size <= MI_SMALL_SIZE_MAX` + // the block must be allocated within a small page and can thus be handled safely by `mi_free_size`. + // (since even with over-allocation the block size will be less than 2*MI_SMALL_SIZE_MAX <= MI_SMALL_OBJ_SIZE_MAX) + if mi_likely(alignment <= size) { + mi_free_size(p,size); + } + else { + mi_free(p); + } +} + +void mi_free_aligned(void* p, size_t alignment) mi_attr_noexcept { + MI_UNUSED_RELEASE(alignment); + mi_assert(((uintptr_t)p % alignment) == 0); + mi_free(p); +} + +// checked free +bool mi_cfree(void* p) mi_attr_noexcept { + mi_page_t* const page = mi_checked_ptr_page(p); + if mi_likely(page!=NULL) { + mi_free_nonnull(p, page, NULL, true /* allow reclaim? */); + return true; + } + else { + return false; + } +} + + +// -------------------------------------------------------------------------------------------- +// `mi_free_try_collect_mt`: Potentially collect a page in a free in an abandoned page. +// 1. if the page becomes empty, free it +// 2. if it can be reclaimed, reclaim it in our theap +// 3. if it went to < 7/8th used, re-abandon to be mapped (so it can be found by theaps looking for free pages) +// -------------------------------------------------------------------------------------------- + +// Consume an empty page, either freeing it or leaving it abandoned until its grace period ends. +static bool mi_abandoned_page_collect_empty(mi_page_t* page) +{ + if (!mi_page_all_free(page)) return false; + const bool should_delay = _mi_page_qsbr_should_delay(page); + // first remove it from the abandoned pages in the arena (if mapped, this might wait for any readers to finish) + _mi_arenas_page_unabandon(page,NULL); + if (!should_delay) { + _mi_page_qsbr_clear(page); + _mi_arenas_page_free(page,NULL); + } + else { + _mi_arenas_page_abandon(page,NULL); + } + return true; +} + +// Helper for mi_free_try_collect_mt: try if we can reabandon a previously abandoned mostly full page to be mapped +static bool mi_abandoned_page_try_reabandon_to_mapped(mi_page_t* page) +{ + // if the page is unmapped, try to reabandon so it can possibly be mapped and found for allocations + // We only reabandon if a full page starts to have enough blocks available to prevent immediate re-abandon of a full page + if (mi_page_is_mostly_used(page)) return false; // not too full (1/8th should be free) + if (page->memid.memkind != MI_MEM_ARENA || mi_page_is_abandoned_mapped(page)) return false; // and not already mapped (or unmappable) + + mi_assert(!mi_page_is_full(page)); + return _mi_arenas_page_try_reabandon_to_mapped(page); +} + +// Release ownership of a page. This may free or reabandoned the page if other blocks are concurrently +// freed in the meantime. Returns `true` if the page was freed. +// By passing the captured `expected_thread_free`, we can often avoid calling `mi_page_free_collect`. +static void mi_abandoned_page_unown_from_free(mi_page_t* page, mi_block_t* page_thread_free) { + mi_assert_internal(mi_page_is_owned(page)); + mi_assert_internal(mi_page_is_abandoned(page)); + mi_assert_internal(!mi_page_all_free(page)); + // try to cas atomically the original free list (`mt_free`) back with the ownership cleared. + mi_thread_free_t tf_expect = mi_tf_create(page_thread_free,true); + mi_thread_free_t tf_new = mi_tf_set_owned(tf_expect,false); + while mi_unlikely(!mi_atomic_cas_strong_acq_rel(&page->xthread_free, &tf_expect, tf_new)) { + mi_assert_internal(mi_tf_is_owned(tf_expect)); + // while the xthread_free list is not empty.. + while (mi_tf_block(tf_expect) != NULL) { + // if there were concurrent updates to the thread-free list, we retry to free or reabandon to mapped (if it became !mosty_used). + _mi_page_free_collect(page,false); // update used count + if (mi_abandoned_page_collect_empty(page)) return; + if (mi_abandoned_page_try_reabandon_to_mapped(page)) return; + // otherwise continue un-owning + tf_expect = mi_atomic_load_acquire(&page->xthread_free); + } + // and try again to release ownership + mi_subproc_stat_counter_increase(mi_page_subproc(page), pages_unown_cas_retry, 1); + mi_assert_internal(mi_tf_block(tf_expect)==NULL); + tf_new = mi_tf_create(NULL, false); + } +} + +static inline bool mi_page_queue_len_is_atmost( mi_theap_t* theap, size_t block_size, long atmost) { + if (atmost < 0) return false; + mi_page_queue_t* const pq = mi_page_queue(theap,block_size); + mi_assert_internal(pq!=NULL); + return (pq->count <= (size_t)atmost); +} + +// Helper for mi_free_try_collect_mt: try to reclaim the page for ourselves +static mi_decl_noinline bool mi_abandoned_page_try_reclaim(mi_page_t* page, long reclaim_on_free) mi_attr_noexcept +{ + // note: reclaiming can improve benchmarks like `larson` or `rbtree-ck` a lot even in the single-threaded case, + // since free-ing from an owned page avoids atomic operations. However, if we reclaim too eagerly in + // a multi-threaded scenario we may start to hold on to too much memory and reduce reuse among threads. + // If the current theap is where the page originally came from, we reclaim much more eagerly while + // 'cross-thread' reclaiming on free is by default off (and we only 'reclaim' these by finding the abandoned + // pages when we allocate a fresh page). + mi_assert_internal(mi_page_is_owned(page)); + mi_assert_internal(mi_page_is_abandoned(page)); + mi_assert_internal(!mi_page_all_free(page)); + mi_assert_internal(page->block_size <= MI_MAX_RECLAIM_OBJ_SIZE); + mi_assert_internal(reclaim_on_free >= 0); + + // dont reclaim if we just have terminated this thread and we should + // not reinitialize the theap for this thread. (can happen due to thread-local destructors for example -- issue #944) + if (!_mi_thread_is_initialized()) return false; + + // get our theap + mi_theap_t* const theap = _mi_page_associated_theap_peek(page); + if (theap==NULL || theap->tld==NULL || !theap->allow_page_reclaim) return false; // see issue #1289 + + // todo: cache `is_in_threadpool` and `exclusive_arena` directly in the theap for performance? + // set max_reclaim limit + long max_reclaim = 0; + if mi_likely(theap == page->theap) { // did this page originate from the current theap? (and thus allocated from this thread) + // originating theap + max_reclaim = _mi_option_get_fast(theap->tld->is_in_threadpool ? mi_option_page_cross_thread_max_reclaim : mi_option_page_max_reclaim); + } + else if (reclaim_on_free == 1 && // if cross-thread is allowed + !theap->tld->is_in_threadpool && // and we are not part of a threadpool + !mi_page_is_used_at_frac(page,4) && // and at least 1/4th of the page is free + _mi_arena_memid_is_suitable(page->memid, mi_theap_heap(theap)->exclusive_arena)) { // and it fits our memory + // across threads + max_reclaim = _mi_option_get_fast(mi_option_page_cross_thread_max_reclaim); + } + else { + return false; + } + + // are we within the reclaim limit? + if (max_reclaim >= 0 && !mi_page_queue_len_is_atmost(theap, page->block_size, max_reclaim)) { + return false; + } + + // reclaim the page into this theap + // first remove it from the abandoned pages in the arena -- this might wait for any readers to finish + _mi_arenas_page_unabandon(page, theap); + _mi_theap_page_reclaim(theap, page); + mi_theap_stat_counter_increase(theap, pages_reclaim_on_free, 1); + return true; +} + + +// We freed a block in an abandoned page (that was not owned). Try to collect +static void mi_decl_noinline mi_free_try_collect_mt(mi_page_t* page, mi_block_t* mt_free, bool allow_reclaim) mi_attr_noexcept +{ + mi_assert_internal(mi_page_is_owned(page)); + mi_assert_internal(mi_page_is_abandoned(page)); + mi_assert_internal(mt_free != NULL); + // mi_assert_internal(!allow_reclaim || _mi_subproc() == mi_page_subproc(page)); // never reclaim across subprocesses + + // we own the page now, and it is safe to collect the thread atomic free list + if (page->block_size <= MI_SMALL_SIZE_MAX) { + // use the `_partly` version to avoid atomic operations since we already have the `mt_free` pointing into the thread free list + // (after this the `used` count might be too high (as some blocks may have been concurrently added to the thread free list and are yet uncounted). + // however, if the page became completely free, the used count is guaranteed to be 0.) + mi_assert_internal(page->reserved>=16); // below this even one freed block goes from full to no longer mostly used. + mt_free = _mi_page_free_collect_partly(page, mt_free); + } + else { + // for larger blocks we use the regular collect + _mi_page_free_collect(page,false /* no force */); + mt_free = NULL; // expected page->xthread_free value after collection + } + const long reclaim_on_free = mi_heap_reclaim_on_free(mi_page_heap(page)); + #if MI_DEBUG > 1 + if (mi_page_is_singleton(page)) { mi_assert_internal(mi_page_all_free(page)); } + if (mi_page_is_full(page)) { mi_assert(mi_page_is_mostly_used(page)); } + #endif + + // try to: 1. free it, 2. reclaim it, or 3. reabandon it to be mapped + if (mi_abandoned_page_collect_empty(page)) return; + if (allow_reclaim && page->block_size <= MI_MAX_RECLAIM_OBJ_SIZE && reclaim_on_free >= 0) { // early test for better codegen + if (mi_abandoned_page_try_reclaim(page, reclaim_on_free)) return; + } + if (mi_abandoned_page_try_reabandon_to_mapped(page)) return; + + // otherwise unown the page again + mi_abandoned_page_unown_from_free(page, mt_free); +} + + +// ------------------------------------------------------ +// Usable size +// ------------------------------------------------------ + +// Bytes available in a block +static size_t mi_decl_noinline mi_page_usable_aligned_size_of(const mi_page_t* page, const void* p, const mi_block_t* block) mi_attr_noexcept { + // const mi_block_t* block = _mi_page_ptr_unalign(page, p); + const bool is_guarded = mi_block_ptr_is_guarded(block,p); + const size_t size = mi_page_usable_size_of(page, block, is_guarded); + mi_assert_internal((void*)p >= (void*)block); + const size_t adjust = (uint8_t*)p - (uint8_t*)block; + mi_assert_internal(adjust <= size); + const size_t aligned_size = (adjust <= size ? size - adjust : 0); // size can be zero if the padding is corrupted + return aligned_size; +} + +size_t _mi_page_usable_size(const mi_page_t* page, const void* p) mi_attr_noexcept { + if mi_unlikely(page==NULL) return 0; + mi_assert_internal(mi_ptr_page_validate(p,"_mi_page_usable_size") == page); + const mi_block_t* block = _mi_page_ptr_unalign(page, p); // for safety, always unalign + if mi_likely((const void*)block==page) { + return mi_page_usable_size_of(page, block, false /* is guarded */); + } + else { + // split out to separate routine for improved code generation + return mi_page_usable_aligned_size_of(page, p, block); + } +} + +mi_decl_nodiscard size_t mi_usable_size(const void* p) mi_attr_noexcept { + const mi_page_t* const page = mi_ptr_page_validate(p,"mi_usable_size"); + return _mi_page_usable_size(page,p); +} + +mi_decl_nodiscard size_t mi_thread_id_of(const void* p) mi_attr_noexcept { + if (p == NULL) return 0; + const mi_page_t* const page = mi_ptr_page(p); + const mi_threadid_t tid = mi_page_thread_id(page); + return (tid <= MI_THREADID_DETACHED ? 0 : tid); +} + + +// ------------------------------------------------------ +// Deprecated: double free is usually checked with padding now +// as that is faster and works better for cross-thread free'ing. +// Check for double free in secure and debug mode +// This is somewhat expensive so only enabled for secure mode 4 +// ------------------------------------------------------ + +#if MI_SECURE>=3 && !MI_PADDING +// linear check if the free list contains a specific element +static bool mi_list_contains(const mi_page_t* page, const mi_block_t* list, const mi_block_t* elem, const char* list_kind) { + const size_t max_count = page->capacity; // can never hold more blocks than the capacity + size_t count = 0; + while (list != NULL && count <= max_count) { // double-free can create cycles so we limit the number of iterations + if (elem==list) return true; + list = mi_block_next(page, list); + count++; + } + if mi_unlikely(count > max_count) { + _mi_error_message(EFAULT, "corrupted %s list (possibly due to a double free)\n", list_kind); + } + return false; +} + +static mi_decl_noinline bool mi_check_double_freex(const mi_page_t* page, const mi_block_t* block) { + // The decoded value is in the same page (or NULL). + // Walk the free lists to verify positively if it is already freed + if (mi_list_contains(page, page->free, block, "free") || + mi_list_contains(page, page->local_free, block, "local free") || + mi_list_contains(page, mi_page_thread_free(page), block, "thread free")) + { + _mi_error_message(EAGAIN, "double free detected of block %p with size %zu\n", block, mi_page_block_size(page)); + return false; + } + return true; +} + +// Used for double free checking to avoid checking free lists too frequently +static inline bool mi_block_could_be_double_free(const mi_page_t* page, const mi_block_t* block) { + mi_block_t* n = mi_block_nextx(page,block,page->keys); + return (mi_is_aligned(block,MI_SIZE_SIZE) && // quick check: aligned pointer? + (n==NULL || mi_page_contains_address(page,n))); // quick check: in the same page or NULL? +} + +// check if `block` was free'd before +static inline bool mi_check_double_free(const mi_page_t* page, const mi_block_t* block) { + if mi_unlikely(mi_block_could_be_double_free(page,block)) // quick check: next field is aligned in the same page or NULL? + { + // Suspicious: decoded value a in block is in the same page (or NULL) -- maybe a double free? + // (continue in separate function to improve code generation) + return mi_check_double_freex(page, block); + } + else return true; +} +#else +static inline bool mi_check_double_free(const mi_page_t* page, const mi_block_t* block) { + MI_UNUSED(page); + MI_UNUSED(block); + return true; +} +#endif + + +// --------------------------------------------------------------------------- +// Check for theap block overflow by setting up padding at the end of the block +// --------------------------------------------------------------------------- + +#if MI_PADDING // && !MI_TRACK_ENABLED +static inline bool mi_page_decode_padding(const mi_page_t* page, const mi_block_t* block, size_t* delta, size_t* bsize, bool* double_free) { + *bsize = mi_page_usable_block_size(page); + mi_padding_t* const padding = (mi_padding_t*)((uint8_t*)block + *bsize); + mi_track_mem_defined(padding,sizeof(mi_padding_t)); + *delta = padding->delta; + const uint32_t canary = padding->canary; + const bool ok = (mi_ptr_encode_canary(page,block,page->keys) == canary && *delta <= *bsize); + if (double_free!=NULL) { + if mi_unlikely(!ok) { *double_free = mi_ptr_decode_canary_is_freed(canary); } // double free? + else { padding->canary = mi_ptr_encode_canary_freed(); } // mark as freed + } + mi_track_mem_noaccess(padding,sizeof(mi_padding_t)); + return ok; +} + +// Return the exact usable size of a block. +static size_t mi_page_usable_size_of(const mi_page_t* page, const mi_block_t* block, bool is_guarded) { + if mi_unlikely(is_guarded) { + const size_t bsize = mi_page_block_size(page); + return (bsize - _mi_os_page_size()); + } + else { + size_t bsize; + size_t delta; + bool ok = mi_page_decode_padding(page, block, &delta, &bsize, NULL); + mi_assert_internal(ok); mi_assert_internal(delta <= bsize); + return (ok ? bsize - delta : 0); + } +} + +// When a non-thread-local block is freed, it becomes part of the thread delayed free +// list that is freed later by the owning theap. If the exact usable size is too small to +// contain the pointer for the delayed list, then shrink the padding (by decreasing delta) +// so it will later not trigger an overflow error in `mi_free_block`. +void _mi_padding_shrink(const mi_page_t* page, const mi_block_t* block, const size_t min_size) { + if (mi_block_ptr_is_guarded(block,block + 1)) return; + size_t bsize; + size_t delta; + bool ok = mi_page_decode_padding(page, block, &delta, &bsize, NULL); + mi_assert_internal(ok); + if (!ok || (bsize - delta) >= min_size) return; // usually already enough space + mi_assert_internal(bsize >= min_size); + if (bsize < min_size) return; // should never happen + size_t new_delta = (bsize - min_size); + mi_assert_internal(new_delta < bsize); + mi_padding_t* padding = (mi_padding_t*)((uint8_t*)block + bsize); + mi_track_mem_defined(padding,sizeof(mi_padding_t)); + padding->delta = (uint32_t)new_delta; + mi_track_mem_noaccess(padding,sizeof(mi_padding_t)); +} +#else +static inline size_t mi_page_usable_size_of(const mi_page_t* page, const mi_block_t* block, bool is_guarded) { + MI_UNUSED(block); + if mi_unlikely(is_guarded) { + const size_t bsize = mi_page_block_size(page); + return (bsize - _mi_os_page_size()); + } + else { + return mi_page_usable_block_size(page); + } +} + +void _mi_padding_shrink(const mi_page_t* page, const mi_block_t* block, const size_t min_size) { + MI_UNUSED(page); MI_UNUSED(block); MI_UNUSED(min_size); +} +#endif + +#if MI_PADDING + +static bool mi_verify_padding(const mi_page_t* page, const mi_block_t* block, size_t* size, size_t* wrong, bool* is_double_free) { + size_t bsize; + size_t delta; + bool ok = mi_page_decode_padding(page, block, &delta, &bsize, is_double_free ); + *size = *wrong = bsize; + if (!ok) return false; + mi_assert_internal(bsize >= delta); + *size = bsize - delta; + #if MI_PADDING_CHECK_BYTES + if (!mi_page_is_huge(page)) { + uint8_t* fill = (uint8_t*)block + bsize - delta; + const size_t maxpad = (delta > MI_MAX_ALIGN_SIZE ? MI_MAX_ALIGN_SIZE : delta); // check at most the first N padding bytes + mi_track_mem_defined(fill, maxpad); + for (size_t i = 0; i < maxpad; i++) { + if (fill[i] != MI_DEBUG_PADDING) { + *wrong = bsize - delta + i; + ok = false; + break; + } + } + mi_track_mem_noaccess(fill, maxpad); + } + #endif + return ok; +} + +mi_decl_nodiscard static bool mi_check_padding_on_free(const mi_page_t* page, const mi_block_t* block, bool is_guarded, size_t* usable_size) { + if mi_unlikely(is_guarded) { + const size_t bsize = mi_page_block_size(page); + *usable_size = (bsize - _mi_os_page_size()); + return true; + } + else { + size_t wrong; + bool is_double_free; + if mi_unlikely(!mi_verify_padding(page,block,usable_size,&wrong,&is_double_free)) { + if (is_double_free) { + _mi_error_message(EAGAIN, "double free detected of heap block %p with size %zu\n", block, *usable_size); + } + else { + _mi_error_message(EFAULT, "buffer overflow in heap block %p of size %zu: write after %zu bytes\n", block, *usable_size, wrong ); + } + return false; + } + return true; + } +} + +#else + +mi_decl_nodiscard static bool mi_check_padding_on_free(const mi_page_t* page, const mi_block_t* block, bool is_guarded, size_t* usable_size) { + *usable_size = mi_page_usable_size_of(page,block,is_guarded); + return true; +} + +#endif + +// only maintain stats for smaller objects if requested +#if 0 && (MI_STATS>0) +static void mi_stat_free(const mi_page_t* page, const mi_block_t* block) { + MI_UNUSED(block); + mi_theap_t* theap = _mi_theap_default(); + mi_lock_t* lock = NULL; + mi_subproc_t* const subproc = mi_page_subproc(page); + mi_theap_t* const theap_meta = subproc->theap_meta; + if mi_unlikely(!mi_theap_is_initialized(theap) || // can happen if free'd after thread_done was called (usually a thread cleanup call by the OS) + // page->theap == subproc->theap_meta .. but we cannot read `theap` if we don't own the page + (theap_meta != NULL && mi_page_thread_id(page) == theap_meta->tld->thread_id)) { + theap = theap_meta; + lock = &subproc->theap_meta_lock; + mi_lock_acquire(lock); + } + + const size_t bsize = mi_page_usable_block_size(page); + // #if (MI_STATS>=2) + // const size_t usize = mi_page_usable_size_of(page, block); + // mi_theap_stat_decrease(theap, malloc_requested, usize); + // #endif + if (bsize <= MI_LARGE_MAX_OBJ_SIZE) { + mi_theap_stat_decrease(theap, malloc_normal, bsize); + #if (MI_STATS > 1) + mi_theap_stat_decrease(theap, malloc_bins[_mi_bin(bsize)], 1); + #endif + } + else { + const size_t bpsize = mi_page_block_size(page); // match stat in page.c:mi_huge_page_alloc + mi_theap_stat_decrease(theap, malloc_huge, bpsize); + } + + if mi_unlikely(lock!=NULL) { + mi_lock_release(lock); + } +} +#else +void mi_stat_free(const mi_page_t* page, const mi_block_t* block) { + MI_UNUSED(page); MI_UNUSED(block); +} +#endif diff --git a/Objects/mimalloc/alloc/page-queue.c b/Objects/mimalloc/alloc/page-queue.c new file mode 100644 index 000000000000000..a9d8c42e3007bc5 --- /dev/null +++ b/Objects/mimalloc/alloc/page-queue.c @@ -0,0 +1,400 @@ +/*---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +/* ----------------------------------------------------------- + Definition of page queues for each block size +----------------------------------------------------------- */ + +#include "mimalloc.h" +#include "mimalloc/types.h" +#include "page-queue.h" + +/* ----------------------------------------------------------- + Minimal alignment in machine words (i.e. `sizeof(void*)`) +----------------------------------------------------------- */ + +#if (MI_MAX_ALIGN_SIZE > 4*MI_SIZE_SIZE) + #error "define alignment for more than 4x word size for this platform" +#elif (MI_MAX_ALIGN_SIZE > 2*MI_SIZE_SIZE) + #define MI_ALIGN4W // 4 machine words minimal alignment +#elif (MI_MAX_ALIGN_SIZE > MI_SIZE_SIZE) + #define MI_ALIGN2W // 2 machine words minimal alignment +#else + // ok, default alignment is 1 word +#endif + + +/* ----------------------------------------------------------- + Bins +----------------------------------------------------------- */ + +// Return the bin for a given field size. +// Returns MI_BIN_HUGE if the size is too large. +// We use `wsize` for the size in "machine word sizes", +// i.e. byte size == `wsize*sizeof(void*)`. +static size_t mi_bin(size_t size) { + size_t wsize = mi_wsize_from_size(size); +#if defined(MI_ALIGN4W) + if mi_likely(wsize <= 4) { + return (wsize <= 1 ? 1 : (wsize+1)&~1); // round to double word sizes + } +#elif defined(MI_ALIGN2W) + if mi_likely(wsize <= 8) { + return (wsize <= 1 ? 1 : (wsize+1)&~1); // round to double word sizes + } +#else + if mi_likely(wsize <= 8) { + return (wsize == 0 ? 1 : wsize); + } +#endif + else if mi_unlikely(wsize > MI_LARGE_MAX_OBJ_WSIZE) { + return MI_BIN_HUGE; + } + else { + #if defined(MI_ALIGN4W) + if (wsize <= 16) { wsize = (wsize+3)&~3; } // round to 4x word sizes + #endif + wsize--; + // find the highest bit + const size_t b = (MI_SIZE_BITS - 1 - mi_clz(wsize)); // note: wsize != 0 + // and use the top 3 bits to determine the bin (~12.5% worst internal fragmentation). + // - adjust with 3 because we use do not round the first 8 sizes + // which each get an exact bin + const size_t bin = ((b << 2) + ((wsize >> (b - 2)) & 0x03)) - 3; + mi_assert_internal(bin > 0 && bin < MI_BIN_HUGE); + return bin; + } +} + + + +/* ----------------------------------------------------------- + Queue of pages with free blocks +----------------------------------------------------------- */ + +size_t _mi_bin(size_t size) { + return mi_bin(size); +} + +size_t _mi_bin_size(size_t bin) { + mi_assert_internal(bin <= MI_BIN_HUGE); + return _mi_theap_empty.pages[bin].block_size; +} + +// Good size for allocation +mi_decl_nodiscard mi_decl_export size_t mi_good_size(size_t size) mi_attr_noexcept { + if (size <= MI_LARGE_MAX_OBJ_SIZE - MI_PADDING_SIZE) { + return _mi_bin_size(mi_bin(size + MI_PADDING_SIZE)); + } + else if (size <= MI_MAX_ALLOC_SIZE - MI_PADDING_SIZE) { + return mi_align_up(size + MI_PADDING_SIZE,_mi_os_page_size()); + } + else { + return size; + } +} + +#if (MI_DEBUG>1) +bool mi_page_queue_contains(mi_page_queue_t* queue, const mi_page_t* page) { + mi_assert_internal(page != NULL); + mi_page_t* list = queue->first; + while (list != NULL) { + mi_assert_internal(list->next == NULL || list->next->prev == list); + mi_assert_internal(list->prev == NULL || list->prev->next == list); + if (list == page) break; + list = list->next; + } + return (list == page); +} + +#endif + +bool _mi_page_queue_is_valid(mi_theap_t* theap, const mi_page_queue_t* pq) { + MI_UNUSED_RELEASE(theap); + if (pq==NULL) return false; + size_t count = 0; MI_UNUSED_RELEASE(count); + mi_page_t* prev = NULL; MI_UNUSED_RELEASE(prev); + for (mi_page_t* page = pq->first; page != NULL; page = page->next) { + mi_assert_internal(page->prev == prev); + if (mi_page_is_in_full(page)) { + mi_assert_internal(mi_wsize_from_size(pq->block_size) == MI_LARGE_MAX_OBJ_WSIZE + 2); + } + else if (mi_page_is_huge(page)) { + mi_assert_internal(mi_wsize_from_size(pq->block_size) == MI_LARGE_MAX_OBJ_WSIZE + 1); + } + else { + mi_assert_internal(mi_page_block_size(page) == pq->block_size); + } + mi_assert_internal(page->theap == theap); + if (page->next == NULL) { + mi_assert_internal(pq->last == page); + } + count++; + prev = page; + } + mi_assert_internal(pq->count == count); + return true; +} + +static size_t mi_page_bin(const mi_page_t* page) { + const size_t bin = (mi_page_is_in_full(page) ? MI_BIN_FULL : (mi_page_is_huge(page) ? MI_BIN_HUGE : mi_bin(mi_page_block_size(page)))); + mi_assert_internal(bin <= MI_BIN_FULL); + return bin; +} + +// returns the page bin without using MI_BIN_FULL for statistics +size_t _mi_page_stats_bin(const mi_page_t* page) { + const size_t bin = (mi_page_is_huge(page) ? MI_BIN_HUGE : mi_bin(mi_page_block_size(page))); + mi_assert_internal(bin <= MI_BIN_HUGE); + return bin; +} + +mi_page_queue_t* mi_theap_page_queue_of(mi_theap_t* theap, const mi_page_t* page) { + mi_assert_internal(theap!=NULL); + const size_t bin = mi_page_bin(page); + mi_page_queue_t* pq = &theap->pages[bin]; + mi_assert_internal((mi_page_block_size(page) == pq->block_size) || + (mi_page_is_huge(page) && mi_page_queue_is_huge(pq)) || + (mi_page_is_in_full(page) && mi_page_queue_is_full(pq))); + return pq; +} + +// The current small page array is for efficiency and for each +// small size (up to 256) it points directly to the page for that +// size without having to compute the bin. This means when the +// current free page queue is updated for a small bin, we need to update a +// range of entries in `_mi_page_small_free`. +static inline void mi_theap_queue_first_update(mi_theap_t* theap, const mi_page_queue_t* pq) { + mi_assert_internal(mi_theap_contains_queue(theap,pq)); + const size_t size = pq->block_size; + if (size > MI_SMALL_SIZE_MAX) return; + + mi_page_t* page = pq->first; + if (pq->first == NULL) page = _mi_page_empty_get(); + + // find index in the right direct page array + const size_t idx = mi_wsize_from_size(size); + mi_page_t** const pages_free = theap->pages_free_direct; + if (pages_free[idx] == page) return; // already set + + // find start slot + size_t start; + if (idx<=1) { + start = 0; + } + else { + // find previous size; due to minimal alignment upto 3 previous bins may need to be skipped + mi_assert_internal(pq > &theap->pages[0]); // since idx > 1 + size_t bin = mi_bin(size); + const mi_page_queue_t* prev = pq - 1; + while( bin == mi_bin(prev->block_size) && prev > &theap->pages[0]) { + prev--; + } + start = 1 + mi_wsize_from_size(prev->block_size); + if (start > idx) start = idx; + } + + // set size range to the right page + mi_assert(start <= idx); + for (size_t sz = start; sz <= idx; sz++) { + pages_free[sz] = page; + } +} + +/* +static bool mi_page_queue_is_empty(mi_page_queue_t* queue) { + return (queue->first == NULL); +} +*/ + +void mi_page_queue_remove(mi_page_queue_t* queue, mi_page_t* page) { + mi_assert_internal(page != NULL); + mi_assert_expensive(mi_page_queue_contains(queue, page)); + mi_assert_internal(queue->count >= 1); + mi_assert_internal(mi_page_block_size(page) == queue->block_size || + (mi_page_is_huge(page) && mi_page_queue_is_huge(queue)) || + (mi_page_is_in_full(page) && mi_page_queue_is_full(queue))); + mi_theap_t* theap = mi_page_theap(page); + if (page->prev != NULL) page->prev->next = page->next; + if (page->next != NULL) page->next->prev = page->prev; + if (page == queue->last) queue->last = page->prev; + if (page == queue->first) { + queue->first = page->next; + // update first + mi_assert_internal(mi_theap_contains_queue(theap, queue)); + mi_theap_queue_first_update(theap,queue); + } + theap->page_count--; + if (page->memid.memkind != MI_MEM_ARENA) { + mi_assert_internal(theap->page_count_os > 0); + theap->page_count_os--; + } + queue->count--; + page->next = NULL; + page->prev = NULL; + mi_page_set_in_full(page,false); +} + + +void mi_page_queue_push(mi_theap_t* theap, mi_page_queue_t* queue, mi_page_t* page) { + mi_assert_internal(mi_page_theap(page) == theap); + mi_assert_internal(!mi_page_queue_contains(queue, page)); + #if MI_HUGE_PAGE_ABANDON + mi_assert_internal(_mi_page_segment(page)->page_kind != MI_PAGE_HUGE); + #endif + mi_assert_internal(mi_page_block_size(page) == queue->block_size || + (mi_page_is_huge(page) && mi_page_queue_is_huge(queue)) || + (mi_page_is_in_full(page) && mi_page_queue_is_full(queue))); + + mi_page_set_in_full(page, mi_page_queue_is_full(queue)); + + page->next = queue->first; + page->prev = NULL; + if (queue->first != NULL) { + mi_assert_internal(queue->first->prev == NULL); + queue->first->prev = page; + queue->first = page; + } + else { + queue->first = queue->last = page; + } + queue->count++; + + // update direct + mi_theap_queue_first_update(theap, queue); + theap->page_count++; + if (page->memid.memkind != MI_MEM_ARENA) { theap->page_count_os++; } +} + +void mi_page_queue_push_at_end(mi_theap_t* theap, mi_page_queue_t* queue, mi_page_t* page) { + mi_assert_internal(mi_page_theap(page) == theap); + mi_assert_internal(!mi_page_queue_contains(queue, page)); + + mi_assert_internal(mi_page_block_size(page) == queue->block_size || + (mi_page_is_huge(page) && mi_page_queue_is_huge(queue)) || + (mi_page_is_in_full(page) && mi_page_queue_is_full(queue))); + + mi_page_set_in_full(page, mi_page_queue_is_full(queue)); + + page->prev = queue->last; + page->next = NULL; + if (queue->last != NULL) { + mi_assert_internal(queue->last->next == NULL); + queue->last->next = page; + queue->last = page; + } + else { + queue->first = queue->last = page; + } + queue->count++; + + // update direct + if (queue->first == page) { + mi_theap_queue_first_update(theap, queue); + } + theap->page_count++; + if (page->memid.memkind != MI_MEM_ARENA) { theap->page_count_os++; } +} + +static void mi_page_queue_enqueue_from_ex(mi_page_queue_t* to, mi_page_queue_t* from, bool enqueue_at_end, mi_page_t* page) { + mi_assert_internal(page != NULL); + mi_assert_internal(from->count >= 1); + mi_assert_expensive(mi_page_queue_contains(from, page)); + mi_assert_expensive(!mi_page_queue_contains(to, page)); + const size_t bsize = mi_page_block_size(page); + MI_UNUSED(bsize); + mi_assert_internal((bsize == to->block_size && bsize == from->block_size) || + (bsize == to->block_size && mi_page_queue_is_full(from)) || + (bsize == from->block_size && mi_page_queue_is_full(to)) || + (mi_page_is_huge(page) && mi_page_queue_is_huge(to)) || + (mi_page_is_huge(page) && mi_page_queue_is_full(to))); + + mi_theap_t* theap = mi_page_theap(page); + + // delete from `from` + if (page->prev != NULL) page->prev->next = page->next; + if (page->next != NULL) page->next->prev = page->prev; + if (page == from->last) from->last = page->prev; + if (page == from->first) { + from->first = page->next; + // update first + mi_assert_internal(mi_theap_contains_queue(theap, from)); + mi_theap_queue_first_update(theap, from); + } + from->count--; + + // insert into `to` + to->count++; + if (enqueue_at_end) { + // enqueue at the end + page->prev = to->last; + page->next = NULL; + if (to->last != NULL) { + mi_assert_internal(theap == mi_page_theap(to->last)); + to->last->next = page; + to->last = page; + } + else { + to->first = page; + to->last = page; + mi_theap_queue_first_update(theap, to); + } + } + else { + if (to->first != NULL) { + // enqueue at 2nd place + mi_assert_internal(theap == mi_page_theap(to->first)); + mi_page_t* next = to->first->next; + page->prev = to->first; + page->next = next; + to->first->next = page; + if (next != NULL) { + next->prev = page; + } + else { + to->last = page; + } + } + else { + // enqueue at the head (singleton list) + page->prev = NULL; + page->next = NULL; + to->first = page; + to->last = page; + mi_theap_queue_first_update(theap, to); + } + } + + mi_page_set_in_full(page, mi_page_queue_is_full(to)); +} + +void mi_page_queue_enqueue_from(mi_page_queue_t* to, mi_page_queue_t* from, mi_page_t* page) { + mi_page_queue_enqueue_from_ex(to, from, true /* enqueue at the end */, page); +} + +void mi_page_queue_enqueue_from_full(mi_page_queue_t* to, mi_page_queue_t* from, mi_page_t* page) { + // note: we could insert at the front to increase reuse, but it slows down certain benchmarks (like `alloc-test`) + mi_page_queue_enqueue_from_ex(to, from, true /* enqueue at the end of the `to` queue? */, page); +} + +void mi_page_queue_move_to_front(mi_theap_t* theap, mi_page_queue_t* queue, mi_page_t* page) { + mi_assert_internal(mi_page_theap(page) == theap); + mi_assert_internal(mi_page_queue_contains(queue, page)); + if (queue->first == page) return; + mi_page_queue_remove(queue, page); + mi_page_queue_push(theap, queue, page); + mi_assert_internal(queue->first == page); +} + +void mi_page_queue_move_to_back(mi_theap_t* theap, mi_page_queue_t* queue, mi_page_t* page) { + mi_assert_internal(mi_page_theap(page) == theap); + mi_assert_internal(mi_page_queue_contains(queue, page)); + if (queue->last == page) return; + mi_page_queue_remove(queue, page); + mi_page_queue_push_at_end(theap, queue, page); + mi_assert_internal(queue->last == page); +} diff --git a/Objects/mimalloc/alloc/page-queue.h b/Objects/mimalloc/alloc/page-queue.h new file mode 100644 index 000000000000000..3a7096d3b685320 --- /dev/null +++ b/Objects/mimalloc/alloc/page-queue.h @@ -0,0 +1,57 @@ +/*---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +#pragma once +#ifndef MI_PAGE_QUEUE_H +#define MI_PAGE_QUEUE_H + +#include "mimalloc.h" +#include "mimalloc/types.h" +#include "mimalloc/alloc/page.h" + +#if (MI_DEBUG>1) +bool mi_page_queue_contains(mi_page_queue_t* queue, const mi_page_t* page); + +static inline bool mi_theap_contains_queue(const mi_theap_t* theap, const mi_page_queue_t* pq) { + return (pq >= &theap->pages[0] && pq <= &theap->pages[MI_BIN_FULL]); +} +#endif + +mi_page_queue_t* mi_theap_page_queue_of(mi_theap_t* theap, const mi_page_t* page); +void mi_page_queue_remove(mi_page_queue_t* queue, mi_page_t* page); +void mi_page_queue_push(mi_theap_t* theap, mi_page_queue_t* queue, mi_page_t* page); +void mi_page_queue_push_at_end(mi_theap_t* theap, mi_page_queue_t* queue, mi_page_t* page); +void mi_page_queue_enqueue_from(mi_page_queue_t* to, mi_page_queue_t* from, mi_page_t* page); +void mi_page_queue_enqueue_from_full(mi_page_queue_t* to, mi_page_queue_t* from, mi_page_t* page); +void mi_page_queue_move_to_front(mi_theap_t* theap, mi_page_queue_t* queue, mi_page_t* page); +void mi_page_queue_move_to_back(mi_theap_t* theap, mi_page_queue_t* queue, mi_page_t* page); + + +static inline bool mi_page_queue_is_huge(const mi_page_queue_t* pq) { + return (pq->block_size == (MI_LARGE_MAX_OBJ_SIZE+sizeof(uintptr_t))); +} + +static inline bool mi_page_queue_is_full(const mi_page_queue_t* pq) { + return (pq->block_size == (MI_LARGE_MAX_OBJ_SIZE+(2*sizeof(uintptr_t)))); +} + +static inline bool mi_page_queue_is_special(const mi_page_queue_t* pq) { + return (pq->block_size > MI_LARGE_MAX_OBJ_SIZE); +} + +static inline size_t mi_page_queue_count(const mi_page_queue_t* pq) { + return pq->count; +} + +static inline mi_page_queue_t* mi_page_queue_of(const mi_page_t* page) { + mi_theap_t* theap = mi_page_theap(page); + mi_page_queue_t* pq = mi_theap_page_queue_of(theap, page); + mi_assert_expensive(mi_page_queue_contains(pq, page)); + return pq; +} + +#endif diff --git a/Objects/mimalloc/alloc/page.c b/Objects/mimalloc/alloc/page.c new file mode 100644 index 000000000000000..f738197ca0fae56 --- /dev/null +++ b/Objects/mimalloc/alloc/page.c @@ -0,0 +1,956 @@ +/*---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +#include "mimalloc.h" +#include "mimalloc/types.h" +#include "mimalloc/prim/prim-tls.h" +#include "mimalloc/util/random.h" +#include "mimalloc/util/stats.h" +#include "mimalloc/arena/arena-page.h" +#include "mimalloc/alloc/page.h" +#include "page-queue.h" +#include "page.h" + + +/* ----------------------------------------------------------- + Page invariants +----------------------------------------------------------- */ + +// Index a block in a page +static inline mi_block_t* mi_page_block_at(const mi_page_t* page, void* page_start, size_t block_size, size_t i) { + MI_UNUSED(page); + mi_assert_internal(page != NULL); + mi_assert_internal(i <= page->reserved); + return (mi_block_t*)((uint8_t*)page_start + (i * block_size)); +} + +#if (MI_DEBUG>=3) +static size_t mi_page_list_count(mi_page_t* page, mi_block_t* head) { + mi_assert_internal(mi_ptr_page(mi_page_start(page)) == page); + const uint8_t* slice_start = mi_page_slice_start(page); + mi_assert_internal(mi_is_aligned(slice_start,MI_PAGE_ALIGN)); + size_t count = 0; + while (head != NULL) { + mi_assert_internal((uint8_t*)head - slice_start > (ptrdiff_t)MI_LARGE_PAGE_SIZE || page == mi_ptr_page(head)); + count++; + head = mi_block_next(page, head); + } + return count; +} + +/* +// Start of the page available memory +static inline uint8_t* mi_page_area(const mi_page_t* page) { + return _mi_page_start(_mi_page_segment(page), page, NULL); +} +*/ + +static bool mi_page_list_is_valid(mi_page_t* page, mi_block_t* p) { + size_t psize; + uint8_t* page_area = mi_page_area(page, &psize); + mi_block_t* start = (mi_block_t*)page_area; + mi_block_t* end = (mi_block_t*)(page_area + psize); + while(p != NULL) { + if (p < start || p >= end) return false; + p = mi_block_next(page, p); + } +#if MI_DEBUG>3 // generally too expensive to check this + if (page->free_is_zero) { + const size_t ubsize = mi_page_usable_block_size(page); + for (mi_block_t* block = page->free; block != NULL; block = mi_block_next(page, block)) { + mi_assert_expensive(mi_mem_is_zero(block + 1, ubsize - sizeof(mi_block_t))); + } + } +#endif + return true; +} + +static bool mi_page_is_valid_init(mi_page_t* page) { + mi_assert_internal(mi_page_block_size(page) > 0); + mi_assert_internal(mi_page_used(page) <= page->capacity); + mi_assert_internal(page->capacity <= page->reserved); + + mi_assert_internal(page->heap!=NULL); + mi_theap_t* const page_theap = _mi_heap_theap_peek(page->heap); + mi_assert_internal(page_theap == NULL || mi_page_theap(page)==page_theap || + mi_page_theap(page)->tld == NULL || mi_page_theap(page)->tld->thread_id == MI_THREADID_DETACHED); + + // const size_t bsize = mi_page_block_size(page); + // uint8_t* start = mi_page_start(page); + //mi_assert_internal(start + page->capacity*page->block_size == page->top); + + mi_assert_internal(mi_page_list_is_valid(page,page->free)); + mi_assert_internal(mi_page_list_is_valid(page,page->local_free)); + + #if MI_DEBUG>3 // generally too expensive to check this + if (page->free_is_zero) { + const size_t ubsize = mi_page_usable_block_size(page); + for(mi_block_t* block = page->free; block != NULL; block = mi_block_next(page,block)) { + mi_assert_expensive(mi_mem_is_zero(block + 1, ubsize - sizeof(mi_block_t))); + } + } + #endif + + #if !MI_TRACK_ENABLED && !MI_TSAN + mi_block_t* tfree = mi_page_thread_free(page); + mi_assert_internal(mi_page_list_is_valid(page, tfree)); + //size_t tfree_count = mi_page_list_count(page, tfree); + //mi_assert_internal(tfree_count <= page->thread_freed + 1); + #endif + + size_t free_count = mi_page_list_count(page, page->free) + mi_page_list_count(page, page->local_free); + mi_assert_internal(mi_page_used(page) + free_count == page->capacity); + + mi_assert_internal(mi_page_alloc_count(page) + mi_page_last_used(page) >= mi_page_used(page)); + return true; +} + +extern mi_decl_hidden bool _mi_process_is_initialized; // has mi_process_init been called? + +bool _mi_page_is_valid(mi_page_t* page) { + mi_assert_internal(mi_page_is_valid_init(page)); + #if MI_SECURE + mi_assert_internal(page->keys[0] != 0); + #endif + if (!mi_page_is_abandoned(page)) { + //mi_assert_internal(!_mi_process_is_initialized); + mi_assert_internal(page->heap!=NULL); + mi_theap_t* const page_theap = _mi_heap_theap_peek(page->heap); + mi_assert_internal(page_theap == NULL || mi_page_theap(page)==page_theap || + mi_page_theap(page)->tld == NULL || mi_page_theap(page)->tld->thread_id == MI_THREADID_DETACHED); + { + mi_page_queue_t* pq = mi_page_queue_of(page); + mi_assert_internal(mi_page_queue_contains(pq, page)); + mi_assert_internal(pq->block_size==mi_page_block_size(page) || mi_page_is_huge(page) || mi_page_is_in_full(page)); + // mi_assert_internal(mi_theap_contains_queue(mi_page_theap(page),pq)); + } + } + mi_assert_internal(mi_page_alloc_count(page) + mi_page_last_used(page) >= mi_page_used(page)); + mi_assert_internal(mi_page_alloc_count(page) >= mi_page_last_alloc(page)); + return true; +} +#endif + +/* ----------------------------------------------------------- + Page statistics +----------------------------------------------------------- */ + + +#if MI_STATS || (MI_SAMPLE==1) +// Gets the theap belonging to a page. +static mi_theap_t* mi_theap_of_page(mi_page_t* page) { + mi_theap_t* theap = page->theap; + if mi_unlikely(mi_page_thread_id(page) != _mi_prim_thread_id()) { + theap = _mi_page_associated_theap_peek(page); + } + return (theap != NULL && theap->tld != NULL ? theap : NULL); +} +#endif + +#if MI_SAMPLE==1 /* for ==2, the countdown is already done at every `alloc/alloc.c:mi_page_alloc_zero` */ + +// Adjust the theap->sample_countdown based on the current page allocation count +// in the `mi_malloc_generic` function this is countdown is checked +static void mi_theap_adjust_sample_countdown(mi_theap_t* theap, mi_page_t* page, size_t alloc_count) +{ + mi_assert_internal(mi_page_alloc_count(page) + mi_page_last_used(page) >= mi_page_used(page)); + mi_assert_internal(theap!=NULL); + if (theap->sample_rate==0) return; + + const size_t last_alloc = mi_page_last_alloc(page); + if (alloc_count <= last_alloc) return; + + // update countdown + const size_t bsize = mi_page_usable_block_size(page); mi_assert_internal(bsize >= MI_PADDING_SIZE); + const size_t alloc_diff = alloc_count - last_alloc; + const uint64_t requested = (uint64_t)alloc_diff * (uint64_t)(bsize - MI_PADDING_SIZE); + if (requested <= SIZE_MAX && theap->sample_countdown >= (size_t)requested) { + theap->sample_countdown -= (size_t)requested; + } + else { + theap->sample_requested += (requested - theap->sample_countdown); + theap->sample_countdown = 0; + } +} +#endif + +#if MI_STATS +// Merge stats from the page into the corresponding theap (or heap if theapx==NULL). +static void mi_theap_page_merge_stats(mi_theap_t* theapx, const mi_page_t* page, size_t alloc_count, size_t free_count ) { + // get heap (as the theap might be NULL) + mi_heap_t* const heap = mi_page_heap(page); + mi_assert_internal(theapx == NULL || (theapx->tld != NULL && _mi_thread_id() == theapx->tld->thread_id)); + mi_theapx_stat_counter_increase(heap,theapx,pages_stat_updates,1); + mi_theapx_stat_counter_increase(heap,theapx,pages_stat_update_count, alloc_count + free_count); + + // allocation sizes + const size_t bsize = mi_page_usable_block_size(page); + const uint64_t allocated = (uint64_t)alloc_count * (uint64_t)bsize; + const uint64_t freed = (uint64_t)free_count * (uint64_t)bsize; + #if MI_STATS==1 + const uint64_t requested = allocated - ((uint64_t)alloc_count * MI_PADDING_SIZE); + #endif + mi_assert_internal(allocated <= INT64_MAX); // safe to cast to int64_t for stats + mi_assert_internal(freed <= INT64_MAX); + + // adjust stats + const size_t bin = _mi_bin(bsize); + if (alloc_count > 0) { + mi_theapx_stat_counter_increase(heap, theapx, malloc_count, alloc_count); + mi_theapx_stat_increase(heap, theapx, allocated, allocated); + mi_theapx_stat_increase(heap, theapx, malloc_bins[bin], alloc_count); + if (bin==MI_BIN_HUGE) { mi_theapx_stat_increase(heap,theapx,malloc_huge,allocated); } // separate so we have a count and total + #if MI_STATS==1 + // use coarse total requested bytes + mi_theapx_stat_counter_increase(heap, theapx, malloc_requested, requested); + #endif + // deprecated + if (bin!=MI_BIN_HUGE) { + mi_theapx_stat_counter_increase(heap, theapx, malloc_normal_count, alloc_count); + mi_theapx_stat_increase(heap, theapx, malloc_normal, allocated); + } + else { + mi_theapx_stat_counter_increase(heap, theapx, malloc_huge_count, alloc_count); + } + } + if (free_count > 0) { + mi_theapx_stat_counter_increase(heap, theapx, free_count, free_count); + mi_theapx_stat_counter_increase(heap, theapx, freed, freed); + mi_theapx_stat_decrease(heap, theapx, allocated, freed); + mi_theapx_stat_decrease(heap, theapx, malloc_bins[bin], free_count); + if (bin==MI_BIN_HUGE) { mi_theapx_stat_decrease(heap, theapx, malloc_huge, freed); } + // deprecated + if (bin!=MI_BIN_HUGE) { mi_theapx_stat_decrease(heap, theapx, malloc_normal, freed); } + } + + // merge the theap stats into the heap once N bytes were allocated or freed since the last merge + if (theapx != NULL) { + const size_t threshold = mi_option_get_size(mi_option_stats_merge_threshold); + const int64_t churn = theapx->stats.allocated.total + theapx->stats.freed.total; + if (threshold > 0 && churn >= (int64_t)threshold) { + _mi_theap_merge_stats(theapx); + } + } +} + +// Update stats for a page (and sample countdown). Can be called with a NULL `theapx` +static void mi_theap_page_update_stats(mi_theap_t* theapx, mi_page_t* page) { + mi_assert_internal(mi_page_alloc_count(page) + mi_page_last_used(page) >= mi_page_used(page)); + mi_assert_internal(mi_page_alloc_count(page) >= mi_page_last_alloc(page)); + + // get stat counts + const size_t used = mi_page_used(page); + const size_t alloc_count = mi_page_alloc_count(page); + const size_t last_used = mi_page_last_used(page); + mi_assert_internal(last_used + alloc_count >= used); + mi_assert_internal(used <= UINT16_MAX); + const size_t free_count = last_used + alloc_count - used; + + #if MI_SAMPLE==1 // for ==2 it is already counted in every `alloc/alloc.c:mi_page_alloc_zero` + if (theapx!=NULL) { mi_theap_adjust_sample_countdown(theapx,page,alloc_count); } + #endif + + // update stats? + if (alloc_count + free_count == 0) { + mi_assert_internal(last_used == used); + return; + } + + // reset the `alloc_count` (and `last_alloc`), and set `last_used` to `used` + #if MI_SIZE_SIZE >= 8 + page->xused.used_alloc = (used << 32) | used; + #else + page->xused.used_alloc = used; + page->xlast_used = (uint16_t)used; + page->xlast_alloc = 0; + #endif + mi_assert_internal(mi_page_alloc_count(page) + mi_page_last_used(page) >= mi_page_used(page)); + mi_assert_internal(mi_page_alloc_count(page) >= mi_page_last_alloc(page)); + + mi_theap_page_merge_stats(theapx, page, alloc_count, free_count); +} + +// Update stats for a page (and sample countdown) +void _mi_page_update_stats(mi_page_t* page) { // called on abandoned pages etc. + mi_theap_page_update_stats(mi_theap_of_page(page),page); +} + +#else +void _mi_page_update_stats(mi_page_t* page) { + MI_UNUSED(page); +} +#endif + +// Update the sampling countdown based on the current allocation count of the page +void _mi_page_update_sample_countdown(mi_page_t* page) +{ + mi_assert_internal(mi_page_is_owned(page)); + // note: the threadid may not match and even be abandoned if we free multi-threaded (so we can not always use page->theap) + + // adjust count down + const size_t alloc_count = mi_page_alloc_count(page); + if (alloc_count==0) { + return; + } + else if mi_unlikely(alloc_count>=0x8000) { // if the count could overflow, update stats so the alloc_count is reset + _mi_page_update_stats(page); // calls mi_theap_adjust_sample_countdown if needed + } + #if MI_SAMPLE==1 // if ==2 the countdown is already always counted in `alloc/alloc.c:mi_page_alloc_zero_ex` + else { + mi_theap_t* theap = mi_theap_of_page(page); // careful as the theap might not be valid + if (theap==NULL) return; // can be NULL on heap_destroy or metadata allocation etc. (and it is safe to ignore the sampling for now) + mi_theap_adjust_sample_countdown(theap,page,alloc_count); + // update last_alloc to the current alloc_count + mi_assert_internal(alloc_count <= UINT16_MAX); + #if MI_SIZE_SIZE >= 8 + page->xused.used_alloc = (alloc_count << 48) | (page->xused.used_alloc & (~MI_ZU(0) >> 16)); + #else + page->xlast_alloc = (uint16_t)alloc_count; + #endif + mi_assert_internal(mi_page_alloc_count(page) + mi_page_last_used(page) >= mi_page_used(page)); + mi_assert_internal(mi_page_alloc_count(page) >= mi_page_last_alloc(page)); + } + #endif +} + + +/* ----------------------------------------------------------- + Page collect the `local_free` and `thread_free` lists +----------------------------------------------------------- */ + +static void mi_page_thread_collect_to_local(mi_page_t* page, mi_block_t* head) +{ + if (head == NULL) return; + + // find the last block in the list -- also to get a proper use count (without data races) + size_t max_count = page->capacity; // cannot collect more than capacity + size_t count = 1; + mi_block_t* last = head; + mi_block_t* next; + while ((next = mi_block_next(page, last)) != NULL && count <= max_count) { + count++; + last = next; + } + + // if `count > max_count` there was a memory corruption (possibly infinite list due to double multi-threaded free) + if mi_unlikely(count > max_count) { + _mi_error_message(EFAULT, "corrupted thread-free list (possibly due to a cross-thread double free)\n"); + return; // the thread-free items cannot be freed + } + // if `count > page->used` there was another kind memory corruption (either in the page meta-data or in the linked list) + else if mi_unlikely(count > mi_page_used(page)) { + _mi_error_message(EFAULT, "corrupted meta-data in thread-free list\n"); + return; // the thread-free items cannot be freed + } + + // and append the current local free list + mi_block_set_next(page, last, page->local_free); + page->local_free = head; + + // update counts now + mi_assert_internal(count <= UINT16_MAX); + mi_assert_internal(mi_page_used(page) >= count); + page->xused.used_alloc -= count; // page->used = page->used - (uint16_t)count; + if (mi_page_all_free(page)) { _mi_page_qsbr_clear(page); } +} + +// Collect the local `thread_free` list using an atomic exchange. +// Returns `true` if a non-empty thread free list was collected. +static bool mi_page_thread_free_collect(mi_page_t* page) +{ + // atomically capture the thread free list + mi_block_t* head; + mi_thread_free_t tfreex; + mi_thread_free_t tfree = mi_atomic_load_relaxed(&page->xthread_free); + do { + head = mi_tf_block(tfree); + if mi_likely(head == NULL) return false; // return if the list is empty + tfreex = mi_tf_create(NULL,mi_tf_is_owned(tfree)); // set the thread free list to NULL + } while (!mi_atomic_cas_weak_acq_rel(&page->xthread_free, &tfree, tfreex)); // release is enough? + mi_assert_internal(head != NULL); + + // and move it to the local list + mi_page_thread_collect_to_local(page, head); + return true; +} + + +bool _mi_page_free_collect(mi_page_t* page, bool force) { + mi_assert_internal(page!=NULL); + + // collect the thread free list + const bool collected_xfree = mi_page_thread_free_collect(page); + + // and the local free list + if (page->local_free != NULL) { + if mi_likely(page->free == NULL) { + // usual case + page->free = page->local_free; + page->local_free = NULL; + page->free_is_zero = false; + } + else if (force) { + // append -- only on shutdown (force) as this is a linear operation + mi_block_t* tail = page->local_free; + mi_block_t* next; + while ((next = mi_block_next(page, tail)) != NULL) { + tail = next; + } + mi_block_set_next(page, tail, page->free); + page->free = page->local_free; + page->local_free = NULL; + page->free_is_zero = false; + } + _mi_page_update_sample_countdown(page); + } + mi_assert_internal(!force || page->local_free == NULL); + return collected_xfree; +} + +// Collect elements in the thread-free list starting at `head`. This is an optimized +// version of `_mi_page_free_collect` to be used from `alloc/free.c:_mi_free_collect_mt` that avoids atomic access to `xthread_free`. +// returns a possibly updated expected value for the thread_free pointer. +// +// `head` must be in the `xthread_free` list. It will not collect `head` itself +// so the `used` count is not fully updated in general. However, if the `head` is +// the last remaining element, it will be collected and the used count will become `0` (so `mi_page_all_free` becomes true). +mi_block_t* _mi_page_free_collect_partly(mi_page_t* page, mi_block_t* head) { + mi_assert_internal(mi_page_is_owned(page)); + if (head == NULL) return NULL; + mi_block_t* next = mi_block_next(page,head); // we cannot collect the head element itself as `page->thread_free` may point to it (and we want to avoid atomic ops) + if (next != NULL) { + mi_block_set_next(page, head, NULL); + mi_page_thread_collect_to_local(page, next); + if (page->local_free != NULL && page->free == NULL) { + page->free = page->local_free; + page->local_free = NULL; + page->free_is_zero = false; + _mi_page_update_sample_countdown(page); + } + } + if (mi_page_used(page) == 1) { + // all elements are free'd since we skipped the `head` element itself + mi_assert_internal(mi_tf_block(mi_atomic_load_relaxed(&page->xthread_free)) == head); + mi_assert_internal(mi_block_next(page,head) == NULL); + _mi_page_free_collect(page, false); // collect the final element + return NULL; + } + else { + return head; + } +} + + +/* ----------------------------------------------------------- + Page fresh and retire +----------------------------------------------------------- */ + +// called from `mi_free` on a reclaim, and fresh_alloc if we get an abandoned page +void _mi_theap_page_reclaim(mi_theap_t* theap, mi_page_t* page) +{ + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_assert_internal(mi_ptr_page(mi_page_start(page))==page); + mi_assert_internal(mi_page_is_owned(page)); + mi_assert_internal(mi_page_is_abandoned(page)); + + mi_page_set_theap(page,theap); + _mi_page_free_collect(page, false); // ensure used count is up to date + + mi_page_queue_t* pq = mi_theap_page_queue_of(theap, page); + mi_page_queue_push_at_end(theap, pq, page); + if (!mi_page_all_free(page)) { _mi_page_qsbr_clear(page); } + else if (!_mi_page_qsbr_is_clear(page)) { mi_page_qsbr_enqueue(page, theap); } + mi_assert_expensive(_mi_page_is_valid(page)); +} + +void _mi_page_abandon(mi_page_t* page, mi_page_queue_t* pq) { + _mi_page_free_collect(page, false); // ensure used count is up to date + pq = mi_page_queue_of(page); // a preceding release attempt may have unfulled the page + if (mi_page_all_free(page) && _mi_page_free(page, pq)) return; + pq = mi_page_queue_of(page); // _mi_page_free may have moved an empty page out of the full queue + mi_page_queue_remove(pq, page); + mi_theap_t* theap = page->theap; + mi_page_qsbr_unlink(page); + mi_page_set_theap(page, NULL); + page->theap = theap; // don't actually set theap to NULL so we can reclaim_on_free within the same theap + _mi_arenas_page_abandon(page, theap); +} + + +// allocate a fresh page from an arena +mi_page_t* _mi_page_fresh_alloc(mi_theap_t* theap, mi_page_queue_t* pq, size_t block_size, size_t page_alignment) { + #if !MI_HUGE_PAGE_ABANDON + mi_assert_internal(pq != NULL); + mi_assert_internal(mi_theap_contains_queue(theap, pq)); + mi_assert_internal(page_alignment > 0 || block_size > MI_LARGE_MAX_OBJ_SIZE || block_size == pq->block_size); + #endif + mi_page_t* page = _mi_arenas_page_alloc(theap, block_size, page_alignment); + if (page == NULL) { + // out-of-memory + return NULL; + } + if (mi_page_is_abandoned(page)) { + _mi_theap_page_reclaim(theap, page); + if (!mi_page_immediate_available(page)) { + if (mi_page_is_expandable(page)) { + if (!_mi_page_extend_free(theap, page)) { + // cannot commit + _mi_page_abandon(page,pq); + return NULL; + }; + } + else { + mi_assert(false); // should not happen? + return NULL; + } + } + } + else if (pq != NULL) { + mi_page_queue_push(theap, pq, page); + } + mi_assert_internal(pq!=NULL || mi_page_block_size(page) >= block_size); + mi_assert_expensive(_mi_page_is_valid(page)); + return page; +} + +// Get a fresh page to use +mi_page_t* _mi_page_fresh(mi_theap_t* theap, mi_page_queue_t* pq) { + mi_assert_internal(mi_theap_contains_queue(theap, pq)); + mi_page_t* page = _mi_page_fresh_alloc(theap, pq, pq->block_size, 0); + if (page==NULL) return NULL; + mi_assert_internal(pq->block_size==mi_page_block_size(page)); + mi_assert_internal(pq==mi_theap_page_queue_of(theap, page)); + return page; +} + + +/* ----------------------------------------------------------- + Unfull, abandon, free and retire +----------------------------------------------------------- */ + +// Move a page from the full list back to a regular list (called from thread-local mi_free) +void _mi_page_unfull(mi_page_t* page) { + mi_assert_internal(page != NULL); + mi_assert_expensive(_mi_page_is_valid(page)); + mi_assert_internal(mi_page_is_in_full(page)); + mi_assert_internal(!mi_page_theap(page)->allow_page_abandon); + if (!mi_page_is_in_full(page)) return; + + mi_theap_t* theap = mi_page_theap(page); + mi_page_queue_t* pqfull = &theap->pages[MI_BIN_FULL]; + mi_page_set_in_full(page, false); // to get the right queue + mi_page_queue_t* pq = mi_theap_page_queue_of(theap, page); + mi_page_set_in_full(page, true); + mi_page_queue_enqueue_from_full(pq, pqfull, page); +} + +void _mi_page_to_full(mi_page_t* page, mi_page_queue_t* pq) { + mi_assert_internal(pq == mi_page_queue_of(page)); + mi_assert_internal(!mi_page_immediate_available(page)); + mi_assert_internal(!mi_page_is_in_full(page)); + + mi_theap_t* theap = mi_page_theap(page); + if (theap->allow_page_abandon) { + // abandon full pages (this is the usual case in order to allow for sharing of memory between theaps) + _mi_page_abandon(page, pq); + } + else if (!mi_page_is_in_full(page)) { + // put full pages in a theap local queue (this is for theaps that cannot abandon, for example, if the theap can be destroyed) + mi_page_queue_enqueue_from(&mi_page_theap(page)->pages[MI_BIN_FULL], pq, page); + _mi_page_free_collect(page, false); // try to collect right away in case another thread freed just before MI_USE_DELAYED_FREE was set + _mi_page_update_stats(page); // we must update stats here as mi_theap_collect does not normally visit full pages + } +} + + +// Physically release an empty page after its grace period has finished. +static void mi_page_free_prim(mi_page_t* page, mi_page_queue_t* pq) { + mi_assert_internal(page != NULL); + mi_assert_internal(_mi_page_qsbr_is_clear(page)); + mi_assert_expensive(_mi_page_is_valid(page)); + mi_assert_internal(pq == mi_page_queue_of(page)); + mi_assert_internal(mi_page_all_free(page)); + // mi_assert_internal(mi_page_thread_free_flag(page)!=MI_DELAYED_FREEING); + + // remove from the page list + // (no need to do _mi_theap_delayed_free first as all blocks are already free) + mi_page_queue_remove(pq, page); + + // and free it + mi_theap_t* theap = mi_page_theap(page); mi_assert_internal(theap!=NULL); + mi_page_set_theap(page,NULL); + _mi_arenas_page_free(page, theap); + // _mi_arenas_collect(false, false, theap->tld); // allow purging +} + +// Release an empty page (or retain it until its grace period has finished). +bool _mi_page_free(mi_page_t* page, mi_page_queue_t* pq) { + mi_assert_internal(mi_page_all_free(page)); + mi_assert_internal(pq == mi_page_queue_of(page)); + + // no more aligned blocks in here + mi_page_set_has_interior_pointers(page, false); + + // try to free + if (_mi_page_qsbr_should_delay(page)) { + // for pages that are delayed due to qsbr (e.g. in CPython) + page->retire_expire = 0; + if (mi_page_is_in_full(page)) { _mi_page_unfull(page); } + mi_page_qsbr_enqueue(page, mi_page_theap(page)); + return false; + } + else { + // free the page + _mi_page_qsbr_clear(page); + mi_page_free_prim(page, pq); + return true; + } +} + +#define MI_RETIRE_CYCLES (16) /* keep a retired page around for about 16 "admin cycles" before free'ing it */ +#define MI_RETIRE_MAX_PAGES (3) /* keep at most N pages per size bin as retired */ + +// Retire a page with no more used blocks +// Important to not retire too quickly though as new +// allocations might coming. +// +// Note: called from `mi_free` and benchmarks often +// trigger this due to freeing everything and then +// allocating again so careful when changing this. +void _mi_page_retire(mi_page_t* page) mi_attr_noexcept { + mi_assert_internal(page != NULL); + mi_assert_expensive(_mi_page_is_valid(page)); + mi_assert_internal(mi_page_all_free(page)); + + if (page->retire_expire!=0) return; // already retired, just keep it retired + mi_page_set_has_interior_pointers(page, false); + + // don't retire too often.. + // (or we end up retiring and re-allocating most of the time) + // NOTE: refine this more: we should not retire if this + // is the only page left with free blocks. It is not clear + // how to check this efficiently though... + // for now, we don't retire if it is the only page left of this size class. + mi_page_queue_t* pq = mi_page_queue_of(page); + #if MI_RETIRE_CYCLES > 0 + const size_t bsize = mi_page_block_size(page); + if mi_likely( pq->count <= MI_RETIRE_MAX_PAGES && !mi_page_queue_is_special(pq)) { // not full or huge queue? + if (pq->count==1 || bsize < MI_SMALL_SIZE_MAX) { + mi_theap_t* theap = mi_page_theap(page); + mi_theap_stat_counter_increase(theap, pages_retire, 1); + page->retire_expire = (bsize <= MI_SMALL_MAX_OBJ_SIZE ? MI_RETIRE_CYCLES : MI_RETIRE_CYCLES/4); + mi_assert_internal(pq >= theap->pages); + const size_t index = pq - theap->pages; + mi_assert_internal(index < MI_BIN_FULL && index < MI_BIN_HUGE); + if (index < theap->page_retired_min) theap->page_retired_min = index; + if (index > theap->page_retired_max) theap->page_retired_max = index; + mi_assert_internal(mi_page_all_free(page)); + return; // don't free after all + } + } + #endif + _mi_page_free(page, pq); +} + + +static void mi_theap_collect_full_pages(mi_theap_t* theap) { + // note: normally full pages get immediately abandoned and the full queue is always empty + // this path is only used if abandoning is disabled due to a destroy-able theap or options + // set by the user. + mi_page_queue_t* pq = &theap->pages[MI_BIN_FULL]; + for (mi_page_t* page = pq->first; page != NULL; ) { + mi_page_t* next = page->next; // get next in case we free the page + _mi_page_free_collect(page, false); // register concurrent free's + // no longer full? + if (!mi_page_is_full(page)) { + if (mi_page_all_free(page)) { + _mi_page_free(page, pq); + } + else { + _mi_page_unfull(page); + } + } + page = next; + } +} + +static void mi_page_try_retire(mi_page_queue_t* pq, mi_page_t* page, size_t bin, bool force, size_t* min, size_t* max) { + mi_assert_internal(page!=NULL && page->retire_expire!=0); + if (mi_page_all_free(page)) { + page->retire_expire--; + if (page->retire_expire == 0 || force) { + _mi_page_free(page, pq); + } + else { + // keep retired, update min/max + if (bin < *min) *min = bin; + if (bin > *max) *max = bin; + } + } + else { + page->retire_expire = 0; + } +} + +// free retired pages: we don't need to look at the entire queues +// since we only retire pages that are at the head position in a queue. +void _mi_theap_collect_retired(mi_theap_t* theap, bool force) { + size_t min = MI_BIN_FULL; + size_t max = 0; + for(size_t bin = theap->page_retired_min; bin <= theap->page_retired_max; bin++) { + mi_page_queue_t* pq = &theap->pages[bin]; + mi_page_t* page = pq->first; + for (int i = 0; iretire_expire!=0; i++) { + mi_page_t* next = page->next; + mi_page_try_retire(pq,page,bin,force,&min,&max); + page = next; + } + } + theap->page_retired_min = min; + theap->page_retired_max = max; + if (!theap->allow_page_abandon) { + mi_theap_collect_full_pages(theap); + } + _mi_theap_qsbr_collect_delayed(theap); +} + + + + +/* ----------------------------------------------------------- + Initialize the initial free list in a page. + In secure mode we initialize a randomized list by + alternating between slices. +----------------------------------------------------------- */ + +#define MI_MAX_SLICE_SHIFT (6) // at most 64 slices +#define MI_MAX_SLICES (1UL << MI_MAX_SLICE_SHIFT) +#define MI_MIN_SLICES (2) + +static void mi_page_free_list_extend_secure(mi_theap_t* const theap, mi_page_t* const page, const size_t bsize, const size_t extend) { + #if (MI_SECURE < 2) + mi_assert_internal(page->free == NULL); + mi_assert_internal(page->local_free == NULL); + #endif + mi_assert_internal(page->capacity + extend <= page->reserved); + mi_assert_internal(bsize == mi_page_block_size(page)); + void* const page_area = mi_page_start(page); + + // initialize a randomized free list + // set up `slice_count` slices to alternate between + size_t shift = MI_MAX_SLICE_SHIFT; + while ((extend >> shift) == 0) { + shift--; + } + const size_t slice_count = (size_t)1U << shift; + const size_t slice_extend = extend / slice_count; + mi_assert_internal(slice_extend >= 1); + mi_block_t* blocks[MI_MAX_SLICES]; // current start of the slice + size_t counts[MI_MAX_SLICES]; // available objects in the slice + for (size_t i = 0; i < slice_count; i++) { + blocks[i] = mi_page_block_at(page, page_area, bsize, page->capacity + i*slice_extend); + counts[i] = slice_extend; + } + counts[slice_count-1] += (extend % slice_count); // final slice holds the modulus too (todo: distribute evenly?) + + // and initialize the free list by randomly threading through them + // set up first element + const size_t r = _mi_theap_random_next(theap); + size_t current = r % slice_count; + counts[current]--; + mi_block_t* const free_start = blocks[current]; + // and iterate through the rest; use `random_shuffle` for performance + size_t rnd = mi_random_shuffle(r|1); // ensure not 0 + for (size_t i = 1; i < extend; i++) { + // call random_shuffle only every SIZE_SIZE rounds + const size_t round = i%MI_SIZE_SIZE; + if (round == 0) rnd = mi_random_shuffle(rnd); + // select a random next slice index + size_t next = ((rnd >> 8*round) & (slice_count-1)); + while (counts[next]==0) { // ensure it still has space + next++; + if (next==slice_count) next = 0; + } + // and link the current block to it + counts[next]--; + mi_block_t* const block = blocks[current]; + blocks[current] = (mi_block_t*)((uint8_t*)block + bsize); // bump to the following block + mi_block_set_next(page, block, blocks[next]); // and set next; note: we may have `current == next` + current = next; + } + // prepend to the free list (usually NULL) + mi_block_set_next(page, blocks[current], page->free); // end of the list + page->free = free_start; +} + +static mi_decl_noinline void mi_page_free_list_extend( mi_page_t* const page, const size_t bsize, const size_t extend) +{ + #if (MI_SECURE < 2) + mi_assert_internal(page->free == NULL); + mi_assert_internal(page->local_free == NULL); + #endif + mi_assert_internal(page->capacity + extend <= page->reserved); + mi_assert_internal(bsize == mi_page_block_size(page)); + void* const page_area = mi_page_start(page); + + mi_block_t* const start = mi_page_block_at(page, page_area, bsize, page->capacity); + + // initialize a sequential free list + mi_block_t* const last = mi_page_block_at(page, page_area, bsize, page->capacity + extend - 1); + mi_block_t* block = start; + while(block <= last) { + mi_block_t* next = (mi_block_t*)((uint8_t*)block + bsize); + mi_block_set_next(page,block,next); + block = next; + } + // prepend to free list (usually `NULL`) + mi_block_set_next(page, last, page->free); + page->free = start; +} + +/* ----------------------------------------------------------- + Page initialize and extend the capacity +----------------------------------------------------------- */ + +#define MI_MAX_EXTEND_SIZE (8*1024) // heuristic, one or two OS pages seems to work well. +#if (MI_SECURE>=2) +#define MI_MIN_EXTEND (8*MI_SECURE) // extend at least by this many +#else +#define MI_MIN_EXTEND (1) +#endif + +// Extend the capacity (up to reserved) by initializing a free list +// We do at most `MI_MAX_EXTEND` to avoid touching too much memory +// Note: we also experimented with "bump" allocation on the first +// allocations but this did not speed up any benchmark (due to an +// extra test in malloc? or cache effects?) +bool _mi_page_extend_free(mi_theap_t* theap, mi_page_t* page) { + mi_assert_expensive(mi_page_is_valid_init(page)); + #if (MI_SECURE < 2) + mi_assert(page->free == NULL); + mi_assert(page->local_free == NULL); + if (page->free != NULL) return true; + #endif + if (page->capacity >= page->reserved) return true; + + size_t page_size; + //uint8_t* page_start = + mi_page_area(page, &page_size); + mi_theap_stat_counter_increase(theap, pages_extended, 1); + + // calculate the extend count + const size_t bsize = mi_page_block_size(page); + size_t extend = (size_t)page->reserved - page->capacity; + mi_assert_internal(extend > 0); + + size_t max_extend = (bsize >= MI_MAX_EXTEND_SIZE ? MI_MIN_EXTEND : MI_MAX_EXTEND_SIZE/bsize); + if (max_extend < MI_MIN_EXTEND) { max_extend = MI_MIN_EXTEND; } + mi_assert_internal(max_extend > 0); + + if (extend > max_extend) { + // ensure we don't touch memory beyond the page to reduce page commit. + // the `lean` benchmark tests this. Going from 1 to 8 increases rss by 50%. + extend = max_extend; + } + + mi_assert_internal(extend > 0 && extend + page->capacity <= page->reserved); + mi_assert_internal(extend < (1UL<<16)); + + // commit on demand? + const size_t slice_committed = mi_page_slice_committed(page); + if (slice_committed > 0) { + // reduce extend if it commits more than an arena slice + if ((extend * bsize) > MI_ARENA_SLICE_SIZE) { + extend = mi_divide_up(MI_ARENA_SLICE_SIZE, bsize); + } + // commit required size + const size_t needed_size = (page->capacity + extend)*bsize; + mi_assert_internal(needed_size <= page_size); + size_t needed_commit = mi_align_up( mi_page_slice_offset_of(page, needed_size), mi_page_min_commit_size()); + #if MI_SECURE>=5 + // the previous alignup could extend the commit into the guard page; re-adjust if needed + const size_t page_size_commit = mi_align_up( mi_page_slice_offset_of(page, page_size), _mi_os_page_size() ); + if (needed_commit > page_size_commit) { + needed_commit = page_size_commit; + } + #endif + if (needed_commit > slice_committed) { + mi_assert_internal(((needed_commit - slice_committed) % _mi_os_page_size()) == 0); + if (!_mi_os_commit(mi_theap_subproc(theap), mi_page_slice_start(page) + slice_committed, needed_commit - slice_committed, NULL)) { + return false; + } + mi_assert_internal(needed_commit <= UINT16_MAX * _mi_os_page_size()); + page->slice_pcommitted = (uint16_t)(needed_commit / _mi_os_page_size()); + } + } + + // and append the extend the free list + if (extend < MI_MIN_SLICES || MI_SECURE<2) { //!mi_option_is_enabled(mi_option_secure)) { + mi_page_free_list_extend(page, bsize, extend ); + } + else { + mi_page_free_list_extend_secure(theap, page, bsize, extend); + } + // enable the new free list + page->capacity += (uint16_t)extend; + mi_theap_stat_increase(theap, page_committed, extend * bsize); + mi_assert_expensive(mi_page_is_valid_init(page)); + return true; +} + +// Initialize a fresh page (that is already partially initialized) +mi_decl_nodiscard bool _mi_page_init(mi_theap_t* theap, mi_page_t* page) { + mi_assert(page != NULL); + mi_assert(theap!=NULL); + // page->heap = (mi_is_heap_main(mi_theap_heap(theap)) ? NULL : mi_theap_heap(theap)); // faster for `mi_page_associated_theap` + // mi_page_set_theap(page, theap); + + size_t page_size; + uint8_t* page_start = mi_page_area(page, &page_size); MI_UNUSED(page_start); + mi_track_mem_noaccess(page_start,page_size); + mi_assert_internal(page_size / mi_page_block_size(page) < (1L<<16)); + mi_assert_internal(page->reserved > 0); + #if (MI_PADDING || MI_ENCODE_FREELIST) + page->keys[0] = _mi_theap_random_next(theap); + #if MI_PAGE_KEY_COUNT==2 + page->keys[1] = _mi_theap_random_next(theap); + #endif + #endif + #if MI_DEBUG>2 + if (page->memid.initially_zero) { + mi_track_mem_defined(mi_page_start(page), mi_page_committed(page)); + mi_assert_expensive(mi_mem_is_zero(page_start, mi_page_committed(page))); + } + #endif + + mi_assert_internal(page->heap != NULL); + mi_assert_internal(page->heap == mi_theap_heap(theap)); + mi_assert_internal(page->theap!=NULL); + mi_assert_internal(page->theap == mi_page_theap(page)); + mi_assert_internal(page->capacity == 0); + mi_assert_internal(page->free == NULL); + mi_assert_internal(mi_page_used(page) == 0); + mi_assert_internal(page->xused.used_alloc == 0); + mi_assert_internal(mi_page_is_owned(page)); + mi_assert_internal(page->xthread_free == 1); + mi_assert_internal(page->next == NULL); + mi_assert_internal(page->prev == NULL); + mi_assert_internal(page->retire_expire == 0); + mi_assert_internal(!mi_page_has_interior_pointers(page)); + #if (MI_PADDING || MI_ENCODE_FREELIST) + mi_assert_internal(page->keys[0] != 0); + #if MI_PAGE_KEY_COUNT==2 + mi_assert_internal(page->keys[1] != 0); + #endif + #endif + mi_assert_expensive(mi_page_is_valid_init(page)); + + // initialize an initial free list + if (!_mi_page_extend_free(theap,page)) return false; + mi_assert(mi_page_immediate_available(page)); + return true; +} diff --git a/Objects/mimalloc/alloc/page.h b/Objects/mimalloc/alloc/page.h new file mode 100644 index 000000000000000..9b71deb52df2bfd --- /dev/null +++ b/Objects/mimalloc/alloc/page.h @@ -0,0 +1,37 @@ +/*---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#pragma once +#ifndef MI_PAGE_H +#define MI_PAGE_H + +#include +#include "page-queue.h" + +bool _mi_page_extend_free(mi_theap_t* theap, mi_page_t* page); +mi_page_t* _mi_page_fresh_alloc(mi_theap_t* theap, mi_page_queue_t* pq, size_t block_size, size_t page_alignment); +mi_page_t* _mi_page_fresh(mi_theap_t* theap, mi_page_queue_t* pq); +void _mi_page_to_full(mi_page_t* page, mi_page_queue_t* pq); +void _mi_page_update_sample_countdown(mi_page_t* page); + +static inline void _mi_theap_qsbr_collect_delayed(mi_theap_t* theap) { +#if MI_QSBR + mi_page_t* page; + while ((page = theap->qsbr_first) != NULL) { + _mi_page_free_collect(page, false); + if (!mi_page_all_free(page)) { + _mi_page_qsbr_clear(page); + } + else if (!_mi_page_free(page, mi_page_queue_of(page)) && theap->qsbr_first == page) { + break; + } + } +#else + MI_UNUSED(theap); +#endif +} + +#endif diff --git a/Objects/mimalloc/alloc/readme.md b/Objects/mimalloc/alloc/readme.md new file mode 100644 index 000000000000000..0c354f70aae6a3a --- /dev/null +++ b/Objects/mimalloc/alloc/readme.md @@ -0,0 +1,12 @@ +# Allocation and Freeing + +- [`alloc.c`](alloc.c): Main allocation and reallocation functions, including the allocation fast paths. +- [`alloc-generic.c`](alloc-generic.c): Allocation slow path (`mi_malloc_generic`), page selection, deferred-free callbacks. +- [`alloc-aligned.c`](alloc-aligned.c): Aligned allocation and reallocation, including allocations with an alignment offset. +- [`alloc-posix.c`](alloc-posix.c): POSIX, Unix, and Microsoft-compatible allocation interfaces and convenience functions. +- [`alloc-override.c`](alloc-override.c): Standard allocator and C++ allocation overrides, included by `alloc.c`. +- [`free.c`](free.c): Local and cross-thread freeing, pointer validation, and usable-size queries, included by `alloc.c`. +- [`page.c`](page.c): Allocation-page initialization, free-list management, collection, and retirement. +- [`page.h`](page.h): Private declarations for page helpers used by generic allocation. +- [`page-queue.c`](page-queue.c): Size-class mapping and management of page queues. +- [`page-queue.h`](page-queue.h): Private page-queue declarations and inline helpers. diff --git a/Objects/mimalloc/arena.c b/Objects/mimalloc/arena.c deleted file mode 100644 index 5db5d950c43b683..000000000000000 --- a/Objects/mimalloc/arena.c +++ /dev/null @@ -1,935 +0,0 @@ -/* ---------------------------------------------------------------------------- -Copyright (c) 2019-2023, Microsoft Research, Daan Leijen -This is free software; you can redistribute it and/or modify it under the -terms of the MIT license. A copy of the license can be found in the file -"LICENSE" at the root of this distribution. ------------------------------------------------------------------------------*/ - -/* ---------------------------------------------------------------------------- -"Arenas" are fixed area's of OS memory from which we can allocate -large blocks (>= MI_ARENA_MIN_BLOCK_SIZE, 4MiB). -In contrast to the rest of mimalloc, the arenas are shared between -threads and need to be accessed using atomic operations. - -Arenas are used to for huge OS page (1GiB) reservations or for reserving -OS memory upfront which can be improve performance or is sometimes needed -on embedded devices. We can also employ this with WASI or `sbrk` systems -to reserve large arenas upfront and be able to reuse the memory more effectively. - -The arena allocation needs to be thread safe and we use an atomic bitmap to allocate. ------------------------------------------------------------------------------*/ -#include "mimalloc.h" -#include "mimalloc/internal.h" -#include "mimalloc/atomic.h" - -#include // memset -#include // ENOMEM - -#include "bitmap.h" // atomic bitmap - -/* ----------------------------------------------------------- - Arena allocation ------------------------------------------------------------ */ - -// Block info: bit 0 contains the `in_use` bit, the upper bits the -// size in count of arena blocks. -typedef uintptr_t mi_block_info_t; -#define MI_ARENA_BLOCK_SIZE (MI_SEGMENT_SIZE) // 64MiB (must be at least MI_SEGMENT_ALIGN) -#define MI_ARENA_MIN_OBJ_SIZE (MI_ARENA_BLOCK_SIZE/2) // 32MiB -#define MI_MAX_ARENAS (112) // not more than 126 (since we use 7 bits in the memid and an arena index + 1) - -// A memory arena descriptor -typedef struct mi_arena_s { - mi_arena_id_t id; // arena id; 0 for non-specific - mi_memid_t memid; // memid of the memory area - _Atomic(uint8_t*) start; // the start of the memory area - size_t block_count; // size of the area in arena blocks (of `MI_ARENA_BLOCK_SIZE`) - size_t field_count; // number of bitmap fields (where `field_count * MI_BITMAP_FIELD_BITS >= block_count`) - size_t meta_size; // size of the arena structure itself (including its bitmaps) - mi_memid_t meta_memid; // memid of the arena structure itself (OS or static allocation) - int numa_node; // associated NUMA node - bool exclusive; // only allow allocations if specifically for this arena - bool is_large; // memory area consists of large- or huge OS pages (always committed) - _Atomic(size_t) search_idx; // optimization to start the search for free blocks - _Atomic(mi_msecs_t) purge_expire; // expiration time when blocks should be decommitted from `blocks_decommit`. - mi_bitmap_field_t* blocks_dirty; // are the blocks potentially non-zero? - mi_bitmap_field_t* blocks_committed; // are the blocks committed? (can be NULL for memory that cannot be decommitted) - mi_bitmap_field_t* blocks_purge; // blocks that can be (reset) decommitted. (can be NULL for memory that cannot be (reset) decommitted) - mi_bitmap_field_t blocks_inuse[1]; // in-place bitmap of in-use blocks (of size `field_count`) -} mi_arena_t; - - -// The available arenas -static mi_decl_cache_align _Atomic(mi_arena_t*) mi_arenas[MI_MAX_ARENAS]; -static mi_decl_cache_align _Atomic(size_t) mi_arena_count; // = 0 - - -//static bool mi_manage_os_memory_ex2(void* start, size_t size, bool is_large, int numa_node, bool exclusive, mi_memid_t memid, mi_arena_id_t* arena_id) mi_attr_noexcept; - -/* ----------------------------------------------------------- - Arena id's - id = arena_index + 1 ------------------------------------------------------------ */ - -static size_t mi_arena_id_index(mi_arena_id_t id) { - return (size_t)(id <= 0 ? MI_MAX_ARENAS : id - 1); -} - -static mi_arena_id_t mi_arena_id_create(size_t arena_index) { - mi_assert_internal(arena_index < MI_MAX_ARENAS); - return (int)arena_index + 1; -} - -mi_arena_id_t _mi_arena_id_none(void) { - return 0; -} - -static bool mi_arena_id_is_suitable(mi_arena_id_t arena_id, bool arena_is_exclusive, mi_arena_id_t req_arena_id) { - return ((!arena_is_exclusive && req_arena_id == _mi_arena_id_none()) || - (arena_id == req_arena_id)); -} - -bool _mi_arena_memid_is_suitable(mi_memid_t memid, mi_arena_id_t request_arena_id) { - if (memid.memkind == MI_MEM_ARENA) { - return mi_arena_id_is_suitable(memid.mem.arena.id, memid.mem.arena.is_exclusive, request_arena_id); - } - else { - return mi_arena_id_is_suitable(0, false, request_arena_id); - } -} - -bool _mi_arena_memid_is_os_allocated(mi_memid_t memid) { - return (memid.memkind == MI_MEM_OS); -} - -/* ----------------------------------------------------------- - Arena allocations get a (currently) 16-bit memory id where the - lower 8 bits are the arena id, and the upper bits the block index. ------------------------------------------------------------ */ - -static size_t mi_block_count_of_size(size_t size) { - return _mi_divide_up(size, MI_ARENA_BLOCK_SIZE); -} - -static size_t mi_arena_block_size(size_t bcount) { - return (bcount * MI_ARENA_BLOCK_SIZE); -} - -static size_t mi_arena_size(mi_arena_t* arena) { - return mi_arena_block_size(arena->block_count); -} - -static mi_memid_t mi_memid_create_arena(mi_arena_id_t id, bool is_exclusive, mi_bitmap_index_t bitmap_index) { - mi_memid_t memid = _mi_memid_create(MI_MEM_ARENA); - memid.mem.arena.id = id; - memid.mem.arena.block_index = bitmap_index; - memid.mem.arena.is_exclusive = is_exclusive; - return memid; -} - -static bool mi_arena_memid_indices(mi_memid_t memid, size_t* arena_index, mi_bitmap_index_t* bitmap_index) { - mi_assert_internal(memid.memkind == MI_MEM_ARENA); - *arena_index = mi_arena_id_index(memid.mem.arena.id); - *bitmap_index = memid.mem.arena.block_index; - return memid.mem.arena.is_exclusive; -} - - - -/* ----------------------------------------------------------- - Special static area for mimalloc internal structures - to avoid OS calls (for example, for the arena metadata) ------------------------------------------------------------ */ - -#define MI_ARENA_STATIC_MAX (MI_INTPTR_SIZE*MI_KiB) // 8 KiB on 64-bit - -static uint8_t mi_arena_static[MI_ARENA_STATIC_MAX]; -static _Atomic(size_t) mi_arena_static_top; - -static void* mi_arena_static_zalloc(size_t size, size_t alignment, mi_memid_t* memid) { - *memid = _mi_memid_none(); - if (size == 0 || size > MI_ARENA_STATIC_MAX) return NULL; - if ((mi_atomic_load_relaxed(&mi_arena_static_top) + size) > MI_ARENA_STATIC_MAX) return NULL; - - // try to claim space - if (alignment == 0) { alignment = 1; } - const size_t oversize = size + alignment - 1; - if (oversize > MI_ARENA_STATIC_MAX) return NULL; - const size_t oldtop = mi_atomic_add_acq_rel(&mi_arena_static_top, oversize); - size_t top = oldtop + oversize; - if (top > MI_ARENA_STATIC_MAX) { - // try to roll back, ok if this fails - mi_atomic_cas_strong_acq_rel(&mi_arena_static_top, &top, oldtop); - return NULL; - } - - // success - *memid = _mi_memid_create(MI_MEM_STATIC); - const size_t start = _mi_align_up(oldtop, alignment); - uint8_t* const p = &mi_arena_static[start]; - _mi_memzero(p, size); - return p; -} - -static void* mi_arena_meta_zalloc(size_t size, mi_memid_t* memid, mi_stats_t* stats) { - *memid = _mi_memid_none(); - - // try static - void* p = mi_arena_static_zalloc(size, MI_ALIGNMENT_MAX, memid); - if (p != NULL) return p; - - // or fall back to the OS - return _mi_os_alloc(size, memid, stats); -} - -static void mi_arena_meta_free(void* p, mi_memid_t memid, size_t size, mi_stats_t* stats) { - if (mi_memkind_is_os(memid.memkind)) { - _mi_os_free(p, size, memid, stats); - } - else { - mi_assert(memid.memkind == MI_MEM_STATIC); - } -} - -static void* mi_arena_block_start(mi_arena_t* arena, mi_bitmap_index_t bindex) { - return (arena->start + mi_arena_block_size(mi_bitmap_index_bit(bindex))); -} - - -/* ----------------------------------------------------------- - Thread safe allocation in an arena ------------------------------------------------------------ */ - -// claim the `blocks_inuse` bits -static bool mi_arena_try_claim(mi_arena_t* arena, size_t blocks, mi_bitmap_index_t* bitmap_idx) -{ - size_t idx = 0; // mi_atomic_load_relaxed(&arena->search_idx); // start from last search; ok to be relaxed as the exact start does not matter - if (_mi_bitmap_try_find_from_claim_across(arena->blocks_inuse, arena->field_count, idx, blocks, bitmap_idx)) { - mi_atomic_store_relaxed(&arena->search_idx, mi_bitmap_index_field(*bitmap_idx)); // start search from found location next time around - return true; - }; - return false; -} - - -/* ----------------------------------------------------------- - Arena Allocation ------------------------------------------------------------ */ - -static mi_decl_noinline void* mi_arena_try_alloc_at(mi_arena_t* arena, size_t arena_index, size_t needed_bcount, - bool commit, mi_memid_t* memid, mi_os_tld_t* tld) -{ - MI_UNUSED(arena_index); - mi_assert_internal(mi_arena_id_index(arena->id) == arena_index); - - mi_bitmap_index_t bitmap_index; - if (!mi_arena_try_claim(arena, needed_bcount, &bitmap_index)) return NULL; - - // claimed it! - void* p = mi_arena_block_start(arena, bitmap_index); - *memid = mi_memid_create_arena(arena->id, arena->exclusive, bitmap_index); - memid->is_pinned = arena->memid.is_pinned; - - // none of the claimed blocks should be scheduled for a decommit - if (arena->blocks_purge != NULL) { - // this is thread safe as a potential purge only decommits parts that are not yet claimed as used (in `blocks_inuse`). - _mi_bitmap_unclaim_across(arena->blocks_purge, arena->field_count, needed_bcount, bitmap_index); - } - - // set the dirty bits (todo: no need for an atomic op here?) - if (arena->memid.initially_zero && arena->blocks_dirty != NULL) { - memid->initially_zero = _mi_bitmap_claim_across(arena->blocks_dirty, arena->field_count, needed_bcount, bitmap_index, NULL); - } - - // set commit state - if (arena->blocks_committed == NULL) { - // always committed - memid->initially_committed = true; - } - else if (commit) { - // commit requested, but the range may not be committed as a whole: ensure it is committed now - memid->initially_committed = true; - bool any_uncommitted; - _mi_bitmap_claim_across(arena->blocks_committed, arena->field_count, needed_bcount, bitmap_index, &any_uncommitted); - if (any_uncommitted) { - bool commit_zero = false; - if (!_mi_os_commit(p, mi_arena_block_size(needed_bcount), &commit_zero, tld->stats)) { - memid->initially_committed = false; - } - else { - if (commit_zero) { memid->initially_zero = true; } - } - } - } - else { - // no need to commit, but check if already fully committed - memid->initially_committed = _mi_bitmap_is_claimed_across(arena->blocks_committed, arena->field_count, needed_bcount, bitmap_index); - } - - return p; -} - -// allocate in a specific arena -static void* mi_arena_try_alloc_at_id(mi_arena_id_t arena_id, bool match_numa_node, int numa_node, size_t size, size_t alignment, - bool commit, bool allow_large, mi_arena_id_t req_arena_id, mi_memid_t* memid, mi_os_tld_t* tld ) -{ - MI_UNUSED_RELEASE(alignment); - mi_assert_internal(alignment <= MI_SEGMENT_ALIGN); - const size_t bcount = mi_block_count_of_size(size); - const size_t arena_index = mi_arena_id_index(arena_id); - mi_assert_internal(arena_index < mi_atomic_load_relaxed(&mi_arena_count)); - mi_assert_internal(size <= mi_arena_block_size(bcount)); - - // Check arena suitability - mi_arena_t* arena = mi_atomic_load_ptr_acquire(mi_arena_t, &mi_arenas[arena_index]); - if (arena == NULL) return NULL; - if (!allow_large && arena->is_large) return NULL; - if (!mi_arena_id_is_suitable(arena->id, arena->exclusive, req_arena_id)) return NULL; - if (req_arena_id == _mi_arena_id_none()) { // in not specific, check numa affinity - const bool numa_suitable = (numa_node < 0 || arena->numa_node < 0 || arena->numa_node == numa_node); - if (match_numa_node) { if (!numa_suitable) return NULL; } - else { if (numa_suitable) return NULL; } - } - - // try to allocate - void* p = mi_arena_try_alloc_at(arena, arena_index, bcount, commit, memid, tld); - mi_assert_internal(p == NULL || _mi_is_aligned(p, alignment)); - return p; -} - - -// allocate from an arena with fallback to the OS -static mi_decl_noinline void* mi_arena_try_alloc(int numa_node, size_t size, size_t alignment, - bool commit, bool allow_large, - mi_arena_id_t req_arena_id, mi_memid_t* memid, mi_os_tld_t* tld ) -{ - MI_UNUSED(alignment); - mi_assert_internal(alignment <= MI_SEGMENT_ALIGN); - const size_t max_arena = mi_atomic_load_relaxed(&mi_arena_count); - if mi_likely(max_arena == 0) return NULL; - - if (req_arena_id != _mi_arena_id_none()) { - // try a specific arena if requested - if (mi_arena_id_index(req_arena_id) < max_arena) { - void* p = mi_arena_try_alloc_at_id(req_arena_id, true, numa_node, size, alignment, commit, allow_large, req_arena_id, memid, tld); - if (p != NULL) return p; - } - } - else { - // try numa affine allocation - for (size_t i = 0; i < max_arena; i++) { - void* p = mi_arena_try_alloc_at_id(mi_arena_id_create(i), true, numa_node, size, alignment, commit, allow_large, req_arena_id, memid, tld); - if (p != NULL) return p; - } - - // try from another numa node instead.. - if (numa_node >= 0) { // if numa_node was < 0 (no specific affinity requested), all arena's have been tried already - for (size_t i = 0; i < max_arena; i++) { - void* p = mi_arena_try_alloc_at_id(mi_arena_id_create(i), false /* only proceed if not numa local */, numa_node, size, alignment, commit, allow_large, req_arena_id, memid, tld); - if (p != NULL) return p; - } - } - } - return NULL; -} - -// try to reserve a fresh arena space -static bool mi_arena_reserve(size_t req_size, bool allow_large, mi_arena_id_t req_arena_id, mi_arena_id_t *arena_id) -{ - if (_mi_preloading()) return false; // use OS only while pre loading - if (req_arena_id != _mi_arena_id_none()) return false; - - const size_t arena_count = mi_atomic_load_acquire(&mi_arena_count); - if (arena_count > (MI_MAX_ARENAS - 4)) return false; - - size_t arena_reserve = mi_option_get_size(mi_option_arena_reserve); - if (arena_reserve == 0) return false; - - if (!_mi_os_has_virtual_reserve()) { - arena_reserve = arena_reserve/4; // be conservative if virtual reserve is not supported (for some embedded systems for example) - } - arena_reserve = _mi_align_up(arena_reserve, MI_ARENA_BLOCK_SIZE); - if (arena_count >= 8 && arena_count <= 128) { - arena_reserve = ((size_t)1<<(arena_count/8)) * arena_reserve; // scale up the arena sizes exponentially - } - if (arena_reserve < req_size) return false; // should be able to at least handle the current allocation size - - // commit eagerly? - bool arena_commit = false; - if (mi_option_get(mi_option_arena_eager_commit) == 2) { arena_commit = _mi_os_has_overcommit(); } - else if (mi_option_get(mi_option_arena_eager_commit) == 1) { arena_commit = true; } - - return (mi_reserve_os_memory_ex(arena_reserve, arena_commit, allow_large, false /* exclusive */, arena_id) == 0); -} - - -void* _mi_arena_alloc_aligned(size_t size, size_t alignment, size_t align_offset, bool commit, bool allow_large, - mi_arena_id_t req_arena_id, mi_memid_t* memid, mi_os_tld_t* tld) -{ - mi_assert_internal(memid != NULL && tld != NULL); - mi_assert_internal(size > 0); - *memid = _mi_memid_none(); - - const int numa_node = _mi_os_numa_node(tld); // current numa node - - // try to allocate in an arena if the alignment is small enough and the object is not too small (as for heap meta data) - if (size >= MI_ARENA_MIN_OBJ_SIZE && alignment <= MI_SEGMENT_ALIGN && align_offset == 0) { - void* p = mi_arena_try_alloc(numa_node, size, alignment, commit, allow_large, req_arena_id, memid, tld); - if (p != NULL) return p; - - // otherwise, try to first eagerly reserve a new arena - if (req_arena_id == _mi_arena_id_none()) { - mi_arena_id_t arena_id = 0; - if (mi_arena_reserve(size, allow_large, req_arena_id, &arena_id)) { - // and try allocate in there - mi_assert_internal(req_arena_id == _mi_arena_id_none()); - p = mi_arena_try_alloc_at_id(arena_id, true, numa_node, size, alignment, commit, allow_large, req_arena_id, memid, tld); - if (p != NULL) return p; - } - } - } - - // if we cannot use OS allocation, return NULL - if (mi_option_is_enabled(mi_option_limit_os_alloc) || req_arena_id != _mi_arena_id_none()) { - errno = ENOMEM; - return NULL; - } - - // finally, fall back to the OS - if (align_offset > 0) { - return _mi_os_alloc_aligned_at_offset(size, alignment, align_offset, commit, allow_large, memid, tld->stats); - } - else { - return _mi_os_alloc_aligned(size, alignment, commit, allow_large, memid, tld->stats); - } -} - -void* _mi_arena_alloc(size_t size, bool commit, bool allow_large, mi_arena_id_t req_arena_id, mi_memid_t* memid, mi_os_tld_t* tld) -{ - return _mi_arena_alloc_aligned(size, MI_ARENA_BLOCK_SIZE, 0, commit, allow_large, req_arena_id, memid, tld); -} - - -void* mi_arena_area(mi_arena_id_t arena_id, size_t* size) { - if (size != NULL) *size = 0; - size_t arena_index = mi_arena_id_index(arena_id); - if (arena_index >= MI_MAX_ARENAS) return NULL; - mi_arena_t* arena = mi_atomic_load_ptr_acquire(mi_arena_t, &mi_arenas[arena_index]); - if (arena == NULL) return NULL; - if (size != NULL) { *size = mi_arena_block_size(arena->block_count); } - return arena->start; -} - - -/* ----------------------------------------------------------- - Arena purge ------------------------------------------------------------ */ - -static long mi_arena_purge_delay(void) { - // <0 = no purging allowed, 0=immediate purging, >0=milli-second delay - return (mi_option_get(mi_option_purge_delay) * mi_option_get(mi_option_arena_purge_mult)); -} - -// reset or decommit in an arena and update the committed/decommit bitmaps -// assumes we own the area (i.e. blocks_in_use is claimed by us) -static void mi_arena_purge(mi_arena_t* arena, size_t bitmap_idx, size_t blocks, mi_stats_t* stats) { - mi_assert_internal(arena->blocks_committed != NULL); - mi_assert_internal(arena->blocks_purge != NULL); - mi_assert_internal(!arena->memid.is_pinned); - const size_t size = mi_arena_block_size(blocks); - void* const p = mi_arena_block_start(arena, bitmap_idx); - bool needs_recommit; - if (_mi_bitmap_is_claimed_across(arena->blocks_committed, arena->field_count, blocks, bitmap_idx)) { - // all blocks are committed, we can purge freely - needs_recommit = _mi_os_purge(p, size, stats); - } - else { - // some blocks are not committed -- this can happen when a partially committed block is freed - // in `_mi_arena_free` and it is conservatively marked as uncommitted but still scheduled for a purge - // we need to ensure we do not try to reset (as that may be invalid for uncommitted memory), - // and also undo the decommit stats (as it was already adjusted) - mi_assert_internal(mi_option_is_enabled(mi_option_purge_decommits)); - needs_recommit = _mi_os_purge_ex(p, size, false /* allow reset? */, stats); - _mi_stat_increase(&stats->committed, size); - } - - // clear the purged blocks - _mi_bitmap_unclaim_across(arena->blocks_purge, arena->field_count, blocks, bitmap_idx); - // update committed bitmap - if (needs_recommit) { - _mi_bitmap_unclaim_across(arena->blocks_committed, arena->field_count, blocks, bitmap_idx); - } -} - -// Schedule a purge. This is usually delayed to avoid repeated decommit/commit calls. -// Note: assumes we (still) own the area as we may purge immediately -static void mi_arena_schedule_purge(mi_arena_t* arena, size_t bitmap_idx, size_t blocks, mi_stats_t* stats) { - mi_assert_internal(arena->blocks_purge != NULL); - const long delay = mi_arena_purge_delay(); - if (delay < 0) return; // is purging allowed at all? - - if (_mi_preloading() || delay == 0) { - // decommit directly - mi_arena_purge(arena, bitmap_idx, blocks, stats); - } - else { - // schedule decommit - mi_msecs_t expire = mi_atomic_loadi64_relaxed(&arena->purge_expire); - if (expire != 0) { - mi_atomic_addi64_acq_rel(&arena->purge_expire, delay/10); // add smallish extra delay - } - else { - mi_atomic_storei64_release(&arena->purge_expire, _mi_clock_now() + delay); - } - _mi_bitmap_claim_across(arena->blocks_purge, arena->field_count, blocks, bitmap_idx, NULL); - } -} - -// purge a range of blocks -// return true if the full range was purged. -// assumes we own the area (i.e. blocks_in_use is claimed by us) -static bool mi_arena_purge_range(mi_arena_t* arena, size_t idx, size_t startidx, size_t bitlen, size_t purge, mi_stats_t* stats) { - const size_t endidx = startidx + bitlen; - size_t bitidx = startidx; - bool all_purged = false; - while (bitidx < endidx) { - // count consecutive ones in the purge mask - size_t count = 0; - while (bitidx + count < endidx && (purge & ((size_t)1 << (bitidx + count))) != 0) { - count++; - } - if (count > 0) { - // found range to be purged - const mi_bitmap_index_t range_idx = mi_bitmap_index_create(idx, bitidx); - mi_arena_purge(arena, range_idx, count, stats); - if (count == bitlen) { - all_purged = true; - } - } - bitidx += (count+1); // +1 to skip the zero bit (or end) - } - return all_purged; -} - -// returns true if anything was purged -static bool mi_arena_try_purge(mi_arena_t* arena, mi_msecs_t now, bool force, mi_stats_t* stats) -{ - if (arena->memid.is_pinned || arena->blocks_purge == NULL) return false; - mi_msecs_t expire = mi_atomic_loadi64_relaxed(&arena->purge_expire); - if (expire == 0) return false; - if (!force && expire > now) return false; - - // reset expire (if not already set concurrently) - mi_atomic_casi64_strong_acq_rel(&arena->purge_expire, &expire, 0); - - // potential purges scheduled, walk through the bitmap - bool any_purged = false; - bool full_purge = true; - for (size_t i = 0; i < arena->field_count; i++) { - size_t purge = mi_atomic_load_relaxed(&arena->blocks_purge[i]); - if (purge != 0) { - size_t bitidx = 0; - while (bitidx < MI_BITMAP_FIELD_BITS) { - // find consecutive range of ones in the purge mask - size_t bitlen = 0; - while (bitidx + bitlen < MI_BITMAP_FIELD_BITS && (purge & ((size_t)1 << (bitidx + bitlen))) != 0) { - bitlen++; - } - // try to claim the longest range of corresponding in_use bits - const mi_bitmap_index_t bitmap_index = mi_bitmap_index_create(i, bitidx); - while( bitlen > 0 ) { - if (_mi_bitmap_try_claim(arena->blocks_inuse, arena->field_count, bitlen, bitmap_index)) { - break; - } - bitlen--; - } - // actual claimed bits at `in_use` - if (bitlen > 0) { - // read purge again now that we have the in_use bits - purge = mi_atomic_load_acquire(&arena->blocks_purge[i]); - if (!mi_arena_purge_range(arena, i, bitidx, bitlen, purge, stats)) { - full_purge = false; - } - any_purged = true; - // release the claimed `in_use` bits again - _mi_bitmap_unclaim(arena->blocks_inuse, arena->field_count, bitlen, bitmap_index); - } - bitidx += (bitlen+1); // +1 to skip the zero (or end) - } // while bitidx - } // purge != 0 - } - // if not fully purged, make sure to purge again in the future - if (!full_purge) { - const long delay = mi_arena_purge_delay(); - mi_msecs_t expected = 0; - mi_atomic_casi64_strong_acq_rel(&arena->purge_expire,&expected,_mi_clock_now() + delay); - } - return any_purged; -} - -static void mi_arenas_try_purge( bool force, bool visit_all, mi_stats_t* stats ) { - if (_mi_preloading() || mi_arena_purge_delay() <= 0) return; // nothing will be scheduled - - const size_t max_arena = mi_atomic_load_acquire(&mi_arena_count); - if (max_arena == 0) return; - - // allow only one thread to purge at a time - static mi_atomic_guard_t purge_guard; - mi_atomic_guard(&purge_guard) - { - mi_msecs_t now = _mi_clock_now(); - size_t max_purge_count = (visit_all ? max_arena : 1); - for (size_t i = 0; i < max_arena; i++) { - mi_arena_t* arena = mi_atomic_load_ptr_acquire(mi_arena_t, &mi_arenas[i]); - if (arena != NULL) { - if (mi_arena_try_purge(arena, now, force, stats)) { - if (max_purge_count <= 1) break; - max_purge_count--; - } - } - } - } -} - - -/* ----------------------------------------------------------- - Arena free ------------------------------------------------------------ */ - -void _mi_arena_free(void* p, size_t size, size_t committed_size, mi_memid_t memid, mi_stats_t* stats) { - mi_assert_internal(size > 0 && stats != NULL); - mi_assert_internal(committed_size <= size); - if (p==NULL) return; - if (size==0) return; - const bool all_committed = (committed_size == size); - - if (mi_memkind_is_os(memid.memkind)) { - // was a direct OS allocation, pass through - if (!all_committed && committed_size > 0) { - // if partially committed, adjust the committed stats (as `_mi_os_free` will increase decommit by the full size) - _mi_stat_decrease(&stats->committed, committed_size); - } - _mi_os_free(p, size, memid, stats); - } - else if (memid.memkind == MI_MEM_ARENA) { - // allocated in an arena - size_t arena_idx; - size_t bitmap_idx; - mi_arena_memid_indices(memid, &arena_idx, &bitmap_idx); - mi_assert_internal(arena_idx < MI_MAX_ARENAS); - mi_arena_t* arena = mi_atomic_load_ptr_acquire(mi_arena_t,&mi_arenas[arena_idx]); - mi_assert_internal(arena != NULL); - const size_t blocks = mi_block_count_of_size(size); - - // checks - if (arena == NULL) { - _mi_error_message(EINVAL, "trying to free from non-existent arena: %p, size %zu, memid: 0x%zx\n", p, size, memid); - return; - } - mi_assert_internal(arena->field_count > mi_bitmap_index_field(bitmap_idx)); - if (arena->field_count <= mi_bitmap_index_field(bitmap_idx)) { - _mi_error_message(EINVAL, "trying to free from non-existent arena block: %p, size %zu, memid: 0x%zx\n", p, size, memid); - return; - } - - // need to set all memory to undefined as some parts may still be marked as no_access (like padding etc.) - mi_track_mem_undefined(p,size); - - // potentially decommit - if (arena->memid.is_pinned || arena->blocks_committed == NULL) { - mi_assert_internal(all_committed); - } - else { - mi_assert_internal(arena->blocks_committed != NULL); - mi_assert_internal(arena->blocks_purge != NULL); - - if (!all_committed) { - // mark the entire range as no longer committed (so we recommit the full range when re-using) - _mi_bitmap_unclaim_across(arena->blocks_committed, arena->field_count, blocks, bitmap_idx); - mi_track_mem_noaccess(p,size); - if (committed_size > 0) { - // if partially committed, adjust the committed stats (is it will be recommitted when re-using) - // in the delayed purge, we now need to not count a decommit if the range is not marked as committed. - _mi_stat_decrease(&stats->committed, committed_size); - } - // note: if not all committed, it may be that the purge will reset/decommit the entire range - // that contains already decommitted parts. Since purge consistently uses reset or decommit that - // works (as we should never reset decommitted parts). - } - // (delay) purge the entire range - mi_arena_schedule_purge(arena, bitmap_idx, blocks, stats); - } - - // and make it available to others again - bool all_inuse = _mi_bitmap_unclaim_across(arena->blocks_inuse, arena->field_count, blocks, bitmap_idx); - if (!all_inuse) { - _mi_error_message(EAGAIN, "trying to free an already freed arena block: %p, size %zu\n", p, size); - return; - }; - } - else { - // arena was none, external, or static; nothing to do - mi_assert_internal(memid.memkind < MI_MEM_OS); - } - - // purge expired decommits - mi_arenas_try_purge(false, false, stats); -} - -// destroy owned arenas; this is unsafe and should only be done using `mi_option_destroy_on_exit` -// for dynamic libraries that are unloaded and need to release all their allocated memory. -static void mi_arenas_unsafe_destroy(void) { - const size_t max_arena = mi_atomic_load_relaxed(&mi_arena_count); - size_t new_max_arena = 0; - for (size_t i = 0; i < max_arena; i++) { - mi_arena_t* arena = mi_atomic_load_ptr_acquire(mi_arena_t, &mi_arenas[i]); - if (arena != NULL) { - if (arena->start != NULL && mi_memkind_is_os(arena->memid.memkind)) { - mi_atomic_store_ptr_release(mi_arena_t, &mi_arenas[i], NULL); - _mi_os_free(arena->start, mi_arena_size(arena), arena->memid, &_mi_stats_main); - } - else { - new_max_arena = i; - } - mi_arena_meta_free(arena, arena->meta_memid, arena->meta_size, &_mi_stats_main); - } - } - - // try to lower the max arena. - size_t expected = max_arena; - mi_atomic_cas_strong_acq_rel(&mi_arena_count, &expected, new_max_arena); -} - -// Purge the arenas; if `force_purge` is true, amenable parts are purged even if not yet expired -void _mi_arena_collect(bool force_purge, mi_stats_t* stats) { - mi_arenas_try_purge(force_purge, true /* visit all */, stats); -} - -// destroy owned arenas; this is unsafe and should only be done using `mi_option_destroy_on_exit` -// for dynamic libraries that are unloaded and need to release all their allocated memory. -void _mi_arena_unsafe_destroy_all(mi_stats_t* stats) { - mi_arenas_unsafe_destroy(); - _mi_arena_collect(true /* force purge */, stats); // purge non-owned arenas -} - -// Is a pointer inside any of our arenas? -bool _mi_arena_contains(const void* p) { - const size_t max_arena = mi_atomic_load_relaxed(&mi_arena_count); - for (size_t i = 0; i < max_arena; i++) { - mi_arena_t* arena = mi_atomic_load_ptr_acquire(mi_arena_t, &mi_arenas[i]); - if (arena != NULL && arena->start <= (const uint8_t*)p && arena->start + mi_arena_block_size(arena->block_count) > (const uint8_t*)p) { - return true; - } - } - return false; -} - - -/* ----------------------------------------------------------- - Add an arena. ------------------------------------------------------------ */ - -static bool mi_arena_add(mi_arena_t* arena, mi_arena_id_t* arena_id) { - mi_assert_internal(arena != NULL); - mi_assert_internal((uintptr_t)mi_atomic_load_ptr_relaxed(uint8_t,&arena->start) % MI_SEGMENT_ALIGN == 0); - mi_assert_internal(arena->block_count > 0); - if (arena_id != NULL) { *arena_id = -1; } - - size_t i = mi_atomic_increment_acq_rel(&mi_arena_count); - if (i >= MI_MAX_ARENAS) { - mi_atomic_decrement_acq_rel(&mi_arena_count); - return false; - } - arena->id = mi_arena_id_create(i); - mi_atomic_store_ptr_release(mi_arena_t,&mi_arenas[i], arena); - if (arena_id != NULL) { *arena_id = arena->id; } - return true; -} - -static bool mi_manage_os_memory_ex2(void* start, size_t size, bool is_large, int numa_node, bool exclusive, mi_memid_t memid, mi_arena_id_t* arena_id) mi_attr_noexcept -{ - if (arena_id != NULL) *arena_id = _mi_arena_id_none(); - if (size < MI_ARENA_BLOCK_SIZE) return false; - - if (is_large) { - mi_assert_internal(memid.initially_committed && memid.is_pinned); - } - - const size_t bcount = size / MI_ARENA_BLOCK_SIZE; - const size_t fields = _mi_divide_up(bcount, MI_BITMAP_FIELD_BITS); - const size_t bitmaps = (memid.is_pinned ? 2 : 4); - const size_t asize = sizeof(mi_arena_t) + (bitmaps*fields*sizeof(mi_bitmap_field_t)); - mi_memid_t meta_memid; - mi_arena_t* arena = (mi_arena_t*)mi_arena_meta_zalloc(asize, &meta_memid, &_mi_stats_main); // TODO: can we avoid allocating from the OS? - if (arena == NULL) return false; - - // already zero'd due to os_alloc - // _mi_memzero(arena, asize); - arena->id = _mi_arena_id_none(); - arena->memid = memid; - arena->exclusive = exclusive; - arena->meta_size = asize; - arena->meta_memid = meta_memid; - arena->block_count = bcount; - arena->field_count = fields; - arena->start = (uint8_t*)start; - arena->numa_node = numa_node; // TODO: or get the current numa node if -1? (now it allows anyone to allocate on -1) - arena->is_large = is_large; - arena->purge_expire = 0; - arena->search_idx = 0; - arena->blocks_dirty = &arena->blocks_inuse[fields]; // just after inuse bitmap - arena->blocks_committed = (arena->memid.is_pinned ? NULL : &arena->blocks_inuse[2*fields]); // just after dirty bitmap - arena->blocks_purge = (arena->memid.is_pinned ? NULL : &arena->blocks_inuse[3*fields]); // just after committed bitmap - // initialize committed bitmap? - if (arena->blocks_committed != NULL && arena->memid.initially_committed) { - memset((void*)arena->blocks_committed, 0xFF, fields*sizeof(mi_bitmap_field_t)); // cast to void* to avoid atomic warning - } - - // and claim leftover blocks if needed (so we never allocate there) - ptrdiff_t post = (fields * MI_BITMAP_FIELD_BITS) - bcount; - mi_assert_internal(post >= 0); - if (post > 0) { - // don't use leftover bits at the end - mi_bitmap_index_t postidx = mi_bitmap_index_create(fields - 1, MI_BITMAP_FIELD_BITS - post); - _mi_bitmap_claim(arena->blocks_inuse, fields, post, postidx, NULL); - } - return mi_arena_add(arena, arena_id); - -} - -bool mi_manage_os_memory_ex(void* start, size_t size, bool is_committed, bool is_large, bool is_zero, int numa_node, bool exclusive, mi_arena_id_t* arena_id) mi_attr_noexcept { - mi_memid_t memid = _mi_memid_create(MI_MEM_EXTERNAL); - memid.initially_committed = is_committed; - memid.initially_zero = is_zero; - memid.is_pinned = is_large; - return mi_manage_os_memory_ex2(start,size,is_large,numa_node,exclusive,memid, arena_id); -} - -// Reserve a range of regular OS memory -int mi_reserve_os_memory_ex(size_t size, bool commit, bool allow_large, bool exclusive, mi_arena_id_t* arena_id) mi_attr_noexcept { - if (arena_id != NULL) *arena_id = _mi_arena_id_none(); - size = _mi_align_up(size, MI_ARENA_BLOCK_SIZE); // at least one block - mi_memid_t memid; - void* start = _mi_os_alloc_aligned(size, MI_SEGMENT_ALIGN, commit, allow_large, &memid, &_mi_stats_main); - if (start == NULL) return ENOMEM; - const bool is_large = memid.is_pinned; // todo: use separate is_large field? - if (!mi_manage_os_memory_ex2(start, size, is_large, -1 /* numa node */, exclusive, memid, arena_id)) { - _mi_os_free_ex(start, size, commit, memid, &_mi_stats_main); - _mi_verbose_message("failed to reserve %zu k memory\n", _mi_divide_up(size, 1024)); - return ENOMEM; - } - _mi_verbose_message("reserved %zu KiB memory%s\n", _mi_divide_up(size, 1024), is_large ? " (in large os pages)" : ""); - return 0; -} - - -// Manage a range of regular OS memory -bool mi_manage_os_memory(void* start, size_t size, bool is_committed, bool is_large, bool is_zero, int numa_node) mi_attr_noexcept { - return mi_manage_os_memory_ex(start, size, is_committed, is_large, is_zero, numa_node, false /* exclusive? */, NULL); -} - -// Reserve a range of regular OS memory -int mi_reserve_os_memory(size_t size, bool commit, bool allow_large) mi_attr_noexcept { - return mi_reserve_os_memory_ex(size, commit, allow_large, false, NULL); -} - - -/* ----------------------------------------------------------- - Debugging ------------------------------------------------------------ */ - -static size_t mi_debug_show_bitmap(const char* prefix, mi_bitmap_field_t* fields, size_t field_count ) { - size_t inuse_count = 0; - for (size_t i = 0; i < field_count; i++) { - char buf[MI_BITMAP_FIELD_BITS + 1]; - uintptr_t field = mi_atomic_load_relaxed(&fields[i]); - for (size_t bit = 0; bit < MI_BITMAP_FIELD_BITS; bit++) { - bool inuse = ((((uintptr_t)1 << bit) & field) != 0); - if (inuse) inuse_count++; - buf[MI_BITMAP_FIELD_BITS - 1 - bit] = (inuse ? 'x' : '.'); - } - buf[MI_BITMAP_FIELD_BITS] = 0; - _mi_verbose_message("%s%s\n", prefix, buf); - } - return inuse_count; -} - -void mi_debug_show_arenas(void) mi_attr_noexcept { - size_t max_arenas = mi_atomic_load_relaxed(&mi_arena_count); - for (size_t i = 0; i < max_arenas; i++) { - mi_arena_t* arena = mi_atomic_load_ptr_relaxed(mi_arena_t, &mi_arenas[i]); - if (arena == NULL) break; - size_t inuse_count = 0; - _mi_verbose_message("arena %zu: %zu blocks with %zu fields\n", i, arena->block_count, arena->field_count); - inuse_count += mi_debug_show_bitmap(" ", arena->blocks_inuse, arena->field_count); - _mi_verbose_message(" blocks in use ('x'): %zu\n", inuse_count); - } -} - - -/* ----------------------------------------------------------- - Reserve a huge page arena. ------------------------------------------------------------ */ -// reserve at a specific numa node -int mi_reserve_huge_os_pages_at_ex(size_t pages, int numa_node, size_t timeout_msecs, bool exclusive, mi_arena_id_t* arena_id) mi_attr_noexcept { - if (arena_id != NULL) *arena_id = -1; - if (pages==0) return 0; - if (numa_node < -1) numa_node = -1; - if (numa_node >= 0) numa_node = numa_node % _mi_os_numa_node_count(); - size_t hsize = 0; - size_t pages_reserved = 0; - mi_memid_t memid; - void* p = _mi_os_alloc_huge_os_pages(pages, numa_node, timeout_msecs, &pages_reserved, &hsize, &memid); - if (p==NULL || pages_reserved==0) { - _mi_warning_message("failed to reserve %zu GiB huge pages\n", pages); - return ENOMEM; - } - _mi_verbose_message("numa node %i: reserved %zu GiB huge pages (of the %zu GiB requested)\n", numa_node, pages_reserved, pages); - - if (!mi_manage_os_memory_ex2(p, hsize, true, numa_node, exclusive, memid, arena_id)) { - _mi_os_free(p, hsize, memid, &_mi_stats_main); - return ENOMEM; - } - return 0; -} - -int mi_reserve_huge_os_pages_at(size_t pages, int numa_node, size_t timeout_msecs) mi_attr_noexcept { - return mi_reserve_huge_os_pages_at_ex(pages, numa_node, timeout_msecs, false, NULL); -} - -// reserve huge pages evenly among the given number of numa nodes (or use the available ones as detected) -int mi_reserve_huge_os_pages_interleave(size_t pages, size_t numa_nodes, size_t timeout_msecs) mi_attr_noexcept { - if (pages == 0) return 0; - - // pages per numa node - size_t numa_count = (numa_nodes > 0 ? numa_nodes : _mi_os_numa_node_count()); - if (numa_count <= 0) numa_count = 1; - const size_t pages_per = pages / numa_count; - const size_t pages_mod = pages % numa_count; - const size_t timeout_per = (timeout_msecs==0 ? 0 : (timeout_msecs / numa_count) + 50); - - // reserve evenly among numa nodes - for (size_t numa_node = 0; numa_node < numa_count && pages > 0; numa_node++) { - size_t node_pages = pages_per; // can be 0 - if (numa_node < pages_mod) node_pages++; - int err = mi_reserve_huge_os_pages_at(node_pages, (int)numa_node, timeout_per); - if (err) return err; - if (pages < node_pages) { - pages = 0; - } - else { - pages -= node_pages; - } - } - - return 0; -} - -int mi_reserve_huge_os_pages(size_t pages, double max_secs, size_t* pages_reserved) mi_attr_noexcept { - MI_UNUSED(max_secs); - _mi_warning_message("mi_reserve_huge_os_pages is deprecated: use mi_reserve_huge_os_pages_interleave/at instead\n"); - if (pages_reserved != NULL) *pages_reserved = 0; - int err = mi_reserve_huge_os_pages_interleave(pages, 0, (size_t)(max_secs * 1000.0)); - if (err==0 && pages_reserved!=NULL) *pages_reserved = pages; - return err; -} diff --git a/Objects/mimalloc/arena/arena-alloc.c b/Objects/mimalloc/arena/arena-alloc.c new file mode 100644 index 000000000000000..67f742ce2744b42 --- /dev/null +++ b/Objects/mimalloc/arena/arena-alloc.c @@ -0,0 +1,552 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2019-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +#include "mimalloc.h" +#include "mimalloc/util/stats.h" +#include "mimalloc/alloc/free.h" // mi_free_subproc_safe +#include "mimalloc/alloc/page.h" +#include "mimalloc/heap/subproc.h" +#include "mimalloc/arena/arena-alloc.h" +#include "arena.h" + + +mi_decl_nodiscard bool _mi_arena_commit(mi_subproc_t* subproc, mi_arena_t* arena, void* start, size_t size, bool* is_zero, size_t stat_already_committed) { + mi_assert_internal(subproc!=NULL); + if (arena != NULL && arena->commit_fun != NULL) { + return (*arena->commit_fun)(true, start, size, is_zero, arena->commit_fun_arg); + } + else if (stat_already_committed > 0) { + return _mi_os_commit_ex(subproc, start, size, is_zero, stat_already_committed); + } + else { + return _mi_os_commit(subproc, start, size, is_zero); + } +} + +// Create an arena memid +static mi_memid_t mi_memid_create_arena(mi_arena_t* arena, size_t slice_index, size_t slice_count) { + mi_assert_internal(slice_index < UINT32_MAX); + mi_assert_internal(slice_count < UINT32_MAX); + mi_assert_internal(slice_count > 0); + mi_assert_internal(slice_index < arena->slice_count); + mi_memid_t memid = _mi_memid_create(MI_MEM_ARENA); + memid.mem.arena.arena = arena; + memid.mem.arena.slice_index = (uint32_t)slice_index; + memid.mem.arena.slice_count = (uint32_t)slice_count; + return memid; +} + +/* ----------------------------------------------------------- + Arena Allocation +----------------------------------------------------------- */ + +static mi_decl_noinline void* mi_arena_try_alloc_at( + mi_arena_t* arena, size_t slice_count, bool commit, size_t tseq, mi_memid_t* memid) +{ + mi_assert_internal(arena!=NULL); + mi_assert_internal(slice_count>0); + size_t slice_index; + if (!mi_bbitmap_try_find_and_clearN(arena->slices_free, tseq, slice_count, &slice_index)) return NULL; + + // claimed it! + void* p = mi_arena_slice_start(arena, slice_index); + *memid = mi_memid_create_arena(arena, slice_index, slice_count); + memid->is_pinned = arena->memid.is_pinned; + + // set the dirty bits and track which slices become accessible + size_t touched_slices = slice_count; + if (arena->memid.initially_zero) { + size_t already_dirty = 0; + memid->initially_zero = mi_bitmap_setN(arena->slices_dirty, slice_index, slice_count, &already_dirty); + mi_assert_internal(already_dirty <= touched_slices); + touched_slices -= already_dirty; + } + else { + // todo: properly count touched pages with a separate bitmap? + touched_slices = 0; + } + + // set commit state + if (commit) { + // commit requested, but the range may not be committed as a whole: ensure it is committed now + const size_t already_committed = mi_bitmap_popcountN(arena->slices_committed, slice_index, slice_count); + if (already_committed < slice_count) { + // not all committed, try to commit now + bool commit_zero = false; + if (!_mi_arena_commit(arena->subproc, arena, p, _mi_size_of_slices(slice_count), &commit_zero, _mi_size_of_slices(already_committed))) { + // if the commit fails, release ownership, and return NULL; + // note: this does not roll back dirty bits but that is ok. + mi_bbitmap_setN(arena->slices_free, slice_index, slice_count); + return NULL; + } + if (commit_zero) { + memid->initially_zero = true; + } + + // set the commit bits + mi_bitmap_setN(arena->slices_committed, slice_index, slice_count, NULL); + + // committed + #if MI_DEBUG > 1 + if (memid->initially_zero) { + if (!mi_mem_is_zero(p, _mi_size_of_slices(slice_count))) { + _mi_error_message(EFAULT, "internal error: arena allocation was not zero-initialized!\n"); + memid->initially_zero = false; + } + } + #endif + } + else { + // already fully committed. + _mi_os_reuse(arena->subproc, p, _mi_size_of_slices(slice_count)); + // if the OS has overcommit, and this is the first time we access these pages, then + // count the commit now (as at arena reserve we didn't count those commits as these are on-demand) + if (_mi_os_has_overcommit() && touched_slices > 0 && !arena->memid.is_pinned /* huge pages, issue #1236 */) { + mi_subproc_stat_increase( arena->subproc, committed, _mi_size_of_slices(touched_slices)); + } + } + + mi_assert_internal(mi_bitmap_is_setN(arena->slices_committed, slice_index, slice_count)); + memid->initially_committed = true; + + // tool support + if (memid->initially_zero) { + mi_track_mem_defined(p, slice_count * MI_ARENA_SLICE_SIZE); + } + else { + mi_track_mem_undefined(p, slice_count * MI_ARENA_SLICE_SIZE); + } + } + else { + // no need to commit, but check if it is already fully committed + memid->initially_committed = mi_bitmap_is_setN(arena->slices_committed, slice_index, slice_count); + if (!memid->initially_committed) { + // partly committed.. adjust stats + size_t already_committed_count = 0; + mi_bitmap_setN(arena->slices_committed, slice_index, slice_count, &already_committed_count); + mi_bitmap_clearN(arena->slices_committed, slice_index, slice_count); + mi_subproc_stat_decrease(arena->subproc, committed, _mi_size_of_slices(already_committed_count)); + } + } + + mi_assert_internal(mi_bbitmap_is_clearN(arena->slices_free, slice_index, slice_count)); + if (commit) { mi_assert_internal(mi_bitmap_is_setN(arena->slices_committed, slice_index, slice_count)); } + if (commit) { mi_assert_internal(memid->initially_committed); } + mi_assert_internal(mi_bitmap_is_setN(arena->slices_dirty, slice_index, slice_count)); + + return p; +} + +/* ----------------------------------------------------------- + Arena allocation +----------------------------------------------------------- */ + +typedef struct mi_arena_alloc_visit_info_s { + size_t slice_count; + bool commit; + size_t tseq; + mi_memid_t* memid; +} mi_arena_alloc_visit_info_t; + +static bool mi_arena_try_alloc_visit(mi_arena_t* arena, const void* arg, void** result) { + const mi_arena_alloc_visit_info_t* info = (const mi_arena_alloc_visit_info_t*)arg; + mi_assert_internal(result != NULL); + *result = mi_arena_try_alloc_at(arena, info->slice_count, info->commit, info->tseq, info->memid); + if (*result != NULL) return false; // break + return true; // continue +} + +// allocate slices from the arenas +static mi_decl_noinline void* mi_arenas_try_find_free( + mi_heap_t* heap, size_t slice_count, size_t alignment, + bool commit, bool allow_large, mi_arena_t* req_arena, size_t tseq, int numa_node, mi_memid_t* memid) +{ + // mi_assert_internal(slice_count <= _mi_slice_count_of_size(MI_ARENA_MAX_CHUNK_OBJ_SIZE)); + mi_assert(alignment <= MI_ARENA_SLICE_ALIGN); + if (alignment > MI_ARENA_SLICE_ALIGN) return NULL; + + // search arena's + const mi_arena_alloc_visit_info_t info = { slice_count, commit, tseq, memid }; + void* p = NULL; + if (!mi_forall_suitable_arenas(heap, req_arena, tseq, true /* only numa matching */, numa_node, allow_large, &mi_arena_try_alloc_visit, &info, &p)) { + return p; + } + if (numa_node < 0) return NULL; + + // search again but now regardless of preferred numa affinity + mi_forall_suitable_arenas(heap, req_arena, tseq, false /* numa non-matching now */, numa_node, allow_large, &mi_arena_try_alloc_visit, &info, &p); + return p; +} + +// Allocate slices from the arena's -- potentially allocating a fresh arena +mi_decl_noinline void* _mi_arenas_try_alloc( + mi_heap_t* heap, + size_t slice_count, size_t alignment, + bool commit, bool allow_large, + mi_arena_t* req_arena, size_t tseq, int numa_node, mi_memid_t* memid) +{ + // mi_assert(slice_count <= MI_ARENA_MAX_CHUNK_OBJ_SLICES); + mi_assert(alignment <= MI_ARENA_SLICE_ALIGN); + void* p; + + // not too large? + if (slice_count * MI_ARENA_SLICE_SIZE > MI_ARENA_MAX_SIZE) return NULL; + + // try to find free slices in the arena's + p = mi_arenas_try_find_free(heap, slice_count, alignment, commit, allow_large, req_arena, tseq, numa_node, memid); + if (p != NULL) return p; + + // did we need a specific arena? + if (req_arena != NULL) return NULL; + + // don't create arena's if OS allocation is disallowed + if (mi_option_is_enabled(mi_option_disallow_os_alloc)) return NULL; + + // otherwise, try to reserve a new arena -- but one thread at a time.. (todo: allow 2 or 4 to reduce contention?) + mi_subproc_t* const subproc = heap->subproc; + const size_t arena_count = mi_arenas_get_count(subproc); + mi_lock(&subproc->arena_reserve_lock) { + if (arena_count == mi_arenas_get_count(subproc)) { + // we are the first to enter the lock, reserve a fresh arena + mi_arena_id_t arena_id = _mi_arena_id_none(); + _mi_arena_reserve(subproc, _mi_size_of_slices(slice_count), allow_large, &arena_id); + } + else { + // another thread already reserved a new arena + } + } + // try once more to allocate in the new arena + mi_assert_internal(req_arena == NULL); + p = mi_arenas_try_find_free(heap, slice_count, alignment, commit, allow_large, req_arena, tseq, numa_node, memid); + if (p != NULL) return p; + + return NULL; +} + +// Allocate from the OS (if allowed) +void* _mi_arena_os_alloc_aligned( + mi_subproc_t* subproc, + size_t size, size_t alignment, size_t align_offset, + bool commit, bool allow_large, + mi_arena_id_t req_arena_id, mi_memid_t* memid) +{ + // if we cannot use OS allocation, return NULL + if (mi_option_is_enabled(mi_option_disallow_os_alloc) || req_arena_id != _mi_arena_id_none()) { + errno = ENOMEM; + return NULL; + } + + if (align_offset > 0) { + return _mi_os_alloc_aligned_at_offset(subproc, size, alignment, align_offset, commit, allow_large, memid); + } + else { + return _mi_os_alloc_aligned(subproc, size, alignment, commit, allow_large, memid); + } +} + + +// Allocate large sized memory +void* _mi_arenas_alloc_aligned( mi_heap_t* heap, + size_t size, size_t alignment, size_t align_offset, + bool commit, bool allow_large, + mi_arena_t* req_arena, size_t tseq, int numa_node, mi_memid_t* memid) +{ + mi_assert_internal(memid != NULL); + mi_assert_internal(size > 0); + + // try to allocate in an arena if the alignment is small enough and the object is not too small (as for theap meta data) + if (!mi_option_is_enabled(mi_option_disallow_arena_alloc) && // is arena allocation allowed? + size >= MI_ARENA_MIN_OBJ_SIZE && size <= mi_arena_max_object_size() && // and not too small or too large + alignment <= MI_ARENA_SLICE_ALIGN && align_offset == 0) // and good alignment + { + const size_t slice_count = _mi_slice_count_of_size(size); + void* p = _mi_arenas_try_alloc(heap, slice_count, alignment, commit, allow_large, req_arena, tseq, numa_node, memid); + if (p != NULL) return p; + } + + // fall back to the OS + void* p = _mi_arena_os_alloc_aligned(heap->subproc, size, alignment, align_offset, commit, allow_large, req_arena, memid); + return p; +} + +void* _mi_arenas_alloc(mi_heap_t* heap, size_t size, bool commit, bool allow_large, mi_arena_t* req_arena, size_t tseq, int numa_node, mi_memid_t* memid) +{ + return _mi_arenas_alloc_aligned(heap, size, MI_ARENA_SLICE_SIZE, 0, commit, allow_large, req_arena, tseq, numa_node, memid); +} + +/* ----------------------------------------------------------- + Arena free +----------------------------------------------------------- */ +static void mi_arena_schedule_purge(mi_arena_t* arena, size_t slice_index, size_t slices); +static void mi_arenas_try_purge(bool force, bool visit_all, mi_subproc_t* subproc, size_t tseq); + +void _mi_arenas_free(mi_subproc_t* subproc, void* p, size_t size, mi_memid_t memid) { + if (p==NULL) return; + if (size==0) return; + + // need to set all memory to undefined as some parts may still be marked as no_access (like padding etc.) + mi_track_mem_undefined(p, size); + + if (mi_memkind_is_os(memid.memkind)) { + // was a direct OS allocation, pass through + _mi_os_free(subproc, p, size, memid); + } + else if (memid.memkind == MI_MEM_ARENA) { + // allocated in an arena + size_t slice_count; + size_t slice_index; + mi_arena_t* arena = mi_arena_from_memid(memid, &slice_index, &slice_count); + mi_assert_internal(arena!=NULL); + mi_assert_internal(arena->subproc == subproc); + mi_assert_internal((size%MI_ARENA_SLICE_SIZE)==0); + mi_assert_internal((slice_count*MI_ARENA_SLICE_SIZE)==size); + mi_assert_internal(mi_arena_slice_start(arena,slice_index) <= (uint8_t*)p); + mi_assert_internal(mi_arena_slice_start(arena,slice_index) + _mi_size_of_slices(slice_count) > (uint8_t*)p); + // checks + if (arena == NULL) { + _mi_error_message(EINVAL, "trying to free from an invalid arena: %p, size %zu, memkind: 0x%x\n", p, size, memid.memkind); + return; + } + mi_assert_internal(slice_index < arena->slice_count); + mi_assert_internal(slice_index >= mi_arena_info_slices(arena)); + if (slice_index < mi_arena_info_slices(arena) || slice_index >= arena->slice_count) { + _mi_error_message(EINVAL, "trying to free from an invalid arena block: %p, size %zu, memkind: 0x%x\n", p, size, memid.memkind); + return; + } + + // potentially decommit + if (!arena->memid.is_pinned /* && !arena->memid.initially_committed */) { // todo: allow decommit even if initially committed? + // (delay) purge the page + mi_arena_schedule_purge(arena, slice_index, slice_count); + } + + // and make it available to others again + bool all_inuse = mi_bbitmap_setN(arena->slices_free, slice_index, slice_count); + if (!all_inuse) { + _mi_error_message(EAGAIN, "trying to free an already freed arena block: %p, size %zu\n", mi_arena_slice_start(arena,slice_index), _mi_size_of_slices(slice_count)); + return; + }; + } + else if (memid.memkind == MI_MEM_MALLOC) { + _mi_free_subproc_safe(p); + } + else { + // arena was none, external, or static; nothing to do + mi_assert_internal(mi_memid_needs_no_free(memid)); + } + + // try to purge expired decommits + // mi_arenas_try_purge(false, false, NULL); +} + +// Purge the arenas; if `force_purge` is true, amenable parts are purged even if not yet expired +void _mi_arenas_collect(bool force_purge, bool visit_all, mi_tld_t* tld) { + mi_arenas_try_purge(force_purge, visit_all, tld->subproc, tld->thread_seq); +} + +/* ----------------------------------------------------------- + Arena purge +----------------------------------------------------------- */ + +static long mi_arena_purge_delay(void) { + // <0 = no purging allowed, 0=immediate purging, >0=milli-second delay + const long delay = mi_option_get(mi_option_purge_delay); + const long mult = mi_option_get(mi_option_arena_purge_mult); + if (delay<0 || mult<0) { return -1; } + if (delay==0 || mult==0) { return 0; } + size_t total; + if (mi_mul_overflow((size_t)delay, (size_t)mult, &total)) { return delay; } + if (total > LONG_MAX) { return delay; } + return (long)total; +} + +// reset or decommit in an arena and update the commit bitmap +// assumes we own the area (i.e. slices_free is claimed by us) +// returns if the memory is no longer committed (versus reset which keeps the commit) +static bool mi_arena_purge(mi_arena_t* arena, size_t slice_index, size_t slice_count) { + mi_assert_internal(!arena->memid.is_pinned); + mi_assert_internal(mi_bbitmap_is_clearN(arena->slices_free, slice_index, slice_count)); + + const size_t size = _mi_size_of_slices(slice_count); + void* const p = mi_arena_slice_start(arena, slice_index); + //const bool all_committed = mi_bitmap_is_setN(arena->slices_committed, slice_index, slice_count); + size_t already_committed; + mi_bitmap_setN(arena->slices_committed, slice_index, slice_count, &already_committed); // pretend all committed.. (as we lack a clearN call that counts the already set bits..) + const bool all_committed = (already_committed == slice_count); + const bool needs_recommit = _mi_os_purge_ex(arena->subproc, p, size, all_committed /* allow reset? */, _mi_size_of_slices(already_committed), arena->commit_fun, arena->commit_fun_arg); + + if (needs_recommit) { + // no longer committed + mi_bitmap_clearN(arena->slices_committed, slice_index, slice_count); + // we just counted in the purge to decommit all, but the some part was not committed so adjust that here + // mi_subproc_stat_decrease(arena->subproc, committed, _mi_size_of_slices(slice_count - already_committed)); + } + else if (!all_committed) { + // we cannot assume any of these are committed any longer (even with reset since we did setN and may have marked uncommitted slices as committed) + mi_bitmap_clearN(arena->slices_committed, slice_index, slice_count); + // we adjust the commit count as parts will be re-committed + // mi_subproc_stat_decrease(arena->subproc, committed, _mi_size_of_slices(already_committed)); + } + + return needs_recommit; +} + + +// Schedule a purge. This is usually delayed to avoid repeated decommit/commit calls. +// Note: assumes we (still) own the area as we may purge immediately +static void mi_arena_schedule_purge(mi_arena_t* arena, size_t slice_index, size_t slice_count) { + const long delay = mi_arena_purge_delay(); + if (arena->memid.is_pinned || delay < 0 || _mi_preloading()) return; // is purging allowed at all? + + mi_assert_internal(mi_bbitmap_is_clearN(arena->slices_free, slice_index, slice_count)); + if (delay == 0 || _mi_size_of_slices(slice_count) >= mi_option_get_size(mi_option_arena_purge_immediate_size)) { + // purge directly + mi_arena_purge(arena, slice_index, slice_count); + } + else { + // schedule purge + const mi_msecs_t expire = _mi_clock_now() + delay; + mi_msecs_t expire0 = 0; + if (mi_atomic_casi64_strong_acq_rel(&arena->purge_expire, &expire0, expire)) { + // expiration was not yet set + // maybe set the global arenas expire as well (if it wasn't set already) + mi_assert_internal(expire0==0); + mi_atomic_casi64_strong_acq_rel(&arena->subproc->purge_expire, &expire0, expire); + } + else { + // already an expiration was set + } + mi_bitmap_setN(arena->slices_purge, slice_index, slice_count, NULL); + } +} + +typedef struct mi_purge_visit_info_s { + mi_msecs_t now; + mi_msecs_t delay; + bool all_purged; + bool any_purged; +} mi_purge_visit_info_t; + +static bool mi_arena_try_purge_range(mi_arena_t* arena, size_t slice_index, size_t slice_count) { + mi_assert(slice_count < MI_BCHUNK_BITS); + if (mi_bbitmap_try_clearNC(arena->slices_free, slice_index, slice_count)) { + // purge + bool decommitted = mi_arena_purge(arena, slice_index, slice_count); MI_UNUSED(decommitted); + mi_assert_internal(!decommitted || mi_bitmap_is_clearN(arena->slices_committed, slice_index, slice_count)); + // and reset the free range + mi_bbitmap_setN(arena->slices_free, slice_index, slice_count); + return true; + } + else { + // was allocated again already + return false; + } +} + +static bool mi_arena_try_purge_visitor(size_t slice_index, size_t slice_count, mi_arena_t* arena, void* arg) { + mi_purge_visit_info_t* vinfo = (mi_purge_visit_info_t*)arg; + // try to purge: first claim the free blocks + if (mi_arena_try_purge_range(arena, slice_index, slice_count)) { + vinfo->any_purged = true; + vinfo->all_purged = true; + } + else if (slice_count > 1) + { + // failed to claim the full range, try per slice instead + for (size_t i = 0; i < slice_count; i++) { + const bool purged = mi_arena_try_purge_range(arena, slice_index + i, 1); + vinfo->any_purged = vinfo->any_purged || purged; + vinfo->all_purged = vinfo->all_purged && purged; + } + } + // don't clear the purge bits as that is done atomically be the _bitmap_forall_set_ranges + // mi_bitmap_clearN(arena->slices_purge, slice_index, slice_count); + return true; // continue +} + +// returns +// -1 = nothing was purged +// 0 = nothing was purged yet because have not yet reached the expire time +// 1 = some pages in the arena were purged +static int mi_arena_try_purge(mi_arena_t* arena, mi_msecs_t now, bool force) +{ + // check pre-conditions + if (arena->memid.is_pinned) return -1; + + // expired yet? + mi_msecs_t expire = mi_atomic_loadi64_relaxed(&arena->purge_expire); + if (expire==0) return -1; + if (!force && expire > now) return 0; + + // reset expire + mi_atomic_storei64_release(&arena->purge_expire, (mi_msecs_t)0); + mi_subproc_stat_counter_increase(arena->subproc, arena_purges, 1); + + // go through all purge info's (with max MI_BFIELD_BITS ranges at a time) + // this also clears those ranges atomically (so any newly freed blocks will get purged next + // time around) + mi_purge_visit_info_t vinfo = { now, mi_arena_purge_delay(), true /*all?*/, false /*any?*/}; + + // we purge by at least `minslices` to not fragment transparent huge pages for example + const size_t minslices = _mi_slice_count_of_size(_mi_os_minimal_purge_size()); + _mi_bitmap_forall_setc_rangesn(arena->slices_purge, minslices, &mi_arena_try_purge_visitor, arena, &vinfo); + + return (vinfo.any_purged ? 1 : -1); +} + + +static void mi_arenas_try_purge(bool force, bool visit_all, mi_subproc_t* subproc, size_t tseq) +{ + // try purge can be called often so try to only run when needed + const long delay = mi_arena_purge_delay(); + if (_mi_preloading() || delay <= 0) return; // nothing will be scheduled + + // check if any arena needs purging? + const mi_msecs_t now = _mi_clock_now(); + const mi_msecs_t arenas_expire = mi_atomic_loadi64_acquire(&subproc->purge_expire); + if (!visit_all && !force && (arenas_expire == 0 || arenas_expire > now)) return; + + const size_t max_arena = mi_arenas_get_count(subproc); + if (max_arena == 0) return; + + // allow only one thread to purge at a time (todo: allow concurrent purging?) + static mi_atomic_guard_t purge_guard; + mi_atomic_guard(&purge_guard) + { + // increase global expire: at most one purge per delay cycle + if (arenas_expire > now) { mi_atomic_storei64_release(&subproc->purge_expire, now + (delay/10)); } + const size_t arena_start = tseq % max_arena; + size_t max_purge_count = (visit_all ? max_arena : (max_arena/4)+1); + bool all_visited = true; + bool any_purged = false; + for (size_t _i = 0; _i < max_arena; _i++) { + size_t i = _i + arena_start; + if (i >= max_arena) { i -= max_arena; } + mi_arena_t* arena = mi_arena_from_index(subproc,i); + if (arena != NULL) { + const int purged = mi_arena_try_purge(arena, now, force); + if (purged >= 0) { // purged, or arena expire is not yet reached + any_purged = true; + if (purged >= 1) { // purged + if (max_purge_count <= 1) { + all_visited = false; + break; + } + max_purge_count--; + } + } + } + } + if (all_visited && !any_purged) { + mi_atomic_storei64_release(&subproc->purge_expire, (mi_msecs_t)0); + } + } +} + +void mi_arenas_purge(void) mi_attr_noexcept { + mi_arenas_try_purge(true, true, _mi_subproc_main(), 0); +} diff --git a/Objects/mimalloc/arena/arena-page.c b/Objects/mimalloc/arena/arena-page.c new file mode 100644 index 000000000000000..c0c1ace23eb40c3 --- /dev/null +++ b/Objects/mimalloc/arena/arena-page.c @@ -0,0 +1,1362 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2019-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +#include "mimalloc.h" +#include "mimalloc/prim/prim-tls.h" +#include "mimalloc/util/stats.h" +#include "mimalloc/alloc/page.h" +#include "mimalloc/arena/arena-alloc.h" +#include "mimalloc/profile/sample-guarded.h" // _mi_page_unguard_all +#include "mimalloc/arena/arena-page.h" +#include "arena.h" + +/* ----------------------------------------------------------- + Util +----------------------------------------------------------- */ + +#if MI_DEBUG > 1 +static bool mi_heap_has_page(mi_heap_t* heap, mi_arena_t* arena, mi_page_t* page) { + mi_assert(arena->arena_idx < MI_MAX_ARENAS); + mi_arena_pages_t* arena_pages = heap->arena_pages[arena->arena_idx]; + return (page->memid.memkind == MI_MEM_ARENA && + page->memid.mem.arena.arena == arena && + arena_pages != NULL && + mi_bitmap_is_setN(arena_pages->pages, page->memid.mem.arena.slice_index, 1)); +} +#endif + +mi_page_t* mi_arena_page_at_slice(mi_arena_t* arena, size_t slice_index) { + mi_assert_internal(slice_index < arena->slice_count); + #if MI_PAGE_META_IS_ALIGNED + mi_page_t* const page = mi_aligned_ptr_page(mi_arena_slice_start(arena,slice_index)); // todo: optimize? + return page; + #else + if (arena->pages_meta != NULL) { + mi_page_t* const page = &arena->pages_meta[slice_index]; + #if MI_PAGE_META_SMALL_IS_ALIGNED + // pages with small blocks still have the page at the start of the slice (and set the `block_size` in pages_meta to 0) + if (page->block_size>0) return page; + #else + return page; + #endif + } + // fall through (for MI_PAGE_META_SMALL_IS_ALIGNED) + return (mi_page_t*)mi_arena_slice_start(arena,slice_index); + #endif +} + +static size_t mi_page_full_size(mi_page_t* page) { + if (page->memid.memkind == MI_MEM_ARENA) { + return page->memid.mem.arena.slice_count * MI_ARENA_SLICE_SIZE; + } + else if (mi_memid_is_os(page->memid) || page->memid.memkind == MI_MEM_EXTERNAL) { + mi_assert_internal((uint8_t*)page->memid.mem.os.base <= (uint8_t*)page); + const ptrdiff_t presize = (uint8_t*)page - (uint8_t*)page->memid.mem.os.base; + mi_assert_internal((ptrdiff_t)page->memid.mem.os.size >= presize); + return (presize > (ptrdiff_t)page->memid.mem.os.size ? 0 : page->memid.mem.os.size - presize); + } + else { + return 0; + } +} + +/* ----------------------------------------------------------- + Arena pages: these bitmaps track abandoned pages per size + bin, and the overall pages belonging to a particular heap. +----------------------------------------------------------- */ + +static mi_arena_pages_t* mi_heap_arena_pages(mi_heap_t* heap, mi_arena_t* arena) { + mi_assert_internal(arena!=NULL); + mi_assert_internal(heap!=NULL); + mi_assert(arena->arena_idx < MI_MAX_ARENAS); + return mi_atomic_load_ptr_acquire(mi_arena_pages_t, &heap->arena_pages[arena->arena_idx]); +} + + +static size_t mi_arena_pages_size(size_t slice_count, size_t* bitmap_base) { + if (slice_count == 0) slice_count = MI_BCHUNK_BITS; + mi_assert_internal((slice_count % MI_BCHUNK_BITS) == 0); + const size_t base_size = mi_align_up(sizeof(mi_arena_pages_t), MI_BCHUNK_SIZE); + const size_t bitmaps_count = 1 + MI_ARENA_BIN_COUNT; // pages, and abandoned + const size_t bitmaps_size = bitmaps_count * mi_bitmap_size(slice_count, NULL); + const size_t size = base_size + bitmaps_size; + if (bitmap_base != NULL) *bitmap_base = base_size; + return size; +} + +// allocate initial arena_pages from the main heap +static mi_arena_pages_t* mi_arena_pages_alloc(mi_arena_t* arena) { + const size_t slice_count = arena->slice_count; + size_t bitmap_base = 0; + const size_t size = mi_arena_pages_size(slice_count, &bitmap_base); + mi_arena_pages_t* arena_pages = (mi_arena_pages_t*)mi_heap_zalloc_aligned(arena->subproc->heap_main, size, MI_BCHUNK_SIZE); + if (arena_pages==NULL) return NULL; + uint8_t* base = (uint8_t*)arena_pages + bitmap_base; + mi_assert_internal(mi_is_aligned(base, MI_BCHUNK_SIZE)); + arena_pages->pages = mi_arena_bitmap_init(slice_count, &base); + for (size_t i = 0; i < MI_ARENA_BIN_COUNT; i++) { + arena_pages->pages_abandoned[i] = mi_arena_bitmap_init(slice_count, &base); + } + return arena_pages; +} + +static mi_arena_t* mi_page_arena_pages(mi_page_t* page, size_t* slice_index, size_t* slice_count, mi_arena_pages_t** parena_pages) { + // todo: maybe store the arena* directly in the page? + mi_assert_internal(mi_page_is_owned(page)); + mi_arena_t* const arena = mi_arena_from_memid(page->memid, slice_index, slice_count); + mi_assert_internal(arena != NULL); + if (parena_pages != NULL) { + mi_heap_t* heap = mi_page_heap(page); + mi_arena_pages_t* const arena_pages = mi_heap_arena_pages(heap, arena); + mi_assert_internal(arena_pages != NULL); + mi_assert_internal(slice_index==NULL || mi_bitmap_is_set(arena_pages->pages, *slice_index)); + *parena_pages = arena_pages; + } + return arena; +} + +static mi_arena_pages_t* mi_heap_ensure_arena_pages(mi_heap_t* heap, mi_arena_t* arena) { + mi_assert_internal(arena!=NULL); + mi_assert_internal(heap!=NULL); + mi_assert(arena->arena_idx < MI_MAX_ARENAS); + mi_arena_pages_t* arena_pages = mi_heap_arena_pages(heap, arena); + if (arena_pages==NULL) { + mi_lock(&heap->arena_pages_lock) { + arena_pages = mi_atomic_load_ptr_acquire(mi_arena_pages_t, &heap->arena_pages[arena->arena_idx]); + if (arena_pages == NULL) { // still NULL? + if (mi_is_heap_main(heap)) { + // the page info for the main heap is always allocated as part of an arena + arena_pages = &arena->pages_main; + } + else { + // always allocate the arena pages info from the main heap + // todo: allocate into the current arena? + arena_pages = mi_arena_pages_alloc(arena); + } + mi_atomic_store_ptr_release(mi_arena_pages_t, &heap->arena_pages[arena->arena_idx], arena_pages); + } + } + } + if (mi_is_heap_main(heap)) { mi_assert(arena_pages != NULL); } // can never fail + return arena_pages; +} + + +/* ----------------------------------------------------------- + Claim abandoned pages +----------------------------------------------------------- */ + +static bool mi_arena_try_claim_abandoned(size_t slice_index, mi_arena_t* arena, bool* keep_abandoned) { + // found an abandoned page of the right size + mi_page_t* const page = mi_arena_page_at_slice(arena, slice_index); + // can we claim ownership? + if (!mi_page_claim_ownership(page)) { + // there was a concurrent free that reclaims this page .. + // we need to keep it in the abandoned map as the free will call `mi_arena_page_unabandon`, + // and wait for readers (us!) to finish. This is why it is very important to set the abandoned + // bit again (or otherwise the unabandon will never stop waiting). + *keep_abandoned = true; + return false; + } + else { + // yes, we can reclaim it, keep the abandoned map entry clear + *keep_abandoned = false; + return true; + } +} + + +typedef struct mi_arena_abandoned_visit_info_s { + mi_theap_t* theap; + size_t slice_count; + size_t block_size; + size_t bin; +} mi_arena_abandoned_visit_info_t; + +static bool mi_arena_try_find_abandoned_visit(mi_arena_t* arena, const void* arg, void** result) { + const mi_arena_abandoned_visit_info_t* info = (const mi_arena_abandoned_visit_info_t*)arg; + mi_assert_internal(result != NULL); + mi_theap_t* const theap = info->theap; + mi_heap_t* const heap = mi_theap_heap(theap); + const size_t tseq = theap->tld->thread_seq; + mi_arena_pages_t* const arena_pages = mi_heap_arena_pages(heap, arena); + if (arena_pages != NULL) { + size_t slice_index; + mi_bitmap_t* const bitmap = arena_pages->pages_abandoned[info->bin]; + + if (mi_bitmap_try_find_and_claim(bitmap, tseq, &slice_index, &mi_arena_try_claim_abandoned, arena)) { + // found an abandoned page of the right size + // and claimed ownership. + mi_page_t* page = mi_arena_page_at_slice(arena, slice_index); + mi_assert_internal(mi_page_is_owned(page)); + mi_assert_internal(mi_page_is_abandoned(page)); + mi_assert_internal(mi_heap_has_page(heap, arena, page)); + mi_atomic_decrement_relaxed(&heap->abandoned_count[info->bin]); + mi_theap_stat_decrease(theap, pages_abandoned, 1); + mi_theap_stat_counter_increase(theap, pages_reclaim_on_alloc, 1); + + _mi_page_free_collect(page, false); // update `used` count + mi_assert_internal(mi_bbitmap_is_clearN(arena->slices_free, slice_index, info->slice_count)); + mi_assert_internal(mi_page_slice_committed(page) > 0 || mi_bitmap_is_setN(arena->slices_committed, slice_index, info->slice_count)); + mi_assert_internal(mi_bitmap_is_setN(arena->slices_dirty, slice_index, info->slice_count)); + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_assert_internal(mi_ptr_page(mi_page_start(page))==page); + mi_assert_internal(mi_page_block_size(page) == info->block_size); + mi_assert_internal(!mi_page_is_full(page)); + *result = page; + return false; // break + } + } + return true; // continue +} + +static mi_page_t* mi_arenas_page_try_find_abandoned(mi_theap_t* theap, size_t slice_count, size_t block_size) +{ + mi_heap_t* const heap = mi_theap_heap(theap); + const size_t tseq = theap->tld->thread_seq; + mi_arena_t* const req_arena = heap->exclusive_arena; + + const size_t bin = _mi_bin(block_size); + if (bin >= MI_ARENA_BIN_COUNT) { + return NULL; // singleton page size + } + + // any abandoned in our size class? + mi_assert_internal(heap != NULL); + if (mi_atomic_load_relaxed(&heap->abandoned_count[bin]) == 0) { + return NULL; + } + + // search arena's + const bool allow_large = true; + const int any_numa = -1; + const bool match_numa = true; + const mi_arena_abandoned_visit_info_t info = { theap, slice_count, block_size, bin }; + void* page = NULL; + mi_forall_suitable_arenas(heap, req_arena, tseq, match_numa, any_numa, allow_large, &mi_arena_try_find_abandoned_visit, &info, &page); + return (mi_page_t*)page; +} + + +/* ----------------------------------------------------------- + Allocate the memory area and page meta data for a fresh page + (`mi_arenas_page_alloc_fresh_area`) +----------------------------------------------------------- */ + +// Offset of the first block when the page meta data is in front of the slices. +static size_t mi_page_meta_in_front_block_start(size_t block_size, bool os_align) { + size_t offset; + #if MI_GUARDED + // in a guarded build, we align pages with blocks a multiple of an OS page size, to the OS page size + // this ensures that all blocks in such pages are OS page size aligned (which is needed for the guard pages) + const size_t os_page_size = _mi_os_page_size(); + mi_assert_internal(MI_PAGE_ALIGN >= os_page_size); + if (!os_align && block_size % os_page_size == 0 && block_size > os_page_size /* at least 2 or more */ ) { + offset = mi_align_up(mi_page_info_size(), os_page_size); + } + else + #endif + if (os_align) { + offset = MI_PAGE_ALIGN; + } + else if (block_size != 0 && mi_is_power_of_two(block_size) && block_size <= MI_PAGE_MAX_START_BLOCK_ALIGN2) { + // naturally align power-of-2 blocks up to MI_PAGE_MAX_START_BLOCK_ALIGN2 size (4KiB) + offset = mi_align_up(mi_page_info_size(), block_size); + } + else if (block_size != 0 && block_size <= MI_SMALL_SIZE_MAX) { + // align small blocks to their size + offset = mi_align_up(mi_page_info_size(), block_size); + } + else if (block_size != 0 && (block_size % MI_PAGE_OSPAGE_BLOCK_ALIGN2) == 0) { + // also align large pages that are a multiple of MI_PAGE_OSPAGE_BLOCK_ALIGN2 (4KiB) + offset = mi_align_up(mi_page_info_size(), MI_PAGE_OSPAGE_BLOCK_ALIGN2); + } + else { + // otherwise start after the info + offset = mi_page_info_size(); + } + return mi_align_up(offset,MI_MAX_ALIGN_SIZE); +} + +// The usable size of a page area (excluding the guard page at the end in secure mode). +static size_t mi_arenas_page_noguard_size(size_t alloc_size) { + #if MI_SECURE>=5 + mi_assert(alloc_size > _mi_os_secure_guard_page_size()); + return alloc_size - _mi_os_secure_guard_page_size(); + #else + return alloc_size; + #endif +} + +// Can we offset the first block to spread cache sets? (see `MI_PAGE_CACHE_OFFSET`) +static bool mi_page_block_can_cache_offset(size_t block_size, bool os_align) { + if (os_align || !mi_block_size_has_cache_offset(block_size)) return false; + #if MI_GUARDED + if (block_size % _mi_os_page_size() == 0) return false; // guarded blocks must stay OS page aligned + #endif + return true; +} + +static size_t mi_page_cache_offset_index(uint8_t* slice_start, size_t slice_count, size_t noffsets) { + // Fibonacci hash of the page index: the top bits spread consecutive pages evenly over the offsets + const size_t page_index = (size_t)((uintptr_t)slice_start / MI_ARENA_SLICE_SIZE) / slice_count; + #if MI_SIZE_BITS >= 64 + const size_t hash = page_index * MI_ZU(0x9E3779B97F4A7C15); + #else + const size_t hash = page_index * MI_ZU(0x9E3779B9); + #endif + return (((hash >> (MI_SIZE_BITS - 16)) * noffsets) >> 16); +} + +static size_t mi_page_small_block_cache_offset(uint8_t* slice_start, size_t slice_count, size_t block_size) { + const size_t unit = mi_max(MI_PAGE_SMALL_CACHE_OFFSET_UNIT, mi_natural_alignment_of_size(block_size)); + if (unit > MI_PAGE_SMALL_CACHE_OFFSET_MAX) return 0; + return mi_page_cache_offset_index(slice_start, slice_count, 1 + MI_PAGE_SMALL_CACHE_OFFSET_MAX/unit) * unit; +} + +// Cache offset for the first block to spread the blocks of different pages over the cache sets (issue #1121). +// The offset fits in the unused space at the end of the page; if there is none, we skip the last block +// if that wastes less than 12.5% of the page (e.g. 64 KiB blocks in a 4 MiB page, but not 512 KiB blocks). +static size_t mi_page_block_cache_offset(uint8_t* slice_start, size_t slice_count, size_t block_size, size_t block_start, bool os_align) { + if (MI_PAGE_CACHE_OFFSET == 0 || os_align) return 0; + if (!mi_block_size_has_cache_offset(block_size)) return mi_page_small_block_cache_offset(slice_start, slice_count, block_size); + if (!mi_page_block_can_cache_offset(block_size, os_align)) return 0; + const size_t page_noguard_size = mi_arenas_page_noguard_size(_mi_size_of_slices(slice_count)); + mi_assert_internal(block_start + block_size <= page_noguard_size); + size_t slack = (page_noguard_size - block_start) % block_size; + if (slack < MI_PAGE_CACHE_OFFSET_UNIT && block_size < page_noguard_size/8) { slack += block_size; } + const size_t noffsets = 1 + (slack < MI_PAGE_CACHE_OFFSET_MAX ? slack : MI_PAGE_CACHE_OFFSET_MAX) / MI_PAGE_CACHE_OFFSET_UNIT; + return mi_page_cache_offset_index(slice_start, slice_count, noffsets) * MI_PAGE_CACHE_OFFSET_UNIT; +} + +// Use the separate (zero initialized) `page_meta` if available and not preferring in front of the slices, +// and otherwise put the page meta data in front of the slices. Also determines the offset of the first block. +static mi_page_t* mi_page_meta_select(uint8_t* slice_start, size_t slice_count, mi_page_t* page_meta, size_t block_size, bool os_align, size_t* block_start) { + mi_assert_internal(page_meta == NULL || page_meta->block_size == 0); + mi_page_t* page; + size_t offset; + if (page_meta == NULL + #if MI_PAGE_META_SMALL_IS_ALIGNED + // small pages keep the page meta data in front of the slices for a fast `mi_free_small` + // (note: it is important that the separate `page_meta->block_size == 0` for `mi_arena_page_at_slice`) + || (!os_align && block_size <= MI_SMALL_MAX_OBJ_SIZE) + #endif + ) { + // put the actual page meta info in front of the slices + // (note: it is not initialized yet since the slices may not be committed yet) + offset = mi_page_meta_in_front_block_start(block_size, os_align); + page = (mi_page_t*)slice_start; + } + else { + // use the separate page meta data + mi_memzero_aligned(page_meta, sizeof(*page_meta)); + offset = 0; + page = page_meta; + } + offset += mi_page_block_cache_offset(slice_start, slice_count, block_size, offset, os_align); + mi_assert_internal(os_align || offset < MI_ARENA_SLICE_SIZE); // so `mi_page_slice_start` can align down the page start + *block_start = offset; + return page; +} + +#if MI_PAGE_META_IS_ALIGNED +// Ensure the aligned page meta data for the slices at `slice_start` is committed. +static bool mi_arena_page_meta_ensure_committed(mi_arena_t* arena, uint8_t* slice_start) { + uint8_t* const meta_slices = (uint8_t*)mi_align_down_ptr(slice_start,MI_PAGE_META_ALIGNMENT); + mi_assert_internal(meta_slices >= mi_arena_start(arena)); + const size_t meta_slice_index = (meta_slices - mi_arena_start(arena)) / MI_ARENA_SLICE_SIZE; + if mi_unlikely(mi_bitmap_is_clear(arena->slices_committed, meta_slice_index)) { + // try to commit all page meta slices now + const size_t meta_slice_count = mi_arena_page_meta_aligned_slice_count(); + // the following assertion does not hold in a concurrent setting.. + // mi_assert_internal(mi_bitmap_is_clearN(arena->slices_committed, meta_slice_index, meta_slice_count)); + const size_t commit_size = meta_slice_count * MI_ARENA_SLICE_SIZE; + if (!_mi_arena_commit(arena->subproc, arena, meta_slices, commit_size, NULL, 0)) { + return false; + } + mi_bitmap_setN(arena->slices_committed, meta_slice_index, meta_slice_count, NULL); + } + return true; +} +#endif + +// Try to allocate slices from free space in the arena's, and determine the page meta data (`*ppage`) and the offset of the first block (`*pblock_start`). +static uint8_t* mi_arenas_page_alloc_from_slices(mi_theap_t* theap, size_t slice_count, size_t block_size, bool commit, bool allow_large, + mi_memid_t* memid, mi_arena_pages_t** parena_pages, mi_page_t** ppage, size_t* pblock_start) { + mi_heap_t* const heap = mi_theap_heap(theap); + mi_tld_t* const tld = theap->tld; + const int numa_node = (heap->numa_node >= 0 ? heap->numa_node : tld->numa_node); + + uint8_t* const start = (uint8_t*)_mi_arenas_try_alloc(heap, slice_count, MI_ARENA_SLICE_ALIGN, commit, allow_large, heap->exclusive_arena, tld->thread_seq, numa_node, memid); + if (start == NULL) return NULL; + mi_arena_t* const arena = memid->mem.arena.arena; + + // ensure the arena pages (and the aligned page meta data) + mi_arena_pages_t* const arena_pages = mi_heap_ensure_arena_pages(heap, arena); + if (arena_pages==NULL + #if MI_PAGE_META_IS_ALIGNED + || !mi_arena_page_meta_ensure_committed(arena, start) + #endif + ) { + _mi_arenas_free(heap->subproc, start, _mi_size_of_slices(slice_count), *memid); // roll back + return NULL; + } + // note: the following assert should hold if we could check it atomically, but in a concurrent setting we may already allocate in slice_count + // mi_assert_internal(mi_bitmap_is_clearN(arena_pages->pages, memid->mem.arena.slice_index, memid->mem.arena.slice_count)); + mi_assert_internal(mi_bitmap_is_clear(arena_pages->pages, memid->mem.arena.slice_index)); + // don't set yet: mi_bitmap_set(arena_pages->pages, memid->mem.arena.slice_index); + *parena_pages = arena_pages; + + // the separate page meta data (if available) + #if MI_PAGE_META_IS_ALIGNED + mi_page_t* const page_meta = mi_aligned_ptr_page0(start); + #else + mi_assert_internal(MI_PAGE_META_IS_SEPARATED!=0 || arena->pages_meta == NULL); + mi_page_t* const page_meta = (arena->pages_meta == NULL ? NULL : &arena->pages_meta[memid->mem.arena.slice_index]); + #endif + *ppage = mi_page_meta_select(start, slice_count, page_meta, block_size, false, pblock_start); + return start; +} + +#if MI_PAGE_META_IS_ALIGNED +// Allocate slices from the OS with the (committed and zero'd) page meta data at the start of the `MI_PAGE_META_ALIGNMENT` aligned area, +// and determine the page meta data (`*ppage`) and the offset of the first block (`*pblock_start`). +static uint8_t* mi_arenas_page_alloc_from_os(mi_heap_t* heap, size_t slice_count, size_t max_page_meta_count, size_t block_size, size_t block_alignment, bool os_align, bool commit, bool allow_large, + mi_memid_t* memid, mi_page_t** ppage, size_t* pblock_start) { + MI_UNUSED(allow_large); + mi_arena_t* const req_arena = heap->exclusive_arena; + const size_t alloc_size = _mi_size_of_slices(slice_count); + if (block_alignment >= MI_PAGE_META_ALIGNMENT) { + // if we allow alignment >= MI_PAGE_META_ALIGNMENT we need to substract 1 from a pointer + // in mi_aligned_ptr_page (and test for (intptr_t)p < 0 instead of NULL). We avoid this by limiting the max alignment. + _mi_warning_message("requested alignment is too large (%zu KiB)\n", block_alignment / MI_KiB); + errno = EINVAL; + return NULL; + // page_offset = MI_PAGE_META_ALIGNMENT; + // os_start = (uint8_t*)_mi_arena_os_alloc_aligned(heap->subproc, alloc_size + page_offset, block_alignment, MI_PAGE_META_ALIGNMENT /* align offset */, false /* commit */, false /* allow large */, req_arena, memid); + } + + // offset in the block for the page area + const size_t page_offset = (block_alignment < MI_PAGE_ALIGN ? MI_PAGE_ALIGN : block_alignment); + uint8_t* const os_start = (uint8_t*)_mi_arena_os_alloc_aligned(heap->subproc, alloc_size + page_offset, MI_PAGE_META_ALIGNMENT, 0 /* align offset */, false /* commit */, false /* allow large */, req_arena, memid); + if (os_start==NULL) return NULL; + + // commit page info and page + const size_t min_page_count = mi_divide_up(page_offset + MI_ARENA_SLICE_SIZE,MI_ARENA_SLICE_SIZE) + max_page_meta_count; + const size_t min_page_meta = min_page_count * sizeof(mi_page_t); + mi_assert_internal(min_page_meta < page_offset); + bool is_zero; + bool ok = _mi_arena_commit(heap->subproc,req_arena,os_start,min_page_meta,&is_zero,min_page_meta /* don't count in stats? */); + if (ok && commit) { + ok = _mi_arena_commit(heap->subproc,req_arena,os_start + page_offset,alloc_size,NULL,0); + } + if (!ok) { _mi_os_free(heap->subproc,os_start,alloc_size+page_offset,*memid); return NULL; } + if (!is_zero && !memid->initially_zero) { + mi_memzero_aligned(os_start,min_page_meta); + } + uint8_t* const start = os_start + page_offset; + mi_assert_internal(mi_is_aligned(start,block_alignment)); + mi_assert_internal(mi_is_aligned(os_start,MI_PAGE_META_ALIGNMENT)); + mi_assert_internal(mi_align_down_ptr(start-1,MI_PAGE_META_ALIGNMENT) == os_start); + mi_assert_internal((uint8_t*)mi_aligned_ptr_page0(start) < os_start + min_page_meta); + memid->initially_committed = true; // so we don't commit again + *ppage = mi_page_meta_select(start, slice_count, mi_aligned_ptr_page0(start), block_size, os_align, pblock_start); + return start; +} +#else +// Allocate slices from the OS with the page meta data in front of the slices, +// and determine the page meta data (`*ppage`) and the offset of the first block (`*pblock_start`). +static uint8_t* mi_arenas_page_alloc_from_os(mi_heap_t* heap, size_t slice_count, size_t max_page_meta_count, size_t block_size, size_t block_alignment, bool os_align, bool commit, bool allow_large, + mi_memid_t* memid, mi_page_t** ppage, size_t* pblock_start) { + MI_UNUSED(max_page_meta_count); + const size_t page_alignment = MI_ARENA_SLICE_ALIGN; + const size_t alloc_size = _mi_size_of_slices(slice_count); + uint8_t* start; + if (os_align) { + // note: slice_count already includes the page meta data (of MI_PAGE_ALIGN size) in front of the aligned block + start = (uint8_t*)_mi_arena_os_alloc_aligned(heap->subproc, alloc_size, block_alignment, page_alignment /* align offset */, commit, allow_large, heap->exclusive_arena, memid); + mi_assert_internal(start==NULL || mi_is_aligned(start + page_alignment, block_alignment)); + } + else { + start = (uint8_t*)_mi_arena_os_alloc_aligned(heap->subproc, alloc_size, page_alignment, 0 /* align offset */, commit, allow_large, heap->exclusive_arena, memid); + } + if (start == NULL) return NULL; + // page meta data is always placed in front of the slices + *ppage = mi_page_meta_select(start, slice_count, NULL, block_size, os_align, pblock_start); + return start; +} +#endif + +// Allocate the slices for a fresh page and determine its page meta data (`*ppage`) and the offset of the first block (`*pblock_start`). +static uint8_t* mi_arenas_page_alloc_fresh_area(mi_theap_t* theap, size_t slice_count, size_t max_page_meta_count, size_t block_size, size_t block_alignment, bool os_align, bool commit, + mi_memid_t* memid, mi_arena_pages_t** parena_pages, mi_page_t** ppage, size_t* pblock_start ) { + mi_assert_internal(parena_pages!=NULL && ppage!=NULL && pblock_start!=NULL); + *parena_pages = NULL; + *memid = _mi_memid_none(); + const bool allow_large = (MI_SECURE < 5); // 5 = guard page at end of each arena page + mi_heap_t* const heap = mi_theap_heap(theap); + + // try to allocate from free space in arena's + uint8_t* slice_start = NULL; + if (!mi_option_is_enabled(mi_option_disallow_arena_alloc) && // allowed to allocate from arena's? + !os_align && // not large alignment + slice_count <= mi_arena_max_object_size()/MI_ARENA_SLICE_SIZE) // and not too large + { + slice_start = mi_arenas_page_alloc_from_slices(theap, slice_count, block_size, commit, allow_large, memid, parena_pages, ppage, pblock_start); + } + + // otherwise fall back to the OS + if (slice_start == NULL) { + slice_start = mi_arenas_page_alloc_from_os(heap, slice_count, max_page_meta_count, block_size, block_alignment, os_align, commit, allow_large, memid, ppage, pblock_start); + if (slice_start == NULL) return NULL; + mi_heap_stat_increase(heap,pages_os_allocated,1); + } + mi_assert_internal(mi_is_aligned(slice_start, MI_PAGE_ALIGN)); + return slice_start; +} + + +/* ----------------------------------------------------------- + Allocate and initialize a fresh page (`mi_arenas_page_alloc_fresh`) +----------------------------------------------------------- */ + +// Free a page without modifying page_bin stats +static void mi_arenas_page_free_prim(mi_page_t* page); + +// Commit the start of the page up to and including the first block if the slices are not yet committed. +// Sets `*commit_size` to the committed size (or 0 if the slices were already committed). +static bool mi_arenas_page_commit_first_block(mi_theap_t* theap, mi_memid_t memid, uint8_t* slice_start, size_t block_start, size_t block_size, size_t page_noguard_size, size_t* commit_size) { + *commit_size = 0; + if (memid.initially_committed) return true; + size_t csize = mi_align_up(block_start + block_size, mi_page_min_commit_size()); + if (csize > page_noguard_size) { csize = page_noguard_size; } + bool is_zero = false; + if mi_unlikely(!_mi_arena_commit(mi_theap_subproc(theap), mi_memid_arena(memid), slice_start, csize, &is_zero, 0)) { + return false; + } + *commit_size = csize; + return true; +} + +// Prepare the page memory: zero the page meta data if it is in front of the slices, set the guard page, etc. +static void mi_arenas_page_init_memory(mi_theap_t* theap, mi_page_t* page, uint8_t* slice_start, size_t slice_count, size_t block_start, size_t page_noguard_size, mi_memid_t* memid) +{ + MI_UNUSED(theap); MI_UNUSED(slice_count); MI_UNUSED(block_start); MI_UNUSED(page_noguard_size); + + // zero initialize the page meta data + if (!memid->initially_zero && (uint8_t*)page == slice_start) { + mi_memzero_aligned(page, sizeof(*page)); + } + + // set the guard page + #if MI_SECURE>=5 + if (memid->initially_committed) { + _mi_os_secure_guard_page_set_at(mi_theap_subproc(theap), slice_start + page_noguard_size, *memid); + } + #endif + + // claimed free slices: initialize the page partly + if (!memid->initially_zero && memid->initially_committed) { + mi_track_mem_undefined(slice_start, slice_count * MI_ARENA_SLICE_SIZE); + } + else if (memid->initially_committed) { + mi_track_mem_defined(slice_start, slice_count * MI_ARENA_SLICE_SIZE); + } + #if MI_DEBUG > 1 + if (memid->initially_zero && memid->initially_committed) { + if (!mi_mem_is_zero(slice_start, page_noguard_size)) { + _mi_error_message(EFAULT, "internal error: page memory was not zero initialized.\n"); + memid->initially_zero = false; + if (block_start > 0) { mi_memzero_aligned(page, sizeof(*page)); } + } + } + #endif +} + +// Initialize the fields of the page meta data. +static void mi_arenas_page_init_meta(mi_theap_t* theap, mi_page_t* page, uint8_t* slice_start, size_t block_start, size_t block_size, bool os_align, + size_t page_noguard_size, size_t commit_size, mi_memid_t memid) +{ + const size_t reserved = (os_align ? 1 : (page_noguard_size - block_start) / block_size); + mi_assert_internal(reserved > 0 && reserved <= UINT16_MAX); + + // initialize the page start + uint8_t* const start = slice_start + block_start; + mi_assert_internal(start > (uint8_t*)page); + page->page_offset = start - (uint8_t*)page; + + // initialize page meta-data + page->reserved = (uint16_t)reserved; + page->block_size = block_size; + page->memid = memid; + page->free_is_zero = memid.initially_zero; + page->slice_pcommitted = (uint16_t)(commit_size / _mi_os_page_size()); + + page->heap = mi_theap_heap(theap); + mi_page_set_theap(page,theap); + // mi_assert_internal(mi_page_theap(page) == _mi_heap_theap_peek(page->heap)) +} + +#if MI_PAGE_META_IS_ALIGNED +// Point the aligned page meta entries of the slices to the actual page meta data. +static void mi_arenas_page_link_aligned_meta(mi_page_t* page, uint8_t* slice_start, size_t slice_count, size_t max_page_meta_count) +{ + mi_page_t* const page_meta = mi_aligned_ptr_page0(slice_start); + mi_atomic_store_ptr_release(mi_page_t,&page_meta->self,page); + if (slice_count > 1) { + // at least two for large singleton blocks as guard pages can have a large offset beyond a single slice + for(size_t i = 1; i < max_page_meta_count; i++) { + mi_assert_internal(page_meta[i].block_size == 0); + mi_atomic_store_ptr_release(mi_page_t,&page_meta[i].self,page); + } + } + #if MI_DEBUG>1 + mi_page_t* pstart = mi_aligned_ptr_page0(slice_start); + mi_assert_internal(mi_atomic_load_ptr_acquire(mi_page_t,&pstart->self)==page); + if (page->reserved>1) { + mi_page_t* pend = mi_aligned_ptr_page0(slice_start + (slice_count*MI_ARENA_SLICE_SIZE) - 1); + mi_assert_internal(mi_atomic_load_ptr_acquire(mi_page_t,&pend->self)==page); + } + #endif +} +#endif + +// Register a fresh page in its arena pages and the page map, and claim ownership. +// On failure the page is freed. +static bool mi_arenas_page_register(mi_theap_t* theap, mi_page_t* page, mi_arena_pages_t* arena_pages) +{ + // register in the arena_pages + if (arena_pages!=NULL) { + mi_assert_internal(page->memid.memkind == MI_MEM_ARENA); + mi_bitmap_set(arena_pages->pages, page->memid.mem.arena.slice_index); + } + + // and own it + mi_page_claim_ownership(page); + + // register in the page map + if mi_unlikely(!_mi_page_map_register(page)) { + mi_arenas_page_free_prim(page); + return false; + } + + // stats + mi_theap_stat_increase(theap, pages, 1); + mi_theap_stat_increase(theap, page_bins[_mi_page_stats_bin(page)], 1); + return true; +} + +// Allocate a fresh page +static mi_page_t* mi_arenas_page_alloc_fresh(mi_theap_t* theap, size_t slice_count, size_t block_size, size_t block_alignment, bool commit) +{ + const bool os_align = (block_alignment > MI_PAGE_MAX_OVERALLOC_ALIGN); + const bool singleton = (os_align || block_size > MI_LARGE_MAX_OBJ_SIZE); + const size_t max_page_meta_count = (singleton && slice_count > 2 ? 2 : slice_count); + + // allocate the slices and determine the page meta data + mi_memid_t memid = _mi_memid_none(); + mi_arena_pages_t* arena_pages = NULL; + mi_page_t* page = NULL; + size_t block_start = 0; + uint8_t* const slice_start = mi_arenas_page_alloc_fresh_area(theap,slice_count,max_page_meta_count,block_size,block_alignment,os_align,commit,&memid,&arena_pages,&page,&block_start); + if (!slice_start) return NULL; + mi_assert_internal(block_size < MI_MAX_ALIGN_SIZE || block_start % MI_MAX_ALIGN_SIZE == 0); + // all blocks must be naturally aligned (as relied upon by `mi_malloc_aligned`) + mi_assert_internal(block_size % mi_natural_alignment_of_size(block_size) == 0); + mi_assert_internal(mi_is_aligned(slice_start + block_start, mi_natural_alignment_of_size(block_size))); + + // commit the first block (if needed) + const size_t alloc_size = _mi_size_of_slices(slice_count); + const size_t page_noguard_size = mi_arenas_page_noguard_size(alloc_size); + size_t commit_size = 0; + if (!mi_arenas_page_commit_first_block(theap, memid, slice_start, block_start, block_size, page_noguard_size, &commit_size)) { + _mi_arenas_free(mi_theap_subproc(theap), slice_start, alloc_size, memid); + return NULL; + } + mi_assert_internal((commit && commit_size==0) || (!commit && (commit_size <= UINT16_MAX * _mi_os_page_size()))); + + // initialize the page memory and meta data + mi_arenas_page_init_memory(theap, page, slice_start, slice_count, block_start, page_noguard_size, &memid); + mi_arenas_page_init_meta(theap, page, slice_start, block_start, block_size, os_align, page_noguard_size, commit_size, memid); + #if MI_PAGE_META_IS_ALIGNED + mi_arenas_page_link_aligned_meta(page, slice_start, slice_count, max_page_meta_count); + #endif + mi_assert_internal(page->free==NULL); + mi_assert_internal(((uint8_t*)page != slice_start) == mi_page_meta_is_separated(page)); + mi_assert_internal(mi_page_slice_start(page) == slice_start); + mi_assert_internal(mi_page_size(page) <= page_noguard_size); + + // now we can register the page (and use `mi_arenas_page_free_prim` on error) + if (!mi_arenas_page_register(theap, page, arena_pages)) return NULL; + + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page),MI_PAGE_ALIGN)); + mi_assert_internal(mi_ptr_page(mi_page_start(page))==page); + mi_assert_internal(mi_page_block_size(page) == block_size); + #if MI_PAGE_META_SMALL_IS_ALIGNED + // `mi_free_small` finds the page meta data by aligning a block pointer down to `MI_SMALL_PAGE_SIZE` + if (mi_page_is_small(page)) { + mi_assert_internal((uint8_t*)page == slice_start && mi_is_aligned(page, MI_SMALL_PAGE_SIZE)); + mi_assert_internal(mi_page_start(page) + mi_page_size(page) <= (uint8_t*)page + MI_SMALL_PAGE_SIZE); + } + #endif + // mi_assert_internal(mi_page_is_abandoned(page)); + mi_assert_internal(mi_page_is_owned(page)); + return page; +} + + + +/* ----------------------------------------------------------- + Allocate page (`_mi_arenas_page_alloc`) +----------------------------------------------------------- */ + +// Allocate a regular small/medium/large page. +static mi_page_t* mi_arenas_page_regular_alloc(mi_theap_t* theap, size_t slice_count, size_t block_size) +{ + // 1. look for an abandoned page + mi_page_t* page = mi_arenas_page_try_find_abandoned(theap, slice_count, block_size); + if (page != NULL) { + return page; // return as abandoned + } + + // 2. find a free block, potentially allocating a new arena + const long commit_on_demand = mi_option_get(mi_option_page_commit_on_demand); + const bool commit = (mi_page_min_commit_size() >= slice_count * MI_ARENA_SLICE_SIZE || // always commit small pages + (slice_count >= _mi_slice_count_of_size(UINT16_MAX * _mi_os_page_size())) || // always commit pages too large to hold a 32-bit slice_committed + (commit_on_demand == 2 && _mi_os_has_overcommit()) || (commit_on_demand == 0)); + page = mi_arenas_page_alloc_fresh(theap, slice_count, block_size, 1, commit); + if (page == NULL) return NULL; + + mi_assert_internal(page->memid.memkind != MI_MEM_ARENA || page->memid.mem.arena.slice_count == slice_count); + if (!_mi_page_init(theap, page)) { + _mi_arenas_page_free(page,theap); + return NULL; + } + + return page; +} + +// Allocate a page containing one block (very large, or with large alignment) +static mi_page_t* mi_arenas_page_singleton_alloc(mi_theap_t* theap, size_t block_size, size_t block_alignment) +{ + const bool os_align = (block_alignment > MI_PAGE_MAX_OVERALLOC_ALIGN); + #if MI_PAGE_META_IS_ALIGNED + const size_t info_size = 0; + #else + const size_t info_size = (os_align ? MI_PAGE_ALIGN : mi_page_info_size()); + #endif + // room for the cache offset of the block (committed but not touched) + const size_t cache_offset_size = (mi_page_block_can_cache_offset(block_size, os_align) ? MI_PAGE_CACHE_OFFSET_MAX : 0); + #if MI_SECURE < 2 + const size_t slice_count = _mi_slice_count_of_size(info_size + cache_offset_size + block_size); + #else + const size_t slice_count = _mi_slice_count_of_size(mi_align_up(info_size + cache_offset_size + block_size, _mi_os_secure_guard_page_size()) + _mi_os_secure_guard_page_size()); + #endif + + mi_page_t* page = mi_arenas_page_alloc_fresh(theap, slice_count, block_size, block_alignment, true /* commit singletons always */); + if (page == NULL) return NULL; + + mi_assert(page->reserved == 1); + if (!_mi_page_init(theap, page)) { + _mi_arenas_page_free(page,theap); + return NULL; + } + + return page; +} + + +mi_page_t* _mi_arenas_page_alloc(mi_theap_t* theap, size_t block_size, size_t block_alignment) { + mi_page_t* page; + // semi static assert: ensure that all non-singleton block size bins are covered. + mi_assert(_mi_bin(MI_LARGE_MAX_OBJ_SIZE) < MI_ARENA_BIN_COUNT); + if mi_unlikely(block_alignment > MI_PAGE_MAX_OVERALLOC_ALIGN) { + mi_assert_internal(mi_is_power_of_two(block_alignment)); + page = mi_arenas_page_singleton_alloc(theap, block_size, block_alignment); + } + else if (block_size <= MI_SMALL_MAX_OBJ_SIZE) { + page = mi_arenas_page_regular_alloc(theap, _mi_slice_count_of_size(MI_SMALL_PAGE_SIZE), block_size); + } + else if (mi_block_size_is_medium(block_size)) { + page = mi_arenas_page_regular_alloc(theap, _mi_slice_count_of_size(MI_MEDIUM_PAGE_SIZE), block_size); + } + #if MI_ENABLE_LARGE_PAGES + else if (block_size <= MI_LARGE_MAX_OBJ_SIZE) { + page = mi_arenas_page_regular_alloc(theap, _mi_slice_count_of_size(MI_LARGE_PAGE_SIZE), block_size); + } + #endif + else { + page = mi_arenas_page_singleton_alloc(theap, block_size, block_alignment); + } + if mi_unlikely(page == NULL) { + return NULL; + } + // mi_assert_internal(page == NULL || _mi_page_segment(page)->subproc == tld->subproc); + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_assert_internal(mi_ptr_page(mi_page_start(page))==page); + mi_assert_internal(block_alignment <= MI_PAGE_MAX_OVERALLOC_ALIGN || mi_is_aligned(mi_page_start(page), block_alignment)); + + return page; +} + + +/* ----------------------------------------------------------- + Free a page +----------------------------------------------------------- */ + +static void mi_arenas_page_free_prim(mi_page_t* page) { + mi_assert_internal(_mi_page_qsbr_is_clear(page)); + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_assert_internal(mi_ptr_page(mi_page_start(page))==page); + mi_assert_internal(mi_page_is_owned(page)); + mi_assert_internal(mi_page_all_free(page)); + mi_assert_internal(page->next==NULL && page->prev==NULL); + + #if MI_DEBUG>1 + if (page->memid.memkind==MI_MEM_ARENA && !mi_page_is_full(page)) { + size_t bin = _mi_bin(mi_page_block_size(page)); + size_t slice_index; + size_t slice_count; + mi_arena_pages_t* arena_pages = NULL; + mi_arena_t* const arena = mi_page_arena_pages(page, &slice_index, &slice_count, &arena_pages); + mi_assert_internal(mi_bbitmap_is_clearN(arena->slices_free, slice_index, slice_count)); + mi_assert_internal(mi_page_slice_committed(page) > 0 || mi_bitmap_is_setN(arena->slices_committed, slice_index, slice_count)); + mi_assert_internal(bin >= MI_ARENA_BIN_COUNT || mi_bitmap_is_clearN(arena_pages->pages_abandoned[bin], slice_index, 1)); + mi_assert_internal(mi_bitmap_is_setN(arena_pages->pages, slice_index, 1)); + // note: we cannot check for `!mi_page_is_abandoned_and_mapped` since that may + // be (temporarily) not true if the free happens while trying to reclaim + // see `mi_arena_try_claim_abandoned` + } + #endif + + // unregister page + _mi_page_map_unregister(page); + + // recommit guard page at the end? + // we must do this since we may later allocate large spans over this page and cannot have a guard page in between + #if MI_SECURE >= 5 + if (!page->memid.is_pinned) { + _mi_os_secure_guard_page_reset_before(mi_page_subproc(page), mi_page_slice_start(page) + mi_page_full_size(page), page->memid); + } + #endif + + // and free + if (page->memid.memkind == MI_MEM_ARENA) { + mi_arena_pages_t* arena_pages; + size_t slice_index; + size_t slice_count; MI_UNUSED(slice_count); + mi_arena_t* const arena = mi_page_arena_pages(page, &slice_index, &slice_count, &arena_pages); + mi_assert_internal(arena_pages!=NULL); + mi_assert_internal(arena->subproc == mi_page_subproc(page)); + mi_bitmap_clear(arena_pages->pages, slice_index); + const size_t slice_committed = mi_page_slice_committed(page); + if (slice_committed > 0) { + // if committed on-demand, set the commit bits to account commit properly + mi_assert_internal(mi_page_full_size(page) >= slice_committed); + const size_t total_slices = slice_committed / MI_ARENA_SLICE_SIZE; // conservative + //mi_assert_internal(mi_bitmap_is_clearN(arena->slices_committed, slice_index, total_slices)); + mi_assert_internal(slice_count >= total_slices); + if (total_slices > 0) { + mi_bitmap_setN(arena->slices_committed, slice_index, total_slices, NULL); + } + // any left over? + const size_t extra = slice_committed % MI_ARENA_SLICE_SIZE; + if (extra > 0) { + // pretend it was decommitted already + mi_subproc_stat_decrease(arena->subproc, committed, extra); + } + } + else { + mi_assert_internal(mi_bitmap_is_setN(arena->slices_committed, slice_index, slice_count)); + } + } + else { + mi_heap_stat_decrease(page->heap, pages_os_allocated, 1); + } + if (mi_page_meta_is_separated(page)) { page->block_size = 0; } // for assertion checking + _mi_arenas_free( mi_page_subproc(page), mi_page_slice_start(page), mi_page_full_size(page), page->memid); +} + +void _mi_arenas_page_free(mi_page_t* page, mi_theap_t* current_theapx) { + mi_assert_internal(_mi_page_qsbr_is_clear(page)); + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_assert_internal(mi_ptr_page(mi_page_start(page))==page); + mi_assert_internal(mi_page_is_owned(page)); + mi_assert_internal(mi_page_all_free(page)); + mi_assert_internal(mi_page_is_abandoned(page)); + mi_assert_internal(page->next==NULL && page->prev==NULL); + mi_assert_internal(mi_theap_matches_thread(current_theapx)); + + mi_heap_t* const heap = mi_page_heap(page); + mi_theapx_stat_decrease(heap, current_theapx, page_bins[_mi_page_stats_bin(page)], 1); + mi_theapx_stat_decrease(heap, current_theapx, pages, 1); + _mi_page_free_collect(page,false); // update used count for cross-thread free's + _mi_page_update_stats(page); // and update the stats + mi_arenas_page_free_prim(page); +} + + +/* ----------------------------------------------------------- + Abandon a page +----------------------------------------------------------- */ + + +// release ownership of a page. This may free the page if all blocks were concurrently +// freed in the meantime. Returns true if the page was freed. +static bool mi_abandoned_page_unown(mi_page_t* page, mi_theap_t* current_theapx) { + mi_assert_internal(mi_page_is_owned(page)); + mi_assert_internal(mi_page_is_abandoned(page)); + mi_assert_internal(mi_theap_matches_thread(current_theapx)); + mi_thread_free_t tf_new; + mi_thread_free_t tf_old = mi_atomic_load_relaxed(&page->xthread_free); + do { + mi_assert_internal(mi_tf_is_owned(tf_old)); + while mi_unlikely(mi_tf_block(tf_old) != NULL) { + _mi_page_free_collect(page, false); // update used + if (mi_page_all_free(page) && !_mi_page_qsbr_should_delay(page)) { + _mi_page_qsbr_clear(page); + _mi_arenas_page_unabandon(page, current_theapx); + _mi_arenas_page_free(page, current_theapx); + return true; + } + tf_old = mi_atomic_load_relaxed(&page->xthread_free); + } + mi_assert_internal(mi_tf_block(tf_old)==NULL); + tf_new = mi_tf_create(NULL, false); + } while (!mi_atomic_cas_weak_acq_rel(&page->xthread_free, &tf_old, tf_new)); + return false; +} + + +void _mi_arenas_page_abandon(mi_page_t* page, mi_theap_t* current_theapx) { + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_assert_internal(mi_ptr_page(mi_page_start(page))==page); + mi_assert_internal(mi_page_is_owned(page)); + mi_assert_internal(mi_page_is_abandoned(page)); + mi_assert_internal(MI_QSBR || !mi_page_all_free(page)); + mi_assert_internal(page->next==NULL && page->prev == NULL); + mi_assert_internal(mi_theap_matches_thread(current_theapx)); + // mi_assert_internal(current_theap == _mi_page_associated_theap(page)); + #if MI_QSBR + mi_assert_internal(!page->qsbr_in_queue && page->qsbr_next == NULL && page->qsbr_prev == NULL); + #endif + + // note: somewhat expensive to update here, but might be good as then we attribute + // the current allocations/frees to the current thread/theap. Otherwise it might be + // reclaimed later in another thread/theap and those allocations/frees get attributed there... + _mi_page_update_stats(page); + + // add to abandoned? + mi_heap_t* heap = mi_page_heap(page); + if (page->memid.memkind==MI_MEM_ARENA && !mi_page_is_full(page) + #if MI_QSBR + && !mi_page_is_singleton(page) + #endif + ) { + // make available for allocations + size_t bin = _mi_bin(mi_page_block_size(page)); + mi_assert_internal(bin < MI_ARENA_BIN_COUNT); + if (bin < MI_ARENA_BIN_COUNT) { // paranoia + size_t slice_index; + size_t slice_count; + mi_arena_pages_t* arena_pages = NULL; + mi_arena_t* const arena = mi_page_arena_pages(page, &slice_index, &slice_count, &arena_pages); MI_UNUSED(arena); + + mi_assert_internal(!mi_page_is_singleton(page)); + mi_assert_internal(mi_bbitmap_is_clearN(arena->slices_free, slice_index, slice_count)); + mi_assert_internal(mi_page_slice_committed(page) > 0 || mi_bitmap_is_setN(arena->slices_committed, slice_index, slice_count)); + mi_assert_internal(mi_bitmap_is_setN(arena->slices_dirty, slice_index, slice_count)); + + mi_page_set_abandoned_mapped(page); + const bool was_clear = mi_bitmap_set(arena_pages->pages_abandoned[bin], slice_index); + MI_UNUSED(was_clear); mi_assert_internal(was_clear); + mi_atomic_increment_relaxed(&heap->abandoned_count[bin]); + mi_theapx_stat_increase(heap, current_theapx, pages_abandoned, 1); + mi_abandoned_page_unown(page, current_theapx); + return; + } + } + // otherwise, + // page is full (or a singleton), or the page is OS/externally allocated + // leave as is; it will be reclaimed when an object is free'd in the page + // but for non-arena pages, add to the subproc list so these can be visited + if (page->memid.memkind != MI_MEM_ARENA) { + mi_lock(&heap->os_abandoned_pages_lock) { + // push in front + page->prev = NULL; + page->next = heap->os_abandoned_pages; + if (page->next != NULL) { page->next->prev = page; } + heap->os_abandoned_pages = page; + } + mi_theapx_stat_increase(heap, current_theapx, pages_os_abandoned, 1); + } + mi_theapx_stat_increase(heap, current_theapx, pages_abandoned, 1); + mi_abandoned_page_unown(page, current_theapx); +} + + +// this is called from `alloc/free.c:mi_free_try_collect_mt` only. +bool _mi_arenas_page_try_reabandon_to_mapped(mi_page_t* page) { + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_assert_internal(mi_ptr_page(mi_page_start(page))==page); + mi_assert_internal(mi_page_is_owned(page)); + mi_assert_internal(mi_page_is_abandoned(page)); + mi_assert_internal(!mi_page_is_abandoned_mapped(page)); + mi_assert_internal(!mi_page_is_full(page)); + mi_assert_internal(!mi_page_all_free(page)); + mi_assert_internal(!mi_page_is_singleton(page)); + if (mi_page_is_full(page) || mi_page_is_abandoned_mapped(page) || page->memid.memkind != MI_MEM_ARENA) { + return false; + } + else { + // do not use _mi_heap_theap as we may call this during shutdown of threads and don't want to reinitialize the theap + mi_theap_t* const theapx = _mi_page_associated_theap_peek(page); // can be NULL + mi_heap_t* const heap = mi_page_heap(page); + // if (theapx==NULL) return false; + mi_theapx_stat_counter_increase(heap, theapx, pages_reabandon_full, 1); + mi_theapx_stat_adjust_decrease(heap, theapx, pages_abandoned, 1); // adjust as we are not abandoning fresh + _mi_arenas_page_abandon(page, theapx); + return true; + } +} + +// called from `mi_free` if trying to unabandon an abandoned page +void _mi_arenas_page_unabandon(mi_page_t* page, mi_theap_t* current_theapx) { + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_assert_internal(mi_ptr_page(mi_page_start(page))==page); + mi_assert_internal(mi_page_is_owned(page)); + mi_assert_internal(mi_page_is_abandoned(page)); + mi_assert_internal(mi_theap_matches_thread(current_theapx)); + + mi_heap_t* const heap = mi_page_heap(page); + if (mi_page_is_abandoned_mapped(page)) { + mi_assert_internal(page->memid.memkind==MI_MEM_ARENA); + // remove from the abandoned map + const size_t bin = _mi_bin(mi_page_block_size(page)); + mi_assert_internal(bin < MI_ARENA_BIN_COUNT); + size_t slice_index; + size_t slice_count; + mi_arena_pages_t* arena_pages; + mi_arena_t* arena = mi_page_arena_pages(page, &slice_index, &slice_count, &arena_pages); MI_UNUSED(arena); + + mi_assert_internal(mi_bbitmap_is_clearN(arena->slices_free, slice_index, slice_count)); + mi_assert_internal(mi_page_slice_committed(page) > 0 || mi_bitmap_is_setN(arena->slices_committed, slice_index, slice_count)); + + // this busy waits until a concurrent reader (from alloc_abandoned) is done + mi_bitmap_clear_once_set(arena->subproc, arena_pages->pages_abandoned[bin], slice_index); + mi_page_clear_abandoned_mapped(page); + mi_atomic_decrement_relaxed(&heap->abandoned_count[bin]); + } + else { + // page is full (or a singleton), page is OS allocated + // if not an arena page, remove from the subproc os pages list + if (page->memid.memkind != MI_MEM_ARENA) { + mi_lock(&heap->os_abandoned_pages_lock) { + if (page->prev != NULL) { page->prev->next = page->next; } + if (page->next != NULL) { page->next->prev = page->prev; } + if (heap->os_abandoned_pages == page) { heap->os_abandoned_pages = page->next; } + page->next = NULL; + page->prev = NULL; + } + mi_theapx_stat_decrease(heap, current_theapx, pages_os_abandoned, 1); + } + } + mi_theapx_stat_decrease(heap, current_theapx, pages_abandoned, 1); +} + + +/* ----------------------------------------------------------- + Visit all pages and blocks in a heap +----------------------------------------------------------- */ + +typedef struct mi_heap_visit_info_s { + mi_heap_t* heap; + mi_block_visit_fun* visitor; + void* arg; + bool visit_blocks; + bool abandoned_only; +} mi_heap_visit_info_t; + +static bool mi_heap_visit_page(mi_page_t* page, const mi_heap_visit_info_t* vinfo) { + mi_heap_area_t area; + #if MI_QSBR + const bool claimed = (mi_page_is_abandoned(page) && mi_page_claim_ownership(page)); + #endif + _mi_page_free_collect(page, true); // collect cross-thread free'd blocks so `area.used` is precise. + #if MI_QSBR + if (claimed && mi_abandoned_page_unown(page, NULL)) return true; + #endif + _mi_heap_area_init(&area, page); + mi_assert_internal(vinfo->heap == mi_page_heap(page)); + if (!vinfo->visitor(vinfo->heap, &area, NULL, area.block_size, vinfo->arg)) { + return false; // break + } + if (vinfo->visit_blocks) { + return _mi_theap_area_visit_blocks(&area, page, vinfo->visitor, vinfo->arg); // continue or break + } + else { + return true; // continue + } +} + +static bool mi_heap_visit_page_at(size_t slice_index, size_t slice_count, mi_arena_t* arena, void* arg) { + MI_UNUSED(slice_count); + const mi_heap_visit_info_t* vinfo = (const mi_heap_visit_info_t*)arg; + mi_page_t* page = mi_arena_page_at_slice(arena, slice_index); + return mi_heap_visit_page(page, vinfo); // continue or break +} + +static bool mi_heap_visit_arena(mi_arena_t* arena, const void* arg, void** result) { + MI_UNUSED(result); + const mi_heap_visit_info_t* vinfo = (const mi_heap_visit_info_t*)arg; + mi_heap_visit_info_t visit_info = *vinfo; + mi_heap_t* const heap = vinfo->heap; + mi_arena_pages_t* arena_pages = mi_heap_arena_pages(heap, arena); + if (arena_pages == NULL) return true; // continue + if (vinfo->abandoned_only) { + for (size_t bin = 0; bin < MI_ARENA_BIN_COUNT; bin++) { + // TODO: if we had a single abandoned page map as well, this can be faster. Maybe for CPython? + if (mi_atomic_load_relaxed(&heap->abandoned_count[bin]) > 0) { + if (!_mi_bitmap_forall_set(arena_pages->pages_abandoned[bin], &mi_heap_visit_page_at, arena, &visit_info)) return false; // break + } + } + return true; // continue + } + else { + return _mi_bitmap_forall_set(arena_pages->pages, &mi_heap_visit_page_at, arena, &visit_info); // continue or break + } +} + +// The caller holds heap->theaps_lock to prevent thread-exit abandonment and theap removal. +static bool mi_heap_visit_owned_os_blocks(mi_heap_t* heap, bool visit_blocks, mi_block_visit_fun* visitor, void* arg) { + const mi_heap_visit_info_t visit_info = { heap, visitor, arg, visit_blocks, false }; + for (mi_theap_t* theap = heap->theaps; theap != NULL; theap = theap->hnext) { + if (theap->page_count_os == 0) continue; + for (size_t bin = 0; bin <= MI_BIN_FULL; bin++) { + mi_page_t* page = theap->pages[bin].first; + while (page != NULL) { + mi_page_t* const next = page->next; + if (page->memid.memkind != MI_MEM_ARENA && !mi_heap_visit_page(page, &visit_info)) return false; + page = next; + } + } + } + return true; +} + +static bool mi_heap_visit_blocks_locked(mi_heap_t* heap, bool abandoned_only, bool visit_blocks, mi_block_visit_fun* visitor, void* arg) { + // visit all pages in a heap + // Other threads must not access this heap; thread-exit abandonment is blocked by the heap lock. + const mi_heap_visit_info_t visit_info = { heap, visitor, arg, visit_blocks, abandoned_only }; + bool ok = mi_forall_arenas(heap, NULL, 0, &mi_heap_visit_arena, &visit_info, NULL); + if (!ok) return false; + + // visit abandoned pages in OS allocated memory + mi_page_t* page = NULL; + mi_lock(&heap->os_abandoned_pages_lock) { + page = heap->os_abandoned_pages; + } + while (ok && page != NULL) { + mi_page_t* next = page->next; // read upfront in case the visitor frees the page + ok = mi_heap_visit_page(page, &visit_info); + page = next; + } + + // visit owned OS allocated blocks in the theaps + if (ok && !abandoned_only) { + ok = mi_heap_visit_owned_os_blocks(heap, visit_blocks, visitor, arg); + } + return ok; +} + +bool _mi_heap_visit_blocks(mi_heap_t* heap, bool abandoned_only, bool visit_blocks, mi_block_visit_fun* visitor, void* arg) { + mi_assert(visitor!=NULL); + if (visitor==NULL) return false; + if (heap==NULL) { heap = mi_heap_main(); } + bool ok; + mi_lock(&heap->theaps_lock) { + ok = mi_heap_visit_blocks_locked(heap, abandoned_only, visit_blocks, visitor, arg); + } + return ok; +} + +bool mi_heap_visit_blocks(mi_heap_t* heap, bool visit_blocks, mi_block_visit_fun* visitor, void* arg) { + return _mi_heap_visit_blocks(heap, false, visit_blocks, visitor, arg); +} + +bool mi_heap_visit_abandoned_blocks(mi_heap_t* heap, bool visit_blocks, mi_block_visit_fun* visitor, void* arg) { + return _mi_heap_visit_blocks(heap, true, visit_blocks, visitor, arg); +} + +#if MI_QSBR +static bool mi_heap_collect_abandoned_page(mi_page_t* page) { + if (!mi_page_is_abandoned(page) || !mi_page_claim_ownership(page)) return true; + _mi_page_free_collect(page, true); + if (mi_page_all_free(page) && !_mi_page_qsbr_should_delay(page)) { + _mi_page_qsbr_clear(page); + _mi_arenas_page_unabandon(page, NULL); + _mi_arenas_page_free(page, NULL); + } + else { + mi_abandoned_page_unown(page, NULL); + } + return true; +} + +static bool mi_heap_collect_abandoned_page_at(size_t slice_index, size_t slice_count, mi_arena_t* arena, void* arg) { + MI_UNUSED(slice_count); MI_UNUSED(arg); + return mi_heap_collect_abandoned_page(mi_arena_page_at_slice(arena, slice_index)); +} + +static bool mi_heap_collect_abandoned_arena(mi_arena_t* arena, const void* arg, void** result) { + MI_UNUSED(result); + mi_heap_t* heap = (mi_heap_t*)arg; + mi_arena_pages_t* arena_pages = mi_heap_arena_pages(heap, arena); + if (arena_pages == NULL) return true; + return _mi_bitmap_forall_set(arena_pages->pages, &mi_heap_collect_abandoned_page_at, arena, NULL); +} + +void _mi_heap_collect_abandoned(mi_heap_t* heap) { + mi_assert_internal(heap != NULL); + mi_lock(&heap->theaps_lock) { + // Use the all-pages map: abandoned size-class maps exclude singletons and full pages. + mi_forall_arenas(heap, NULL, 0, &mi_heap_collect_abandoned_arena, heap, NULL); + mi_page_t* page = NULL; + mi_lock(&heap->os_abandoned_pages_lock) { page = heap->os_abandoned_pages; } + while (page != NULL) { + mi_page_t* next = page->next; + mi_heap_collect_abandoned_page(page); + page = next; + } + } +} +#endif + + +typedef struct mi_heap_delete_visit_info_s { + mi_heap_t* heap_target; + mi_theap_t* theap_target; + mi_theap_t* theap; +} mi_heap_delete_visit_info_t; + +static bool mi_heap_delete_page(const mi_heap_t* heap, const mi_heap_area_t* area, void* block, size_t block_size, void* arg) { + MI_UNUSED(block); MI_UNUSED(block_size); MI_UNUSED(heap); + const mi_heap_delete_visit_info_t* info = (const mi_heap_delete_visit_info_t*)arg; + mi_heap_t* heap_target = info->heap_target; + mi_theap_t* const theap = NULL; // info->theap; mi_assert_internal(mi_theap_heap(theap) == heap); + mi_page_t* const page = (mi_page_t*)area->reserved1; + + mi_page_claim_ownership(page); // claim ownership + _mi_page_qsbr_clear(page); // deletion requires an unprotected heap; destruction requires no readers + if (mi_page_is_abandoned(page)) { + _mi_arenas_page_unabandon(page,theap); + } + else { + page->next = page->prev = NULL; // yikes.. better not to try to access this from a thread later on.. + mi_page_set_theap(page,NULL); // set threadid to abandoned + } + mi_assert_internal(mi_page_is_abandoned(page)); + mi_assert_internal(mi_page_is_owned(page)); + + if (mi_page_used(page)==0) { + // free the page + _mi_arenas_page_free(page, theap); + } + else if (heap_target==NULL) { + #if MI_GUARDED + _mi_page_unguard_all(page); // remove potential interior guard pages + #endif + // destroy the page + _mi_page_free_collect(page, false); // collect the thread-free list first while `used` still counts those blocks (as `_mi_arenas_page_free` collects it again) (see #1420) + mi_page_used_reset(page); // note: invariant `|local_free| + |free| == reserved - used` does not hold in this case + _mi_arenas_page_free(page, theap); + } + else { + // move the page to `heap_target` as an abandoned page + // first remove it from the current heap + const size_t sbin = _mi_page_stats_bin(page); + mi_arena_t* arena = NULL; + size_t slice_index = 0; + if (page->memid.memkind == MI_MEM_ARENA) { + size_t slice_count; + mi_arena_pages_t* arena_pages = NULL; + arena = mi_page_arena_pages(page, &slice_index, &slice_count, &arena_pages); + mi_assert_internal(mi_bitmap_is_set(arena_pages->pages, slice_index)); + mi_bitmap_clear(arena_pages->pages, slice_index); + } + else { + // os allocated + mi_assert_internal(mi_memid_is_os(page->memid) && page->next == NULL); + } + if (theap != NULL) { + mi_theap_stat_decrease(theap, page_bins[sbin], 1); + mi_theap_stat_decrease(theap, pages, 1); + } + else { + mi_heap_stat_decrease((mi_heap_t*)heap, page_bins[_mi_page_stats_bin(page)], 1); + mi_heap_stat_decrease((mi_heap_t*)heap, pages, 1); + } + mi_theap_t* theap_target = info->theap_target; + + // and then add it to the new target heap + if (arena != NULL) { + mi_arena_pages_t* arena_pages_target = mi_heap_ensure_arena_pages(heap_target, arena); + if mi_unlikely(arena_pages_target==NULL) { + // if we cannot allocate this, we move it to the main heap instead (which does not require allocation) + heap_target = _mi_arena_heap_main(arena); + theap_target = mi_heap_theap(heap_target); // todo: find through theap_target tld? + arena_pages_target = mi_heap_ensure_arena_pages(heap_target, arena); + mi_assert_internal(arena_pages_target!=NULL); + } + mi_assert_internal(mi_bitmap_is_clear(arena_pages_target->pages, slice_index)); + mi_bitmap_set(arena_pages_target->pages, slice_index); + } + page->heap = heap_target; + mi_theap_stat_increase(theap_target, page_bins[sbin], 1); + mi_theap_stat_increase(theap_target, pages, 1); + + // and abandon in the new heap + _mi_arenas_page_abandon(page,theap_target); + } + return true; // continue +} + +static void mi_heap_delete_pages(mi_heap_t* heap, mi_heap_t* heap_target) { + mi_theap_t* const theap_target = (heap_target != NULL ? _mi_heap_theap(heap_target) : NULL); + // mi_theap_t* const theap = _mi_heap_theap(heap); + mi_heap_delete_visit_info_t info = { heap_target, theap_target, NULL }; + _mi_heap_visit_blocks(heap, false, false, &mi_heap_delete_page, &info); + #if MI_DEBUG>1 + // no more arena pages? + for (size_t i = 0; i < MI_MAX_ARENAS; i++) { + mi_arena_pages_t* const arena_pages = mi_atomic_load_ptr_relaxed(mi_arena_pages_t, &heap->arena_pages[i]); + if (arena_pages!=NULL) { + mi_assert_internal(mi_bitmap_is_all_clear(arena_pages->pages)); + } + } + // nor os abandoned pages? + mi_lock(&heap->os_abandoned_pages_lock) { + + mi_assert_internal(heap->os_abandoned_pages == NULL); + } + // nor arena abandoned pages? + for (size_t i = 0; i < MI_ARENA_BIN_COUNT; i++) { + mi_assert_internal(mi_atomic_load_relaxed(&heap->abandoned_count[i])==0); + } + #endif +} + +void _mi_heap_move_pages(mi_heap_t* heap_from, mi_heap_t* heap_to) { + if (mi_is_heap_main(heap_from)) return; + if (heap_to==NULL) { heap_to = heap_from->subproc->heap_main; } + mi_heap_delete_pages(heap_from, heap_to); +} + +void _mi_heap_destroy_pages(mi_heap_t* heap_from) { + if (mi_is_heap_main(heap_from)) return; + mi_heap_delete_pages(heap_from, NULL); +} diff --git a/Objects/mimalloc/arena/arena.c b/Objects/mimalloc/arena/arena.c new file mode 100644 index 000000000000000..ea7fb7d750eaca1 --- /dev/null +++ b/Objects/mimalloc/arena/arena.c @@ -0,0 +1,1188 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2019-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +/* ---------------------------------------------------------------------------- +"Arenas" are fixed area's of OS memory from which we can allocate +large blocks (>= MI_ARENA_MIN_BLOCK_SIZE, 64KiB). +In contrast to the rest of mimalloc, the arenas are shared between +threads and need to be accessed using atomic operations. + +Arenas are also used to for huge OS page (1GiB) reservations or for reserving +OS memory upfront which can be improve performance or is sometimes needed +on embedded devices. We can also employ this with WASI or `sbrk` systems +to reserve large arenas upfront and be able to reuse the memory more effectively. + +The arena allocation needs to be thread safe and we use an atomic bitmap to allocate. +-----------------------------------------------------------------------------*/ + +#include "mimalloc.h" +#include "mimalloc/util/stats.h" +#include "mimalloc/alloc/page.h" +#include "mimalloc/heap/subproc.h" +#include "mimalloc/arena/arena.h" +#include "arena.h" + +mi_heap_t* _mi_arena_heap_main(const mi_arena_t* arena) { + return _mi_subproc_heap_main(arena->subproc); +} + +/* ----------------------------------------------------------- + arena blocks +----------------------------------------------------------- */ + +// Blocks needed for a given byte size +size_t _mi_slice_count_of_size(size_t size) { + return mi_divide_up(size, MI_ARENA_SLICE_SIZE); +} + +// Byte size of a number of blocks +size_t _mi_size_of_slices(size_t bcount) { + return (bcount * MI_ARENA_SLICE_SIZE); +} + +mi_memid_t _mi_memid_create(mi_memkind_t memkind) { + mi_memid_t memid; + _mi_memzero_var(memid); + memid.memkind = memkind; + return memid; +} + +mi_memid_t _mi_memid_none(void) { + return _mi_memid_create(MI_MEM_NONE); +} + +mi_memid_t _mi_memid_create_os(void* base, size_t size, bool committed, bool is_zero, bool is_large) { + mi_memid_t memid = _mi_memid_create(MI_MEM_OS); + memid.mem.os.base = base; + memid.mem.os.size = size; + memid.initially_committed = committed; + memid.initially_zero = is_zero; + memid.is_pinned = is_large; + return memid; +} + +mi_memid_t _mi_memid_create_static(void* p, size_t size) { + mi_memid_t memid = _mi_memid_create(MI_MEM_STATIC); + memid.mem.malloc.base = p; + memid.mem.malloc.size = size; + memid.initially_committed = true; + memid.is_pinned = true; + return memid; +} + +mi_memid_t _mi_memid_create_malloc(void* p, size_t size, bool iszero) { + mi_memid_t memid = _mi_memid_create(MI_MEM_MALLOC); + memid.mem.malloc.base = p; + memid.mem.malloc.size = size; + memid.initially_committed = true; + memid.initially_zero = iszero; + memid.is_pinned = true; + return memid; +} + +size_t _mi_memid_size(mi_memid_t memid) { + if (mi_memid_is_os(memid)) { + return memid.mem.os.size; + } + else if (memid.memkind == MI_MEM_ARENA) { + return _mi_size_of_slices(memid.mem.arena.slice_count); + } + else if (memid.memkind == MI_MEM_MALLOC) { + return memid.mem.malloc.size; + } + else { + mi_assert_internal(mi_memid_needs_no_free(memid)); + return 0; + } +} + + +/* ----------------------------------------------------------- + Arena id's +----------------------------------------------------------- */ + +mi_arena_id_t _mi_arena_id_none(void) { + return NULL; +} + +mi_arena_t* _mi_arena_from_id(mi_arena_id_t id) { + mi_arena_t* const arena = (mi_arena_t*)id; + mi_assert_internal(arena==NULL || arena->parent==NULL); // id's should never point to sub-arena's + return arena; +} + +mi_arena_id_t mi_arena_id_from_arena(mi_arena_t* arena) { + mi_assert_internal(arena==NULL || arena->parent==NULL); + return (arena==NULL ? _mi_arena_id_none() : (mi_arena_id_t)arena); +} + + +bool _mi_arena_memid_is_suitable(mi_memid_t memid, mi_arena_t* request_arena) { + if (memid.memkind == MI_MEM_ARENA) { + return mi_arena_is_suitable(memid.mem.arena.arena, request_arena); + } + else { + return mi_arena_is_suitable(NULL, request_arena); + } +} + +size_t mi_arena_min_alignment(void) { + return MI_ARENA_ALIGNMENT; +} + +size_t mi_arena_min_size(void) { + return MI_ARENA_MIN_SIZE; +} + +// fixed limit for the maximum object size in an arena +static size_t mi_arena_max_fixed_object_size(void) { + #if MI_PAGE_META_IS_ALIGNED + return (MI_PAGE_META_ALIGNMENT - mi_align_up(MI_PAGE_META_ALIGNED_COUNT * sizeof(mi_page_t), MI_ARENA_SLICE_SIZE)); + #else + return (MI_ARENA_MAX_SIZE - MI_ARENA_CHUNK_SIZE); // minus an initial chunk to accommodate meta info + #endif +} + +// Maximum object size allowed to be allocated in an arena +size_t mi_arena_max_object_size(void) { + size_t max_size = mi_option_get_size(mi_option_arena_max_object_size); + max_size = mi_align_up(max_size, MI_ARENA_SLICE_SIZE); + if (max_size <= MI_ARENA_MIN_OBJ_SIZE) { + return MI_ARENA_MIN_OBJ_SIZE; + } + else if (max_size >= mi_arena_max_fixed_object_size()) { + return mi_arena_max_fixed_object_size(); + } + else { + return max_size; + } +} + +// Size of an arena +static size_t mi_arena_size(mi_arena_t* arena) { + return _mi_size_of_slices(arena->slice_count); +} + + +// Arena area +void* mi_arena_area(mi_arena_id_t arena_id, size_t* size) { + if (size != NULL) *size = 0; + mi_arena_t* arena = _mi_arena_from_id(arena_id); + if (arena == NULL) return NULL; + if (size != NULL) { + mi_assert_internal(_mi_size_of_slices(arena->slice_count) <= arena->total_size); + *size = arena->total_size; + } + return mi_arena_start(arena); +} + + + +/* ----------------------------------------------------------- + Arena iteration +----------------------------------------------------------- */ + +static bool mi_arena_is_suitable_ex(mi_arena_t* arena, mi_arena_t* req_arena, bool match_numa, int numa_node, bool allow_pinned) { + if (!allow_pinned && arena->memid.is_pinned) return false; + if (!mi_arena_is_suitable(arena, req_arena)) return false; + if (req_arena == NULL) { // if not specific, check numa affinity + const bool numa_suitable = (numa_node < 0 || arena->numa_node < 0 || arena->numa_node == numa_node); + if (match_numa) { if (!numa_suitable) return false; } + else { if (numa_suitable) return false; } + } + return true; +} + +// determine the start of search; important to keep heaps and threads +// into their own memory regions to reduce contention. +static inline size_t mi_arena_start_idx(mi_heap_t* heap, size_t tseq, size_t arena_cycle) +{ + const size_t hseq = heap->heap_seq; + const size_t hcount = mi_atomic_load_relaxed(&heap->subproc->heap_count); + if (arena_cycle <= 1) return 0; + if (hseq==0 || hcount<=1 || arena_cycle > 0x8FF) return (tseq % arena_cycle); // common for single heap programs + + // spread heaps evenly among arena's, and then evenly for threads in their fraction + size_t start; + mi_assert_internal(arena_cycle <= 0x8FF); // prevent overflow on 32-bit + const size_t frac = (arena_cycle * 256) / hcount; // fraction in the arena_cycle; at most: arena_cycle * 0x100 + if (frac==0) { + // many heaps (> 256 per arena) + start = (hseq % arena_cycle); + } + else { + const size_t hspot = (hseq % hcount); + start = (frac * hspot) / 256; // (arena_cycle * (hseq % hcount)) / hcount + if (frac >= 512) { // at least 2 arena's per heap? + start = start + (tseq % (frac/256)); + } + } + mi_assert_internal(start < arena_cycle); + return start; +} + +mi_decl_forceinline bool mi_forall_arenas(mi_heap_t* heap, mi_arena_t* req_arena, size_t tseq, mi_forall_arena_fun_t* visit, const void* arg, void** result) +{ + if (result != NULL) *result = NULL; + const size_t arena_count = mi_arenas_get_count(heap->subproc); + const size_t arena_cycle = (arena_count == 0 ? 0 : arena_count - 1); /* first search the arenas below the last one */ + /* always start searching in the arena's below the max */ + const size_t start = (req_arena==NULL ? mi_arena_start_idx(heap,tseq,arena_cycle) : req_arena->arena_idx); + mi_assert_internal(start <= arena_count); + for (size_t i = 0; i < arena_count; i++) { + size_t idx; + if (i < arena_cycle) { + idx = i + start; + if (idx >= arena_cycle) { idx -= arena_cycle; } /* adjust so we rotate through the cycle */ + } + else { + idx = i; /* remaining arena's after the cycle */ + } + mi_arena_t* arena = mi_arena_from_index(heap->subproc,idx); + if (req_arena != NULL && arena != req_arena && + (arena == NULL || arena->parent != req_arena)) continue; /* only the requested arena or its children */ + + // call the visitor + if (arena != NULL) { + if (!visit(arena, arg, result)) return false; + } + } + return true; +} + +typedef struct mi_arena_suitable_visit_info_s { + mi_arena_t* req_arena; + bool match_numa; + int numa_node; + bool allow_large; + mi_forall_arena_fun_t* visit; + const void* arg; +} mi_arena_suitable_visit_info_t; + +static inline bool mi_arena_visit_suitable(mi_arena_t* arena, const void* arg, void** result) +{ + const mi_arena_suitable_visit_info_t* info = (const mi_arena_suitable_visit_info_t*)arg; + if (!mi_arena_is_suitable_ex(arena, info->req_arena, info->match_numa, info->numa_node, info->allow_large)) { + return true; // skip and continue + } + else { + return info->visit(arena, info->arg, result); // visit + } +} + +mi_decl_forceinline bool mi_forall_suitable_arenas(mi_heap_t* heap, mi_arena_t* req_arena, size_t tseq, bool match_numa, int numa_node, + bool allow_large, mi_forall_arena_fun_t* visit, const void* arg, void** result) { + const mi_arena_suitable_visit_info_t info = { req_arena, match_numa, numa_node, allow_large, visit, arg }; + return mi_forall_arenas(heap, req_arena, tseq, &mi_arena_visit_suitable, &info, result); +} + +#if defined(__cplusplus) || defined(_MSC_VER) +// since these are mi_decl_forceinline, ensure they are linked in the final binary by adding a reference. +void* _mi_arena_externs[] = { + (void*)&mi_forall_arenas, + (void*)&mi_forall_suitable_arenas +}; +#endif + + +/* ----------------------------------------------------------- + Reserve a fresh arena +----------------------------------------------------------- */ + +static int mi_reserve_os_memory_ex2(mi_subproc_t* subproc, size_t size, bool commit, bool allow_large, bool exclusive, mi_arena_id_t* arena_id); + +// try to reserve a fresh arena space +bool _mi_arena_reserve(mi_subproc_t* subproc, size_t req_size, bool allow_large, mi_arena_id_t* arena_id) +{ + const size_t arena_count = mi_arenas_get_count(subproc); + if (arena_count > (MI_MAX_ARENAS - 4)) return false; + + // calc reserve + size_t arena_reserve = mi_option_get_size(mi_option_arena_reserve); + if (arena_reserve == 0) return false; + + if (!_mi_os_has_virtual_reserve()) { + arena_reserve = arena_reserve/4; // be conservative if virtual reserve is not supported (for WASM for example) + } + arena_reserve = mi_align_up(arena_reserve, MI_ARENA_SLICE_SIZE); + + if (arena_count >= 1 && arena_count <= 128) { + // scale up the arena sizes exponentially every 8 entries + const size_t multiplier = (size_t)1 << mi_clamp(arena_count/8, 0, 16); + size_t reserve = 0; + if (!mi_mul_overflow(multiplier, arena_reserve, &reserve)) { + arena_reserve = reserve; + } + } + + // try to accommodate the requested size for huge allocations + req_size = mi_align_up(req_size + MI_ARENA_MAX_CHUNK_OBJ_SIZE, MI_ARENA_MAX_CHUNK_OBJ_SIZE); // over-reserve for meta-info + if (arena_reserve < req_size) { + arena_reserve = req_size; + } + + // check arena bounds + const size_t min_reserve = MI_ARENA_MIN_SIZE; + const size_t max_reserve = MI_ARENA_MAX_SIZE; // 16 GiB + if (arena_reserve < min_reserve) { + arena_reserve = min_reserve; + } + else if (arena_reserve > max_reserve) { + arena_reserve = max_reserve; + } + + // should be able to at least handle the current allocation size + if (arena_reserve < req_size) return false; + + // commit eagerly? + bool arena_commit = false; + const bool overcommit = _mi_os_has_overcommit(); + if (mi_option_get(mi_option_arena_eager_commit) == 2) { arena_commit = overcommit || mi_option_is_enabled(mi_option_allow_large_os_pages); } + else if (mi_option_get(mi_option_arena_eager_commit) == 1) { arena_commit = true; } + + // on an OS with overcommit (Linux) we don't count the commit yet as it is on-demand. Once a slice + // is actually allocated for the first time it will be counted. + const bool adjust = (overcommit && arena_commit); + if (adjust) { mi_subproc_stat_adjust_decrease( subproc, committed, arena_reserve); } + // and try to reserve the arena + int err = mi_reserve_os_memory_ex2(subproc, arena_reserve, arena_commit, allow_large, false /* exclusive? */, arena_id); + if (err != 0) { + if (adjust) { mi_subproc_stat_adjust_increase( subproc, committed, arena_reserve); } // roll back + // failed to allocate: try a smaller size arena as fallback? + const size_t small_arena_reserve = 4 * MI_ARENA_MIN_SIZE; // 128 MiB (or 32 MiB on 32-bit) + if (arena_reserve > small_arena_reserve && small_arena_reserve > req_size) { + // try again + if (adjust) { mi_subproc_stat_adjust_decrease(subproc, committed, small_arena_reserve); } + err = mi_reserve_os_memory_ex2(subproc, small_arena_reserve, arena_commit, allow_large, false /* exclusive? */, arena_id); + if (err != 0 && adjust) { mi_subproc_stat_adjust_increase( subproc, committed, small_arena_reserve); } // roll back + } + } + return (err==0); +} + + +// Is a pointer contained in the given arena area? +static bool mi_arena_strictly_contains(mi_arena_t* arena, const void* p) { + return (arena != NULL && + mi_arena_start(arena) <= (const uint8_t*)p && + mi_arena_start(arena) + _mi_size_of_slices(arena->slice_count) >(const uint8_t*)p); +} + +// Is a pointer inside any of our arenas? +static bool mi_arenas_contain_ex(const void* p, mi_arena_t* parent) { + mi_subproc_t* subproc = _mi_subproc(); + const size_t max_arena = mi_arenas_get_count(subproc); + for (size_t i = 0; i < max_arena; i++) { + mi_arena_t* arena = mi_atomic_load_ptr_acquire(mi_arena_t, &subproc->arenas[i]); + if (arena != NULL) { + if (parent==NULL || arena==parent || arena->parent==parent) { + if (mi_arena_strictly_contains(arena, p)) { + return true; + } + } + } + } + return false; +} + +// // Is a pointer inside any of our arenas? +// bool _mi_arenas_contain(const void* p) { +// return mi_arenas_contain_ex(p, NULL); +// } + +// Is a pointer contained in the given arena area? +bool mi_arena_contains(mi_arena_id_t arena_id, const void* p) { + mi_arena_t* arena = _mi_arena_from_id(arena_id); + if (arena==NULL) return false; + else if (mi_arena_strictly_contains(arena, p)) return true; + else return mi_arenas_contain_ex(p, arena); // maybe a subarena? +} + + +/* ----------------------------------------------------------- + Remove an arena. +----------------------------------------------------------- */ + +// destroy owned arenas; this is unsafe and should only be done using `mi_option_destroy_on_exit` +// for dynamic libraries that are unloaded and need to release all their allocated memory. +static void mi_arenas_unsafe_destroy(mi_subproc_t* subproc) { + mi_assert_internal(subproc != NULL); + const size_t arena_count = mi_arenas_get_count(subproc); + for (size_t i = 0; i < arena_count; i++) { + mi_arena_t* arena = mi_atomic_load_ptr_acquire(mi_arena_t, &subproc->arenas[i]); + if (arena != NULL) { + // mi_lock_done(&arena->abandoned_visit_lock); + mi_atomic_store_ptr_release(mi_arena_t, &subproc->arenas[i], NULL); + if (mi_memkind_is_os(arena->memid.memkind)) { + _mi_os_free_ex(subproc, mi_arena_start(arena), mi_arena_size(arena), true, arena->memid); + } + } + } + // try to lower the max arena. + size_t expected = arena_count; + mi_atomic_cas_strong_acq_rel(&subproc->arena_count, &expected, (size_t)0); +} + + +// destroy owned arenas; this is unsafe and should only be done using `mi_option_destroy_on_exit` +// for dynamic libraries that are unloaded and need to release all their allocated memory. +void _mi_arenas_unsafe_destroy_all(mi_subproc_t* subproc) { + mi_arenas_unsafe_destroy(subproc); + // mi_arenas_try_purge(true /* force purge */, true /* visit all*/, subproc, 0 /* thread seq */); // purge non-owned arenas +} + + +/* ----------------------------------------------------------- + Add an arena. +----------------------------------------------------------- */ + +static bool mi_arenas_add(mi_subproc_t* subproc, mi_arena_t* arena, mi_arena_id_t* arena_id) +{ + mi_assert_internal(arena != NULL); + mi_assert_internal(arena->slice_count > 0); + if (arena_id != NULL) { *arena_id = _mi_arena_id_none(); } + + // try to find a NULL entry + mi_arena_t* expected; + size_t count = mi_arenas_get_count(subproc); + for( size_t i = 0; i < count; i++) { + if (mi_arena_from_index(subproc,i) == NULL) { + arena->arena_idx = i; + expected = NULL; + if (mi_atomic_cas_ptr_strong_release(mi_arena_t, &subproc->arenas[i], &expected, arena)) { + // success + if (arena_id != NULL) { *arena_id = mi_arena_id_from_arena(arena); } + return true; + } + } + } + + // otherwise, try to allocate a fresh slot + while(countarena_count, &count, count+1)) { + arena->arena_idx = count; + expected = NULL; + if (mi_atomic_cas_ptr_strong_release(mi_arena_t, &subproc->arenas[count], &expected, arena)) { + mi_subproc_stat_counter_increase(arena->subproc, arena_count, 1); + if (arena_id != NULL) { *arena_id = mi_arena_id_from_arena(arena); } + return true; + } + } + } + + // failed + arena->arena_idx = 0; + arena->subproc = NULL; + return false; +} + + +static mi_arena_t* mi_arena_info(void* area) { + return (mi_arena_t*)((uint8_t*)area + _mi_size_of_slices(mi_arena_page_meta_aligned_slice_count())); +} + +static size_t mi_arena_info_slices_needed(size_t slice_count, size_t* bitmap_base) { + if (slice_count == 0) slice_count = MI_BCHUNK_BITS; + mi_assert_internal((slice_count % MI_BCHUNK_BITS) == 0); + const size_t base_size = _mi_size_of_slices(mi_arena_page_meta_aligned_slice_count()) + mi_align_up(sizeof(mi_arena_t), MI_BCHUNK_SIZE); + const size_t bitmaps_count = 4 + MI_ARENA_BIN_COUNT; // commit, dirty, purge, pages, and abandoned + const size_t bitmaps_size = bitmaps_count * mi_bitmap_size(slice_count, NULL) + mi_bbitmap_size(slice_count, NULL); // + free + #if MI_PAGE_META_IS_SEPARATED && !MI_PAGE_META_IS_ALIGNED + const size_t pages_size = slice_count * sizeof(mi_page_t); + #else + const size_t pages_size = 0; + #endif + const size_t size = base_size + bitmaps_size + pages_size; + + const size_t os_page_size = _mi_os_page_size(); + const size_t info_size = mi_align_up(size, os_page_size) + _mi_os_secure_guard_page_size(); + const size_t info_slices = _mi_slice_count_of_size(info_size); + + if (bitmap_base != NULL) *bitmap_base = base_size; + return info_slices; +} + +static mi_bbitmap_t* mi_arena_bbitmap_init(mi_subproc_t* subproc, size_t slice_count, uint8_t** base) { + mi_bbitmap_t* bbitmap = (mi_bbitmap_t*)(*base); + *base = (*base) + mi_bbitmap_init(subproc, bbitmap, slice_count, true /* already zero */); + return bbitmap; +} + + +static mi_arena_t* mi_arena_initialize(mi_subproc_t* subproc, void* start, + size_t slice_count, mi_arena_t* parent, size_t total_size, + int numa_node, bool exclusive, + mi_memid_t memid, mi_commit_fun_t* commit_fun, void* commit_fun_arg, mi_arena_id_t* arena_id) +{ + mi_assert_internal(mi_is_aligned(start,MI_ARENA_ALIGNMENT)); + mi_assert_internal(_mi_size_of_slices(slice_count)>=MI_ARENA_MIN_SIZE); + + if (slice_count > MI_BITMAP_MAX_BIT_COUNT) { // 16 GiB for now + // note: this should never happen if called from `mi_manage_os_memory` (as that allocates sub-arenas when needed) + _mi_warning_message("cannot use OS memory since it is too large (size %zu MiB, maximum is %zu MiB)", _mi_size_of_slices(slice_count)/MI_MiB, _mi_size_of_slices(MI_BITMAP_MAX_BIT_COUNT)/MI_MiB); + return NULL; + } + + size_t bitmap_base; + const size_t info_slices = mi_arena_info_slices_needed(slice_count, &bitmap_base); + if (slice_count < info_slices+1) { + _mi_warning_message("cannot use OS memory since it is not large enough (size %zu KiB, minimum required is %zu KiB)", _mi_size_of_slices(slice_count)/MI_KiB, _mi_size_of_slices(info_slices+1)/MI_KiB); + return NULL; + } + // else if (info_slices >= MI_ARENA_MAX_CHUNK_OBJ_SLICES) { + // _mi_warning_message("cannot use OS memory since it is too large with respect to the maximum object size (size %zu MiB, meta-info slices %zu, maximum object slices are %zu)", _mi_size_of_slices(slice_count)/MI_MiB, info_slices, MI_ARENA_MAX_CHUNK_OBJ_SLICES); + // return NULL; + // } + + // commit & zero if needed + if (!memid.initially_committed) { + size_t commit_size = _mi_size_of_slices(info_slices); + // leave a guard OS page decommitted at the end? + if (!memid.is_pinned) { commit_size -= _mi_os_secure_guard_page_size(); } + bool ok = false; + if (commit_fun != NULL) { + ok = (*commit_fun)(true /* commit */, start, commit_size, NULL, commit_fun_arg); + } + else { + ok = _mi_os_commit(subproc, start, commit_size, NULL); + } + if (!ok) { + _mi_warning_message("unable to commit meta-data for OS memory"); + return NULL; + } + } + else if (!memid.is_pinned) { + // if MI_SECURE, set a guard page at the end of the arena info + // todo: this does not respect the commit_fun as the memid is of external memory + _mi_os_secure_guard_page_set_before(subproc, (uint8_t*)start + _mi_size_of_slices(info_slices), memid); + } + if (!memid.initially_zero) { + mi_memzero(start, _mi_size_of_slices(info_slices) - _mi_os_secure_guard_page_size()); + } + + // init + mi_arena_t* arena = mi_arena_info(start); + arena->start = start; + arena->subproc = subproc; + arena->memid = memid; + arena->is_exclusive = exclusive; + arena->slice_count = slice_count; + arena->info_slices = info_slices; + if (numa_node<0 && mi_option_is_enabled(mi_option_arena_is_numa_local)) { + arena->numa_node = _mi_os_numa_node(); + } + else { + arena->numa_node = numa_node; + } + arena->purge_expire = 0; + arena->commit_fun = commit_fun; + arena->commit_fun_arg = commit_fun_arg; + arena->parent = parent; + arena->total_size = total_size; + + // init bitmaps + uint8_t* base = mi_arena_start(arena) + bitmap_base; + arena->slices_free = mi_arena_bbitmap_init(subproc, slice_count, &base); + arena->slices_committed = mi_arena_bitmap_init(slice_count, &base); + arena->slices_dirty = mi_arena_bitmap_init(slice_count, &base); + arena->slices_purge = mi_arena_bitmap_init(slice_count, &base); + arena->pages_main.pages = mi_arena_bitmap_init(slice_count, &base); + for (size_t i = 0; i < MI_ARENA_BIN_COUNT; i++) { + arena->pages_main.pages_abandoned[i] = mi_arena_bitmap_init(slice_count, &base); + } + #if MI_PAGE_META_IS_SEPARATED && !MI_PAGE_META_IS_ALIGNED + arena->pages_meta = (mi_page_t*)base; + base += (slice_count * sizeof(mi_page_t)); + #else + arena->pages_meta = NULL; + #endif + mi_assert_internal(_mi_size_of_slices(info_slices) >= (size_t)(base - mi_arena_start(arena))); + + // reserve our meta info (and reserve slices outside the memory area) + #if MI_PAGE_META_IS_ALIGNED + for(size_t i = 0; i < arena->slice_count; i += MI_PAGE_META_ALIGNED_COUNT) { + // set all free slices (and skip the slices reserved for the page meta info) + const size_t meta_slices = (i==0 ? info_slices : mi_arena_page_meta_aligned_slice_count()); + const size_t start_idx = (i + meta_slices); + size_t count = MI_PAGE_META_ALIGNED_COUNT - meta_slices; + if (start_idx < arena->slice_count) { + if (count + start_idx > arena->slice_count) { + count = arena->slice_count - start_idx; + mi_assert_internal(count > 0); + } + mi_bbitmap_unsafe_setN(arena->slices_free, start_idx, count); + } + } + #else + mi_bbitmap_unsafe_setN(arena->slices_free, info_slices /* start */, arena->slice_count - info_slices); + #endif + if (memid.initially_committed) { + mi_bitmap_unsafe_setN(arena->slices_committed, 0, arena->slice_count); + } + if (!memid.initially_zero) { + mi_bitmap_unsafe_setN(arena->slices_dirty, 0, arena->slice_count); + } + + if (!mi_arenas_add(subproc, arena, arena_id)) { return NULL; } + return arena; +} + + +/* ----------------------------------------------------------- + Reserve and manage OS memory (as an arena) +----------------------------------------------------------- */ + +static bool mi_manage_os_memory_ex2(mi_subproc_t* subproc, void* start, size_t size, int numa_node, bool exclusive, + mi_memid_t memid, mi_commit_fun_t* commit_fun, void* commit_fun_arg, mi_arena_id_t* arena_id) mi_attr_noexcept +{ + // checks + mi_assert(start!=NULL); + if (arena_id != NULL) { *arena_id = _mi_arena_id_none(); } + if (start==NULL) return false; + if (!mi_is_aligned(start, MI_ARENA_ALIGNMENT)) { + // we can align the start since the memid tracks the real base of the memory. + void* const aligned_start = mi_align_up_ptr(start, MI_ARENA_ALIGNMENT); + const size_t diff = (uint8_t*)aligned_start - (uint8_t*)start; + if (diff >= size || (size - diff) < MI_ARENA_ALIGNMENT) { + _mi_warning_message("after alignment, the size of the arena becomes too small (memory at %p with size %zu)\n", start, size); + return false; + } + start = aligned_start; + size = size - diff; + } + + // allocate enough arena's to span the full memory area + // the first arena is the owner, the rest are "sub-arena" (with `parent` pointing to the first one) + size_t total_slice_count = mi_align_down(size / MI_ARENA_SLICE_SIZE, MI_BCHUNK_BITS); + size_t total_size = _mi_size_of_slices(total_slice_count); + if (total_size < MI_ARENA_MIN_SIZE) { + _mi_warning_message("cannot use OS memory since it is not large enough (size %zu KiB, minimum required is %zu KiB)", size/MI_KiB, MI_ARENA_MIN_SIZE/MI_KiB); + return false; + } + + mi_arena_t* parent = NULL; + do { + // counting down on the total_slice_count + size_t slice_count = total_slice_count; + if (slice_count > MI_BITMAP_MAX_BIT_COUNT) { // 16 GiB for now (with 64KiB slices) + slice_count = MI_BITMAP_MAX_BIT_COUNT; + } + + // initialize + mi_arena_t* arena = mi_arena_initialize( subproc, start, slice_count, parent, + (parent==NULL ? total_size : 0), numa_node, exclusive, + memid, commit_fun, commit_fun_arg, + (parent==NULL ? arena_id : NULL)); + if (arena==NULL) { + // failed to initialize due to failing commit or too many arena's + if (parent==NULL) { + return false; + } + else { + // partial success, but failed to use the full area.. + // todo: roll-back in this case? that requires a lock on the arena's array though + mi_assert(_mi_size_of_slices(total_slice_count) <= parent->total_size); + parent->total_size -= _mi_size_of_slices(total_slice_count); + return true; + } + } + + // success + if (parent==NULL) { + parent = arena; + memid.memkind = MI_MEM_NONE; + } + mi_assert(slice_count <= total_slice_count); + total_slice_count -= slice_count; + start = (uint8_t*)start + _mi_size_of_slices(slice_count); + } + while (total_slice_count > 0); + + return true; +} + +bool mi_manage_os_memory_ex(void* start, size_t size, bool is_committed, bool is_pinned, bool is_zero, int numa_node, bool exclusive, mi_arena_id_t* arena_id) mi_attr_noexcept { + mi_memid_t memid = _mi_memid_create(MI_MEM_EXTERNAL); + memid.mem.os.base = start; + memid.mem.os.size = size; + memid.initially_committed = is_committed; + memid.initially_zero = is_zero; + memid.is_pinned = is_pinned; + return mi_manage_os_memory_ex2(_mi_subproc(), start, size, numa_node, exclusive, memid, NULL, NULL, arena_id); +} + +bool mi_manage_memory(void* start, size_t size, bool is_committed, bool is_pinned, bool is_zero, int numa_node, bool exclusive, mi_commit_fun_t* commit_fun, void* commit_fun_arg, mi_arena_id_t* arena_id) mi_attr_noexcept +{ + mi_memid_t memid = _mi_memid_create(MI_MEM_EXTERNAL); + memid.mem.os.base = start; + memid.mem.os.size = size; + memid.initially_committed = is_committed; + memid.initially_zero = is_zero; + memid.is_pinned = is_pinned; + return mi_manage_os_memory_ex2(_mi_subproc(), start, size, numa_node, exclusive, memid, commit_fun, commit_fun_arg, arena_id); +} + + +// Reserve a range of regular OS memory +static int mi_reserve_os_memory_ex2(mi_subproc_t* subproc, size_t size, bool commit, bool allow_large, bool exclusive, mi_arena_id_t* arena_id) { + if (arena_id != NULL) *arena_id = _mi_arena_id_none(); + if (size <= MI_MAX_ALLOC_SIZE) { + size = mi_align_up(size, MI_ARENA_SLICE_SIZE); // at least one slice + } + if (size > MI_MAX_ALLOC_SIZE) { + _mi_error_message(EOVERFLOW, "memory reservation request is too large (size %zu)\n", size); + return ENOMEM; + } + mi_memid_t memid; + void* start = _mi_os_alloc_aligned(subproc, size, MI_ARENA_ALIGNMENT, commit, allow_large, &memid); + if (start == NULL) return ENOMEM; + if (!mi_manage_os_memory_ex2(subproc, start, size, -1 /* numa node */, exclusive, memid, NULL, NULL, arena_id)) { + _mi_os_free_ex(subproc, start, size, commit, memid); + _mi_verbose_message("failed to reserve %zu KiB memory\n", mi_divide_up(size, 1024)); + return ENOMEM; + } + _mi_verbose_message("reserved %zu KiB memory%s\n", mi_divide_up(size, 1024), memid.is_pinned ? " (in large os pages)" : ""); + // mi_debug_show_arenas(true, true, false); + + return 0; +} + +// Reserve a range of regular OS memory +int mi_reserve_os_memory_ex(size_t size, bool commit, bool allow_large, bool exclusive, mi_arena_id_t* arena_id) mi_attr_noexcept { + return mi_reserve_os_memory_ex2(_mi_subproc(), size, commit, allow_large, exclusive, arena_id); +} + +// Manage a range of regular OS memory +bool mi_manage_os_memory(void* start, size_t size, bool is_committed, bool is_large, bool is_zero, int numa_node) mi_attr_noexcept { + return mi_manage_os_memory_ex(start, size, is_committed, is_large, is_zero, numa_node, false /* exclusive? */, NULL); +} + +// Reserve a range of regular OS memory +int mi_reserve_os_memory(size_t size, bool commit, bool allow_large) mi_attr_noexcept { + return mi_reserve_os_memory_ex(size, commit, allow_large, false, NULL); +} + + +/* ----------------------------------------------------------- + Debugging +----------------------------------------------------------- */ + +// Return idx of the slice past the last used slice +static size_t mi_arena_used_slices(mi_arena_t* arena) { + size_t idx; + if (mi_bbitmap_bsr_inv(arena->slices_free, &idx)) { + return (idx + 1); + } + else { + return mi_arena_info_slices(arena); + } +} + +static size_t mi_debug_show_bfield(mi_bfield_t field, char* buf, size_t* k) { + size_t bit_set_count = 0; + for (int bit = 0; bit < MI_BFIELD_BITS; bit++) { + bool is_set = ((((mi_bfield_t)1 << bit) & field) != 0); + if (is_set) bit_set_count++; + buf[*k] = (is_set ? 'x' : '.'); + *k = *k + 1; + } + return bit_set_count; +} + +typedef enum mi_ansi_color_e { + MI_BLACK = 30, + MI_MAROON, + MI_DARKGREEN, + MI_ORANGE, + MI_NAVY, + MI_PURPLE, + MI_TEAL, + MI_GRAY, + MI_DARKGRAY = 90, + MI_RED, + MI_GREEN, + MI_YELLOW, + MI_BLUE, + MI_MAGENTA, + MI_CYAN, + MI_WHITE +} mi_ansi_color_t; + +static void mi_debug_color(char* buf, size_t* k, mi_ansi_color_t color) { + *k += _mi_snprintf(buf + *k, 32, "\x1B[%dm", (int)color); +} + +static int mi_page_commit_usage(mi_page_t* page) { + const size_t committed_size = mi_page_committed(page); + const size_t used_size = mi_page_used(page) * mi_page_block_size(page); + return (int)(used_size * 100 / committed_size); +} + +static size_t mi_debug_show_page_bfield(char* buf, size_t* k, mi_arena_t* arena, size_t slice_index, long* pbit_of_page, mi_ansi_color_t* pcolor_of_page ) { + size_t bit_set_count = 0; + long bit_of_page = *pbit_of_page; + mi_ansi_color_t color = *pcolor_of_page; + mi_ansi_color_t prev_color = MI_GRAY; + for (int bit = 0; bit < MI_BFIELD_BITS; bit++, bit_of_page--) { + // bool is_set = ((((mi_bfield_t)1 << bit) & field) != 0); + void* start = mi_arena_slice_start(arena, slice_index + bit); + mi_page_t* page = _mi_safe_ptr_page(start); + char c = ' '; + if (page!=NULL && start==mi_page_slice_start(page)) { + mi_assert_internal(bit_of_page <= 0); + bit_set_count++; + c = 'p'; + color = MI_GRAY; + if (_mi_meta_is_meta_page(arena->subproc,page)) { c = 'm'; } + else if (mi_page_is_singleton(page)) { c = 's'; } + else if (mi_page_is_full(page)) { c = 'f'; } + if (!mi_page_is_abandoned(page)) { c = _mi_toupper(c); } + int commit_usage = mi_page_commit_usage(page); + if (commit_usage < 25) { color = MI_MAROON; } + else if (commit_usage < 50) { color = MI_ORANGE; } + else if (commit_usage < 75) { color = MI_TEAL; } + else color = MI_DARKGREEN; + bit_of_page = (long)page->memid.mem.arena.slice_count; + } + else { + c = '?'; + if (bit_of_page > 0) { c = '-'; } + // else if (_mi_meta_is_meta_page(arena->subproc,start)) { c = 'm'; color = MI_GRAY; } + else if (slice_index + bit < arena->info_slices) { c = 'i'; color = MI_GRAY; } + #if MI_PAGE_META_IS_ALIGNED + else if ((slice_index % MI_PAGE_META_ALIGNED_COUNT) == 0 && (size_t)bit <= mi_arena_page_meta_aligned_slice_count()) { + { c = 'i'; color = MI_GRAY; } + } + #endif + // else if (mi_bitmap_is_setN(arena->pages_purge, slice_index + bit, NULL)) { c = '*'; } + else if (mi_bbitmap_is_setN(arena->slices_free, slice_index+bit,1)) { + if (mi_bitmap_is_set(arena->slices_purge, slice_index + bit)) { c = '~'; color = MI_ORANGE; } + else if (mi_bitmap_is_set(arena->slices_committed, slice_index + bit)) { c = '_'; color = MI_GRAY; } + else { c = '.'; color = MI_GRAY; } + } + if (bit==MI_BFIELD_BITS-1 && bit_of_page > 1) { c = '>'; } + } + if (color != prev_color) { + mi_debug_color(buf, k, color); + prev_color = color; + } + buf[*k] = c; *k += 1; + } + mi_debug_color(buf, k, MI_GRAY); + *pbit_of_page = bit_of_page; + *pcolor_of_page = color; + return bit_set_count; +} + +static size_t mi_debug_show_chunks(const char* header1, const char* header2, const char* header3, + size_t slice_count, size_t chunk_count, + mi_bchunk_t* chunks, mi_bchunkmap_t* chunk_bins, bool invert, mi_arena_t* arena, bool narrow) +{ + _mi_raw_message("\x1B[37m%s%s%s (use/commit: \x1B[31m0 - 25%%\x1B[33m - 50%%\x1B[36m - 75%%\x1B[32m - 100%%\x1B[0m)\n", header1, header2, header3); + const size_t fields_per_line = (narrow ? 2 : 4); + const size_t used_slice_count = mi_arena_used_slices(arena); + size_t bit_count = 0; + size_t bit_set_count = 0; + long bit_of_page = 0; + mi_ansi_color_t color_of_page = MI_GRAY; + for (size_t i = 0; i < chunk_count && bit_count < slice_count; i++) { + char buf[5*MI_BCHUNK_BITS + 64]; mi_memzero(buf, sizeof(buf)); + if (bit_count > used_slice_count && i+2 < chunk_count) { + const size_t diff = chunk_count - 1 - i; + bit_count += diff*MI_BCHUNK_BITS; + _mi_raw_message(" |\n"); + i = chunk_count-1; + } + + size_t k = 0; + + if (i<10) { buf[k++] = ('0' + (char)i); buf[k++] = ' '; buf[k++] = ' '; } + else if (i<100) { buf[k++] = ('0' + (char)(i/10)); buf[k++] = ('0' + (char)(i%10)); buf[k++] = ' '; } + else if (i<1000) { buf[k++] = ('0' + (char)(i/100)); buf[k++] = ('0' + (char)((i%100)/10)); buf[k++] = ('0' + (char)(i%10)); } + + char chunk_kind = ' '; + if (chunk_bins != NULL) { + switch (mi_bbitmap_debug_get_bin(chunk_bins,i)) { + case MI_CBIN_SMALL: chunk_kind = 'S'; break; + case MI_CBIN_MEDIUM: chunk_kind = 'M'; break; + case MI_CBIN_LARGE: chunk_kind = 'L'; break; + case MI_CBIN_HUGE: chunk_kind = 'H'; break; + case MI_CBIN_OTHER: chunk_kind = 'X'; break; + default: chunk_kind = ' '; break; // suppress warning + // case MI_CBIN_NONE: chunk_kind = 'N'; break; + } + } + buf[k++] = chunk_kind; + buf[k++] = ' '; + + for (size_t j = 0; j < MI_BCHUNK_FIELDS; j++) { + if (j > 0 && (j % fields_per_line) == 0) { + // buf[k++] = '\n'; mi_memset(buf+k,' ',7); k += 7; + _mi_raw_message(" %s\n\x1B[37m", buf); + mi_memzero(buf, sizeof(buf)); + mi_memset(buf, ' ', 5); k = 5; + } + if (bit_count < slice_count) { + mi_bfield_t bfield = 0; + if (chunks!=NULL) { + bfield = chunks[i].bfields[j]; + } + if (invert) bfield = ~bfield; + size_t xcount = (chunks==NULL ? mi_debug_show_page_bfield(buf, &k, arena, bit_count, &bit_of_page, &color_of_page) + : mi_debug_show_bfield(bfield, buf, &k)); + if (invert) xcount = MI_BFIELD_BITS - xcount; + bit_set_count += xcount; + buf[k++] = ' '; + } + else { + mi_memset(buf + k, 'o', MI_BFIELD_BITS); + k += MI_BFIELD_BITS; + } + bit_count += MI_BFIELD_BITS; + } + _mi_raw_message(" %s\n\x1B[37m", buf); + } + _mi_raw_message("\x1B[0m total pages: %zu\n", bit_set_count); + return bit_set_count; +} + +//static size_t mi_debug_show_bitmap_binned(const char* header1, const char* header2, const char* header3, size_t slice_count, +// mi_bitmap_t* bitmap, mi_bchunkmap_t* chunk_bins, bool invert, mi_arena_t* arena, bool narrow) { +// return mi_debug_show_chunks(header1, header2, header3, slice_count, mi_bitmap_chunk_count(bitmap), &bitmap->chunks[0], chunk_bins, invert, arena, narrow); +//} + +static void mi_debug_show_arenas_ex(mi_heap_t* heap, bool show_pages, bool narrow) mi_attr_noexcept { + mi_subproc_t* subproc = heap->subproc; + size_t max_arenas = mi_arenas_get_count(subproc); + //size_t free_total = 0; + //size_t slice_total = 0; + //size_t abandoned_total = 0; + size_t page_total = 0; + for (size_t i = 0; i < max_arenas; i++) { + mi_arena_t* arena = mi_atomic_load_ptr_acquire(mi_arena_t, &subproc->arenas[i]); + if (arena == NULL) continue; + mi_assert(arena->subproc == subproc); + // slice_total += arena->slice_count; + _mi_raw_message("%sarena %zu at %p: %zu slices (%zu MiB)%s%s, subproc: %zu, numa: %i\n", + (arena->parent==NULL ? "" : "(sub)"), i, arena, arena->slice_count, (size_t)(_mi_size_of_slices(arena->slice_count)/MI_MiB), + (arena->memid.is_pinned ? ", pinned" : ""), (arena->is_exclusive ? ", exclusive" : ""), + arena->subproc->subproc_seq, arena->numa_node); + //if (show_inuse) { + // free_total += mi_debug_show_bbitmap("in-use slices", arena->slice_count, arena->slices_free, true, NULL); + //} + //if (show_committed) { + // mi_debug_show_bitmap("committed slices", arena->slice_count, arena->slices_committed, false, NULL); + //} + // todo: abandoned slices + //if (show_purge) { + // purge_total += mi_debug_show_bitmap("purgeable slices", arena->slice_count, arena->slices_purge, false, NULL); + //} + if (show_pages) { + // mi_arena_pages_t* arena_pages = mi_heap_arena_pages(heap, arena); + // if (arena_pages != NULL) + { + const char* header1 = "chunks (p:page, f:full, s:single, m:meta-data, i:arena-info, P,F,S,M:not abandoned, ~:free-purgable, _:free-committed, .:free-reserved)"; + const char* header2 = (narrow ? "\n " : " "); + const char* header3 = "(chunk bin: S:small, M : medium, L : large, X : other)"; + page_total += mi_debug_show_chunks(header1, header2, header3, arena->slice_count, + mi_bbitmap_chunk_count(arena->slices_free), NULL, + arena->slices_free->chunkmap_bins, false, arena, narrow); + } + } + } + // if (show_inuse) _mi_raw_message("total inuse slices : %zu\n", slice_total - free_total); + // if (show_abandoned) _mi_raw_message("total abandoned slices: %zu\n", abandoned_total); + if (show_pages) _mi_raw_message("total pages in arenas: %zu\n", page_total); +} + +void mi_debug_show_arenas(void) mi_attr_noexcept { + mi_debug_show_arenas_ex(mi_heap_main(), true /* show pages */, true /* narrow? */); +} + +void mi_arenas_print(void) mi_attr_noexcept { + mi_debug_show_arenas(); +} + + +/* ----------------------------------------------------------- + Reserve a huge page arena. +----------------------------------------------------------- */ +// reserve at a specific numa node +int mi_reserve_huge_os_pages_at_ex(size_t pages, int numa_node, size_t timeout_msecs, bool exclusive, mi_arena_id_t* arena_id) mi_attr_noexcept { + if (arena_id != NULL) *arena_id = NULL; + if (pages==0) return 0; + if (numa_node < -1) numa_node = -1; + if (numa_node >= 0) numa_node = numa_node % _mi_os_numa_node_count(); + mi_subproc_t* subproc = _mi_subproc(); + size_t hsize = 0; + size_t pages_reserved = 0; + mi_memid_t memid; + void* p = _mi_os_alloc_huge_os_pages(subproc, pages, numa_node, timeout_msecs, &pages_reserved, &hsize, &memid); + if (p==NULL || pages_reserved==0) { + _mi_warning_message("failed to reserve %zu GiB huge pages\n", pages); + return ENOMEM; + } + _mi_verbose_message("numa node %i: reserved %zu GiB huge pages (of the %zu GiB requested)\n", numa_node, pages_reserved, pages); + + if (!mi_manage_os_memory_ex2(subproc, p, hsize, numa_node, exclusive, memid, NULL, NULL, arena_id)) { + _mi_os_free(subproc, p, hsize, memid); + return ENOMEM; + } + return 0; +} + +int mi_reserve_huge_os_pages_at(size_t pages, int numa_node, size_t timeout_msecs) mi_attr_noexcept { + return mi_reserve_huge_os_pages_at_ex(pages, numa_node, timeout_msecs, false, NULL); +} + +// reserve huge pages evenly among the given number of numa nodes (or use the available ones as detected) +int mi_reserve_huge_os_pages_interleave(size_t pages, size_t numa_nodes, size_t timeout_msecs) mi_attr_noexcept { + if (pages == 0) return 0; + + // pages per numa node + int numa_count = (numa_nodes > 0 && numa_nodes <= INT_MAX ? (int)numa_nodes : _mi_os_numa_node_count()); + if (numa_count <= 0) { numa_count = 1; } + const size_t pages_per = pages / numa_count; + const size_t pages_mod = pages % numa_count; + const size_t timeout_per = (timeout_msecs==0 ? 0 : (timeout_msecs / numa_count) + 50); + + // reserve evenly among numa nodes + for (int numa_node = 0; numa_node < numa_count && pages > 0; numa_node++) { + size_t node_pages = pages_per; // can be 0 + if ((size_t)numa_node < pages_mod) { node_pages++; } + int err = mi_reserve_huge_os_pages_at(node_pages, numa_node, timeout_per); + if (err) return err; + if (pages < node_pages) { + pages = 0; + } + else { + pages -= node_pages; + } + } + + return 0; +} + +int mi_reserve_huge_os_pages(size_t pages, double max_secs, size_t* pages_reserved) mi_attr_noexcept { + MI_UNUSED(max_secs); + _mi_warning_message("mi_reserve_huge_os_pages is deprecated: use mi_reserve_huge_os_pages_interleave/at instead\n"); + if (pages_reserved != NULL) *pages_reserved = 0; + int err = mi_reserve_huge_os_pages_interleave(pages, 0, (size_t)(max_secs * 1000.0)); + if (err==0 && pages_reserved!=NULL) *pages_reserved = pages; + return err; +} + + +/* ----------------------------------------------------------- + Unloading and reloading an arena. +----------------------------------------------------------- */ +/* +static bool mi_arena_page_register(size_t slice_index, size_t slice_count, mi_arena_t* arena, void* arg) { + MI_UNUSED(arg); MI_UNUSED(slice_count); + mi_assert_internal(slice_count == 1); + mi_page_t* page = mi_arena_page_at_slice(arena, slice_index); + mi_assert_internal(mi_bitmap_is_setN(page->memid.mem.arena.arena->pages, page->memid.mem.arena.slice_index, 1)); + if (!_mi_page_map_register(page)) return false; // break + mi_assert_internal(mi_ptr_page(page)==page); + return true; +} + +mi_decl_nodiscard static bool mi_arena_pages_reregister(mi_arena_t* arena) { + return _mi_bitmap_forall_set(arena->pages, &mi_arena_page_register, arena, NULL); +} + +mi_decl_export bool mi_arena_unload(mi_arena_id_t arena_id, void** base, size_t* accessed_size, size_t* full_size) { + mi_arena_t* arena = _mi_arena_from_id(arena_id); + if (arena==NULL) { + return false; + } + else if (!arena->is_exclusive) { + _mi_warning_message("cannot unload a non-exclusive arena (id %zu at %p)\n", arena_id, arena); + return false; + } + else if (arena->memid.memkind != MI_MEM_EXTERNAL) { + _mi_warning_message("can only unload managed arena's for external memory (id %zu at %p)\n", arena_id, arena); + return false; + } + + // find accessed size + const size_t asize = _mi_size_of_slices(mi_arena_used_slices(arena)); + if (base != NULL) { *base = (void*)arena; } + if (full_size != NULL) { *full_size = arena->memid.mem.os.size; } + if (accessed_size != NULL) { *accessed_size = asize; } + + // adjust abandoned page count + mi_subproc_t* const subproc = arena->subproc; + for (size_t bin = 0; bin < MI_ARENA_BIN_COUNT; bin++) { + const size_t count = mi_bitmap_popcount(arena->pages_abandoned[bin]); + if (count > 0) { mi_atomic_decrement_acq_rel(&subproc->abandoned_count[bin]); } + } + + // unregister the pages + _mi_page_map_unregister_range(arena, asize); + + // set arena entry to NULL + const size_t count = mi_arenas_get_count(subproc); + for(size_t i = 0; i < count; i++) { + if (mi_arena_from_index(subproc, i) == arena) { + mi_atomic_store_ptr_release(mi_arena_t, &subproc->arenas[i], NULL); + if (i + 1 == count) { // try adjust the count? + size_t expected = count; + mi_atomic_cas_strong_acq_rel(&subproc->arena_count, &expected, count-1); + } + break; + } + } + return true; +} + +mi_decl_export bool mi_arena_reload(void* start, size_t size, mi_commit_fun_t* commit_fun, void* commit_fun_arg, mi_arena_id_t* arena_id) { + // assume the memory area is already containing the arena + if (arena_id != NULL) { *arena_id = _mi_arena_id_none(); } + if (start == NULL || size == 0) return false; + mi_arena_t* arena = (mi_arena_t*)start; + mi_memid_t memid = arena->memid; + if (memid.memkind != MI_MEM_EXTERNAL) { + _mi_warning_message("can only reload arena's from external memory (%p)\n", arena); + return false; + } + if (memid.mem.os.base != start) { + _mi_warning_message("the reloaded arena base address differs from the external memory (arena: %p, external: %p)\n", arena, start); + return false; + } + if (memid.mem.os.size != size) { + _mi_warning_message("the reloaded arena size differs from the external memory (arena size: %zu, external size: %zu)\n", arena->memid.mem.os.size, size); + return false; + } + if (!arena->is_exclusive) { + _mi_warning_message("the reloaded arena is not exclusive\n"); + return false; + } + + // re-initialize + arena->is_exclusive = true; + arena->commit_fun = commit_fun; + arena->commit_fun_arg = commit_fun_arg; + arena->subproc = _mi_subproc(); + if (!mi_arenas_add(arena->subproc, arena, arena_id)) { + return false; + } + if (!mi_arena_pages_reregister(arena)) { + // todo: clear arena entry in the subproc? + return false; + } + + // adjust abandoned page count + for (size_t bin = 0; bin < MI_ARENA_BIN_COUNT; bin++) { + const size_t count = mi_bitmap_popcount(arena->pages_abandoned[bin]); + if (count > 0) { mi_atomic_decrement_acq_rel(&arena->subproc->abandoned_count[bin]); } + } + + return true; +} + +*/ diff --git a/Objects/mimalloc/arena/arena.h b/Objects/mimalloc/arena/arena.h new file mode 100644 index 000000000000000..2e01fa4c84d25ac --- /dev/null +++ b/Objects/mimalloc/arena/arena.h @@ -0,0 +1,87 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2019-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +#pragma once +#ifndef MI_ARENA_H +#define MI_ARENA_H + +#include "mimalloc/util/libc.h" +#include "mimalloc/arena/arena.h" +#include "bitmap.h" + +bool _mi_arena_reserve(mi_subproc_t* subproc, size_t req_size, bool allow_large, mi_arena_id_t* arena_id); +mi_decl_nodiscard bool _mi_arena_commit(mi_subproc_t* subproc, mi_arena_t* arena, void* start, size_t size, bool* is_zero, size_t stat_already_committed); +mi_decl_noinline void* _mi_arenas_try_alloc(mi_heap_t* heap, size_t slice_count, size_t alignment, bool commit, bool allow_large, mi_arena_t* req_arena, size_t tseq, int numa_node, mi_memid_t* memid); +void* _mi_arena_os_alloc_aligned(mi_subproc_t* subproc, size_t size, size_t alignment, size_t align_offset, bool commit, bool allow_large, mi_arena_id_t req_arena_id, mi_memid_t* memid); + +// Check if an arena is suitable for a request arena +static inline bool mi_arena_is_suitable(mi_arena_t* arena, mi_arena_t* req_arena) { + if (arena == req_arena) return true; // they match + if (arena == NULL) return false; + if (req_arena == NULL && !arena->is_exclusive) return true; // or the arena is not exclusive, and we didn't request a specific one + if (arena->parent != NULL && arena->parent == req_arena) return true; // sub-arena? (note that req_arena is never a sub arena) + return false; +} + +// Get the number of arenas in a subproc +static inline size_t mi_arenas_get_count(mi_subproc_t* subproc) { + return mi_atomic_load_relaxed(&subproc->arena_count); +} + +static inline mi_arena_t* mi_arena_from_index(mi_subproc_t* subproc, size_t idx) { + mi_assert_internal(idx < mi_arenas_get_count(subproc)); + return mi_atomic_load_ptr_acquire(mi_arena_t, &subproc->arenas[idx]); +} + +static inline size_t mi_arena_info_slices(mi_arena_t* arena) { + return arena->info_slices; +} + +// slices reserved for page meta info at the start of aligned chunks +static inline size_t mi_arena_page_meta_aligned_slice_count(void) { + #if MI_PAGE_META_IS_ALIGNED + return mi_divide_up(MI_PAGE_META_ALIGNED_COUNT * sizeof(mi_page_t), MI_ARENA_SLICE_SIZE); + #else + return 0; + #endif +} + +// Start of the arena memory area +static inline uint8_t* mi_arena_start(mi_arena_t* arena) { + return ((uint8_t*)arena->start); +} + +// Start of a slice +static inline uint8_t* mi_arena_slice_start(mi_arena_t* arena, size_t slice_index) { + mi_assert_internal(slice_index < arena->slice_count); + return (mi_arena_start(arena) + _mi_size_of_slices(slice_index)); +} + +// get the arena and slice span +static inline mi_arena_t* mi_arena_from_memid(mi_memid_t memid, size_t* slice_index, size_t* slice_count) { + mi_assert_internal(memid.memkind == MI_MEM_ARENA); + mi_arena_t* arena = memid.mem.arena.arena; + if (slice_index!=NULL) { *slice_index = memid.mem.arena.slice_index; } + if (slice_count!=NULL) { *slice_count = memid.mem.arena.slice_count; } + return arena; +} + +// initialize the bitmap for `slice_count` slices at base (and advance the base pointer) +static inline mi_bitmap_t* mi_arena_bitmap_init(size_t slice_count, uint8_t** base) { + mi_bitmap_t* bitmap = (mi_bitmap_t*)(*base); + *base = (*base) + mi_bitmap_init(bitmap, slice_count, true /* already zero */); + return bitmap; +} + +typedef bool (mi_forall_arena_fun_t)(mi_arena_t* arena, const void* arg, void** result); + +bool mi_forall_arenas(mi_heap_t* heap, mi_arena_t* req_arena, size_t tseq, mi_forall_arena_fun_t* visit, const void* arg, void** result); +bool mi_forall_suitable_arenas(mi_heap_t* heap, mi_arena_t* req_arena, size_t tseq, bool match_numa, int numa_node, + bool allow_large, mi_forall_arena_fun_t* visit, const void* arg, void** result); + + +#endif diff --git a/Objects/mimalloc/arena/bitmap-chunk.c b/Objects/mimalloc/arena/bitmap-chunk.c new file mode 100644 index 000000000000000..32122ff1ab87197 --- /dev/null +++ b/Objects/mimalloc/arena/bitmap-chunk.c @@ -0,0 +1,1052 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2019-2026 Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +/* ---------------------------------------------------------------------------- +Concurrent bitmap that can set/reset sequences of bits atomically +---------------------------------------------------------------------------- */ + +#include "mimalloc.h" +#include "mimalloc/prim.h" // _mi_prim_thread_yield +#include "mimalloc/util/libc.h" +#include "mimalloc/util/stats.h" +#include "bitmap-chunk.h" + +#ifndef MI_OPT_SIMD +#define MI_OPT_SIMD 0 +#endif + +/* -------------------------------------------------------------------------------- + bfields +-------------------------------------------------------------------------------- */ + +static inline size_t mi_bfield_ctz(mi_bfield_t x) { + return mi_ctz(x); +} + +static inline size_t mi_bfield_clz(mi_bfield_t x) { + return mi_clz(x); +} + +size_t mi_bfield_popcount(mi_bfield_t x) { + return mi_popcount(x); +} + +mi_bfield_t mi_bfield_clear_least_bit(mi_bfield_t x) { + return (x & (x-1)); +} + +// find the least significant bit that is set (i.e. count trailing zero's) +// return false if `x==0` (with `*idx` undefined) and true otherwise, +// with the `idx` is set to the bit index (`0 <= *idx < MI_BFIELD_BITS`). +bool mi_bfield_find_least_bit(mi_bfield_t x, size_t* idx) { + return mi_bsf(x,idx); +} + +// find the most significant bit that is set. +// return false if `x==0` (with `*idx` undefined) and true otherwise, +// with the `idx` is set to the bit index (`0 <= *idx < MI_BFIELD_BITS`). +bool mi_bfield_find_highest_bit(mi_bfield_t x, size_t* idx) { + return mi_bsr(x, idx); +} + + + +// find each set bit in a bit field `x` and clear it, until it becomes zero. +bool mi_bfield_foreach_bit(mi_bfield_t* x, size_t* idx) { + const bool found = mi_bfield_find_least_bit(*x, idx); + *x = mi_bfield_clear_least_bit(*x); + return found; +} + +static inline mi_bfield_t mi_bfield_zero(void) { + return 0; +} + +static inline mi_bfield_t mi_bfield_one(void) { + return 1; +} + +static inline mi_bfield_t mi_bfield_all_set(void) { + return ~((mi_bfield_t)0); +} + +// mask of `bit_count` bits set shifted to the left by `shiftl` +mi_bfield_t mi_bfield_mask(size_t bit_count, size_t shiftl) { + mi_assert_internal(bit_count > 0); + mi_assert_internal(bit_count + shiftl <= MI_BFIELD_BITS); + mi_assert_internal(shiftl < MI_BFIELD_BITS); + const mi_bfield_t mask0 = (bit_count < MI_BFIELD_BITS ? (mi_bfield_one() << bit_count)-1 : mi_bfield_all_set()); + return (mask0 << shiftl); +} + + +// ------- mi_bfield_atomic_set --------------------------------------- +// the `_set` functions return also the count of bits that were already set (for commit statistics) +// the `_clear` functions return also whether the new bfield is all clear or not (for the chunk_map) + +// Set a bit atomically. Returns `true` if the bit transitioned from 0 to 1 +static inline bool mi_bfield_atomic_set(_Atomic(mi_bfield_t)*b, size_t idx) { + mi_assert_internal(idx < MI_BFIELD_BITS); + const mi_bfield_t mask = mi_bfield_mask(1, idx);; + const mi_bfield_t old = mi_atomic_or_acq_rel(b, mask); + return ((old&mask) == 0); +} + +// Clear a bit atomically. Returns `true` if the bit transitioned from 1 to 0. +// `all_clear` is set if the new bfield is zero. +static inline bool mi_bfield_atomic_clear(_Atomic(mi_bfield_t)*b, size_t idx, bool* all_clear) { + mi_assert_internal(idx < MI_BFIELD_BITS); + const mi_bfield_t mask = mi_bfield_mask(1, idx);; + mi_bfield_t old = mi_atomic_and_acq_rel(b, ~mask); + if (all_clear != NULL) { *all_clear = ((old&~mask)==0); } + return ((old&mask) == mask); +} + +// Clear a bit but only when/once it is set. This is used by concurrent free's while +// the page is abandoned and mapped. This can incur a busy wait :-( but it should +// be quite rare (and is accounted for in the stats) +static inline void mi_bfield_atomic_clear_once_set(mi_subproc_t* subproc, _Atomic(mi_bfield_t)*b, size_t idx) { + mi_assert_internal(idx < MI_BFIELD_BITS); + const mi_bfield_t mask = mi_bfield_mask(1, idx);; + mi_bfield_t old = mi_atomic_load_relaxed(b); + do { + if mi_unlikely((old&mask) == 0) { + old = mi_atomic_load_acquire(b); + if ((old&mask)==0) { + mi_subproc_stat_counter_increase(subproc, pages_unabandon_busy_wait, 1); + } + while ((old&mask)==0) { // busy wait + _mi_prim_thread_yield(); + old = mi_atomic_load_acquire(b); + } + } + } while (!mi_atomic_cas_weak_acq_rel(b,&old, (old&~mask))); + mi_assert_internal((old&mask)==mask); // we should only clear when it was set +} + +// Set a mask set of bits atomically, and return true of the mask bits transitioned from all 0's to 1's. +// `already_set` contains the count of bits that were already set (used when committing ranges to account +// statistics correctly). +static inline bool mi_bfield_atomic_set_mask(_Atomic(mi_bfield_t)*b, mi_bfield_t mask, size_t* already_set) { + mi_assert_internal(mask != 0); + const mi_bfield_t old = mi_atomic_or_acq_rel(b,mask); + if (already_set!=NULL) { *already_set = mi_bfield_popcount(old&mask); } + return ((old&mask) == 0); +} + +// Clear a mask set of bits atomically, and return true of the mask bits transitioned from all 1's to 0's +// `all_clear` is set to `true` if the new bfield became zero. +static inline bool mi_bfield_atomic_clear_mask(_Atomic(mi_bfield_t)*b, mi_bfield_t mask, bool* all_clear) { + mi_assert_internal(mask != 0); + const mi_bfield_t old = mi_atomic_and_acq_rel(b,~mask); + if (all_clear != NULL) { *all_clear = ((old&~mask)==0); } + return ((old&mask) == mask); +} + +static inline bool mi_bfield_atomic_setX(_Atomic(mi_bfield_t)*b, size_t* already_set) { + const mi_bfield_t old = mi_atomic_exchange_release(b, mi_bfield_all_set()); + if (already_set!=NULL) { *already_set = mi_bfield_popcount(old); } + return (old==0); +} + + +// ------- mi_bfield_atomic_try_clear --------------------------------------- + +// Tries to clear a mask atomically, and returns true if the mask bits atomically transitioned from mask to 0 +// and false otherwise (leaving the bit field as is). Returns the acquired value back in `expect` (regardless of success). +// `all_clear` is set to `true` if the new bfield became zero. +// `did_temp_clear_bits` is set to `true` some bits were temporarily cleared. +static inline bool mi_bfield_atomic_try_clear_mask_optimistic(_Atomic(mi_bfield_t)*b, mi_bfield_t mask, mi_bfield_t* previous, bool* did_temp_clear_bits) { + mi_assert_internal(mask != 0); + // note: we could also use a strong cas but generally an optimistic atomic and/or is more efficient (at least on arm64) + // try to atomically clear the mask bits + mi_bfield_t old = mi_atomic_and_acq_rel(b,~mask); + if (previous!=NULL) { *previous = old; } + if mi_likely((old&mask)==mask) { + // all bits in the mask transitioned from 1 to 0 atomically + return true; + } + else { + // failed to transition all + if ((old&mask)!=0) { + // restore accidentally cleared ones + mi_atomic_or_acq_rel(b, old&mask); + if (did_temp_clear_bits!=NULL) { *did_temp_clear_bits = true; } + } + return false; + } +} + +// Tries to clear a bit atomically. For performance, it assumes there is a good chance of success. +// Returns `true` if the bit transitioned from 1 to 0 +// and `false` otherwise (leaving the bfield `b` as-is). +// `all_clear` is set to true if the new bfield became zero (and false otherwise) +mi_decl_maybe_unused static inline bool mi_bfield_atomic_try_clear_optimistic(_Atomic(mi_bfield_t)* b, size_t idx, mi_bfield_t* previous) { + mi_assert_internal(idx < MI_BFIELD_BITS); + const mi_bfield_t mask = ((mi_bfield_t)1<bfields[i], idx); + if (already_set != NULL) { *already_set = (was_clear ? 0 : 1); } + return was_clear; +} + +// Set `0 < n <= MI_BFIELD_BITS`, and return true of the mask bits transitioned from all 0's to 1's. +// `already_set` contains the count of bits that were already set (used when committing ranges to account +// statistics correctly). +// Can cross over two bfields. +static inline bool mi_bchunk_setNX(mi_bchunk_t* chunk, size_t cidx, size_t n, size_t* already_set) { + mi_assert_internal(cidx < MI_BCHUNK_BITS); + mi_assert_internal(n > 0 && n <= MI_BFIELD_BITS); + const size_t i = cidx / MI_BFIELD_BITS; + const size_t idx = cidx % MI_BFIELD_BITS; + if mi_likely(idx + n <= MI_BFIELD_BITS) { + // within one field + return mi_bfield_atomic_set_mask(&chunk->bfields[i], mi_bfield_mask(n,idx), already_set); + } + else { + // spanning two fields + const size_t m = MI_BFIELD_BITS - idx; // bits to clear in the first field + mi_assert_internal(m < n); + mi_assert_internal(i < MI_BCHUNK_FIELDS - 1); + mi_assert_internal(idx + m <= MI_BFIELD_BITS); + size_t already_set1; + const bool all_set1 = mi_bfield_atomic_set_mask(&chunk->bfields[i], mi_bfield_mask(m, idx), &already_set1); + mi_assert_internal(n - m > 0); + mi_assert_internal(n - m < MI_BFIELD_BITS); + size_t already_set2; + const bool all_set2 = mi_bfield_atomic_set_mask(&chunk->bfields[i+1], mi_bfield_mask(n - m, 0), &already_set2); + if (already_set != NULL) { *already_set = already_set1 + already_set2; } + return (all_set1 && all_set2); + } +} + +// Set a sequence of `n` bits within a chunk. +// Returns true if all bits transitioned from 0 to 1 (or 1 to 0). +mi_decl_noinline static bool mi_bchunk_xsetNC(mi_xset_t set, mi_bchunk_t* chunk, size_t cidx, size_t n, size_t* palready_set, bool* pmaybe_all_clear) { + mi_assert_internal(cidx + n <= MI_BCHUNK_BITS); + mi_assert_internal(n>0); + bool all_transition = true; + bool maybe_all_clear = true; + size_t total_already_set = 0; + size_t idx = cidx % MI_BFIELD_BITS; + size_t field = cidx / MI_BFIELD_BITS; + while (n > 0) { + size_t m = MI_BFIELD_BITS - idx; // m is the bits to xset in this field + if (m > n) { m = n; } + mi_assert_internal(idx + m <= MI_BFIELD_BITS); + mi_assert_internal(field < MI_BCHUNK_FIELDS); + const mi_bfield_t mask = mi_bfield_mask(m, idx); + size_t already_set = 0; + bool all_clear = false; + const bool transition = (set ? mi_bfield_atomic_set_mask(&chunk->bfields[field], mask, &already_set) + : mi_bfield_atomic_clear_mask(&chunk->bfields[field], mask, &all_clear)); + mi_assert_internal(!set || ((transition && already_set == 0) || (!transition && already_set > 0))); + all_transition = all_transition && transition; + total_already_set += already_set; + maybe_all_clear = maybe_all_clear && all_clear; + // next field + field++; + idx = 0; + mi_assert_internal(m <= n); + n -= m; + } + if (palready_set!=NULL) { *palready_set = total_already_set; } + if (pmaybe_all_clear!=NULL) { *pmaybe_all_clear = maybe_all_clear; } + return all_transition; +} + +bool mi_bchunk_setN(mi_bchunk_t* chunk, size_t cidx, size_t n, size_t* already_set) { + mi_assert_internal(n>0 && n <= MI_BCHUNK_BITS); + if (n==1) return mi_bchunk_set(chunk, cidx, already_set); + // if (n==8 && (cidx%8) == 0) return mi_bchunk_set8(chunk, cidx, already_set); + // if (n==MI_BFIELD_BITS) return mi_bchunk_setX(chunk, cidx, already_set); + if (n<=MI_BFIELD_BITS) return mi_bchunk_setNX(chunk, cidx, n, already_set); + return mi_bchunk_xsetNC(MI_BIT_SET, chunk, cidx, n, already_set, NULL); +} + +// ------- mi_bchunk_clear --------------------------------------- + +bool mi_bchunk_clear(mi_bchunk_t* chunk, size_t cidx, bool* all_clear) { + mi_assert_internal(cidx < MI_BCHUNK_BITS); + const size_t i = cidx / MI_BFIELD_BITS; + const size_t idx = cidx % MI_BFIELD_BITS; + return mi_bfield_atomic_clear(&chunk->bfields[i], idx, all_clear); +} + +bool mi_bchunk_clearN(mi_bchunk_t* chunk, size_t cidx, size_t n, bool* maybe_all_clear) { + mi_assert_internal(n>0 && n <= MI_BCHUNK_BITS); + if (n==1) return mi_bchunk_clear(chunk, cidx, maybe_all_clear); + // if (n==8) return mi_bchunk_clear8(chunk, cidx, maybe_all_clear); + // if (n==MI_BFIELD_BITS) return mi_bchunk_clearX(chunk, cidx, maybe_all_clear); + // todo: implement mi_bchunk_xsetNX instead of setNX + return mi_bchunk_xsetNC(MI_BIT_CLEAR, chunk, cidx, n, NULL, maybe_all_clear); +} + +// Check if a sequence of `n` bits within a chunk are all set/cleared. +// This can cross bfield's +mi_decl_noinline static size_t mi_bchunk_popcountNC(mi_bchunk_t* chunk, size_t field_idx, size_t idx, size_t n) { + mi_assert_internal((field_idx*MI_BFIELD_BITS) + idx + n <= MI_BCHUNK_BITS); + size_t count = 0; + while (n > 0) { + size_t m = MI_BFIELD_BITS - idx; // m is the bits to xset in this field + if (m > n) { m = n; } + mi_assert_internal(idx + m <= MI_BFIELD_BITS); + mi_assert_internal(field_idx < MI_BCHUNK_FIELDS); + const size_t mask = mi_bfield_mask(m, idx); + count += mi_bfield_atomic_popcount_mask(&chunk->bfields[field_idx], mask); + // next field + field_idx++; + idx = 0; + n -= m; + } + return count; +} + +// Count set bits a sequence of `n` bits. +size_t mi_bchunk_popcountN(mi_bchunk_t* chunk, size_t cidx, size_t n) { + mi_assert_internal(cidx + n <= MI_BCHUNK_BITS); + mi_assert_internal(n>0); + if (n==0) return 0; + const size_t i = cidx / MI_BFIELD_BITS; + const size_t idx = cidx % MI_BFIELD_BITS; + if (n==1) { return (mi_bfield_atomic_is_set(&chunk->bfields[i], idx) ? 1 : 0); } + if (idx + n <= MI_BFIELD_BITS) { return mi_bfield_atomic_popcount_mask(&chunk->bfields[i], mi_bfield_mask(n, idx)); } + return mi_bchunk_popcountNC(chunk, i, idx, n); +} + + +// ------- mi_bchunk_is_xset --------------------------------------- + +// Check if a sequence of `n` bits within a chunk are all set/cleared. +// This can cross bfield's +mi_decl_noinline static bool mi_bchunk_is_xsetNC(mi_xset_t set, const mi_bchunk_t* chunk, size_t field_idx, size_t idx, size_t n) { + mi_assert_internal((field_idx*MI_BFIELD_BITS) + idx + n <= MI_BCHUNK_BITS); + while (n > 0) { + size_t m = MI_BFIELD_BITS - idx; // m is the bits to xset in this field + if (m > n) { m = n; } + mi_assert_internal(idx + m <= MI_BFIELD_BITS); + mi_assert_internal(field_idx < MI_BCHUNK_FIELDS); + const size_t mask = mi_bfield_mask(m, idx); + if (!mi_bfield_atomic_is_xset_mask(set, &chunk->bfields[field_idx], mask)) { + return false; + } + // next field + field_idx++; + idx = 0; + n -= m; + } + return true; +} + +// Check if a sequence of `n` bits within a chunk are all set/cleared. +bool mi_bchunk_is_xsetN(mi_xset_t set, const mi_bchunk_t* chunk, size_t cidx, size_t n) { + mi_assert_internal(cidx + n <= MI_BCHUNK_BITS); + mi_assert_internal(n>0); + if (n==0) return true; + const size_t i = cidx / MI_BFIELD_BITS; + const size_t idx = cidx % MI_BFIELD_BITS; + if (n==1) { return mi_bfield_atomic_is_xset(set, &chunk->bfields[i], idx); } + if (idx + n <= MI_BFIELD_BITS) { return mi_bfield_atomic_is_xset_mask(set, &chunk->bfields[i], mi_bfield_mask(n, idx)); } + return mi_bchunk_is_xsetNC(set, chunk, i, idx, n); +} + + +// ------- mi_bchunk_try_clear --------------------------------------- + +// Clear `0 < n <= MI_BITFIELD_BITS`. Can cross over a bfield boundary. +static inline bool mi_bchunk_try_clearNX(mi_bchunk_t* chunk, size_t cidx, size_t n, bool* pmaybe_all_clear, bool* did_temp_clear_bits) { + mi_assert_internal(cidx < MI_BCHUNK_BITS); + mi_assert_internal(n <= MI_BFIELD_BITS); + const size_t i = cidx / MI_BFIELD_BITS; + const size_t idx = cidx % MI_BFIELD_BITS; + if mi_likely(idx + n <= MI_BFIELD_BITS) { + // within one field + return mi_bfield_atomic_try_clear_mask(&chunk->bfields[i], mi_bfield_mask(n, idx), pmaybe_all_clear, did_temp_clear_bits); + } + else { + // spanning two fields (todo: use double-word atomic ops?) + const size_t m = MI_BFIELD_BITS - idx; // bits to clear in the first field + mi_assert_internal(m < n); + mi_assert_internal(i < MI_BCHUNK_FIELDS - 1); + bool field1_is_clear; + if (!mi_bfield_atomic_try_clear_mask(&chunk->bfields[i], mi_bfield_mask(m, idx), &field1_is_clear, did_temp_clear_bits)) return false; + // try the second field as well + mi_assert_internal(n - m > 0); + mi_assert_internal(n - m < MI_BFIELD_BITS); + bool field2_is_clear; + if (!mi_bfield_atomic_try_clear_mask(&chunk->bfields[i+1], mi_bfield_mask(n - m, 0), &field2_is_clear, did_temp_clear_bits)) { + // we failed to clear the second field, restore the first one + mi_bfield_atomic_set_mask(&chunk->bfields[i], mi_bfield_mask(m, idx), NULL); + if (did_temp_clear_bits != NULL) { *did_temp_clear_bits = true; } + return false; + } + if (pmaybe_all_clear != NULL) { *pmaybe_all_clear = field1_is_clear && field2_is_clear; } + return true; + } +} + +// Clear a full aligned bfield. +// static inline bool mi_bchunk_try_clearX(mi_bchunk_t* chunk, size_t cidx, bool* pmaybe_all_clear) { +// mi_assert_internal(cidx < MI_BCHUNK_BITS); +// mi_assert_internal((cidx%MI_BFIELD_BITS) == 0); +// const size_t i = cidx / MI_BFIELD_BITS; +// return mi_bfield_atomic_try_clearX(&chunk->bfields[i], pmaybe_all_clear); +// } + +// Try to atomically clear a sequence of `n` bits within a chunk. +// Returns true if all bits transitioned from 1 to 0, +// and false otherwise leaving all bit fields as is. +// Note: this is the complex one as we need to unwind partial atomic operations if we fail halfway.. +// `maybe_all_clear` is set to `true` if all the bfields involved become zero. +mi_decl_noinline static bool mi_bchunk_try_clearNC(mi_bchunk_t* chunk, size_t cidx, size_t n, bool* pmaybe_all_clear, bool* did_temp_clear_bits) { + mi_assert_internal(cidx + n <= MI_BCHUNK_BITS); + mi_assert_internal(n>0); + if (pmaybe_all_clear != NULL) { *pmaybe_all_clear = true; } + if (n==0) return true; + + // first field + const size_t start_idx = cidx % MI_BFIELD_BITS; + const size_t start_field = cidx / MI_BFIELD_BITS; + size_t field = start_field; + size_t m = MI_BFIELD_BITS - start_idx; // m are the bits to clear in this field + if (m > n) { m = n; } + mi_assert_internal(start_idx + m <= MI_BFIELD_BITS); + mi_assert_internal(start_field < MI_BCHUNK_FIELDS); + const mi_bfield_t mask_start = mi_bfield_mask(m, start_idx); + bool maybe_all_clear; + if (!mi_bfield_atomic_try_clear_mask(&chunk->bfields[field], mask_start, &maybe_all_clear, did_temp_clear_bits)) return false; + + // done? + mi_assert_internal(m <= n); + n -= m; + + // continue with mid fields and last field: if these fail we need to recover by unsetting previous fields + // mid fields? + while (n >= MI_BFIELD_BITS) { + field++; + mi_assert_internal(field < MI_BCHUNK_FIELDS); + bool field_is_clear; + if (!mi_bfield_atomic_try_clearX(&chunk->bfields[field], &field_is_clear)) goto restore; + maybe_all_clear = maybe_all_clear && field_is_clear; + n -= MI_BFIELD_BITS; + } + + // last field? + if (n > 0) { + mi_assert_internal(n < MI_BFIELD_BITS); + field++; + mi_assert_internal(field < MI_BCHUNK_FIELDS); + const mi_bfield_t mask_end = mi_bfield_mask(n, 0); + bool field_is_clear; + if (!mi_bfield_atomic_try_clear_mask(&chunk->bfields[field], mask_end, &field_is_clear, did_temp_clear_bits)) goto restore; + maybe_all_clear = maybe_all_clear && field_is_clear; + } + + if (pmaybe_all_clear != NULL) { *pmaybe_all_clear = maybe_all_clear; } + return true; + +restore: + // `field` is the index of the field that failed to set atomically; we need to restore all previous fields + mi_assert_internal(field > start_field); + if (did_temp_clear_bits != NULL) { *did_temp_clear_bits = true; } + while( field > start_field) { + field--; + if (field == start_field) { + mi_bfield_atomic_set_mask(&chunk->bfields[field], mask_start, NULL); + } + else { + mi_bfield_atomic_setX(&chunk->bfields[field], NULL); // mid-field: set all bits again + } + } + return false; +} + + +bool mi_bchunk_try_clearN(mi_bchunk_t* chunk, size_t cidx, size_t n, bool* maybe_all_clear, bool* did_temp_clear_bits) { + mi_assert_internal(n>0); + // if (n==MI_BFIELD_BITS) return mi_bchunk_try_clearX(chunk, cidx, maybe_all_clear); + if (n<=MI_BFIELD_BITS) return mi_bchunk_try_clearNX(chunk, cidx, n, maybe_all_clear, did_temp_clear_bits); + return mi_bchunk_try_clearNC(chunk, cidx, n, maybe_all_clear, did_temp_clear_bits); +} + + +// ------- mi_bchunk_try_find_and_clear --------------------------------------- + +#if MI_OPT_SIMD && defined(__AVX2__) +mi_decl_maybe_unused static inline __m256i mi_mm256_zero(void) { + return _mm256_setzero_si256(); +} +mi_decl_maybe_unused static inline __m256i mi_mm256_ones(void) { + return _mm256_set1_epi64x(~0); +} +mi_decl_maybe_unused static inline bool mi_mm256_is_ones(__m256i vec) { + return _mm256_testc_si256(vec, _mm256_cmpeq_epi32(vec, vec)); +} +mi_decl_maybe_unused static inline bool mi_mm256_is_zero( __m256i vec) { + return _mm256_testz_si256(vec,vec); +} +#endif + + +static inline bool mi_bchunk_try_find_and_clear_at(mi_bchunk_t* chunk, size_t chunk_idx, size_t* pidx) { + mi_assert_internal(chunk_idx < MI_BCHUNK_FIELDS); + _Atomic(mi_bfield_t)* const bfield = &chunk->bfields[chunk_idx]; + mi_bfield_t b = mi_atomic_load_relaxed(bfield); + if (b==0) return false; + int tries = 0; + do { + const mi_bfield_t mask = (b & (~b+1)); // == (b & -b) but avoids a compiler warning -- clear all bits except the least-significant one + b = mi_atomic_and_acq_rel(bfield,~mask); // clear the bit and set `b` to the previous value + if mi_likely((b&mask)==mask) { // if we transitioned from 1 to 0, we actually cleared it + size_t bitidx = 0; + mi_bfield_find_least_bit(mask,&bitidx); + *pidx = (chunk_idx*MI_BFIELD_BITS) + bitidx; + mi_assert_internal(*pidx < MI_BCHUNK_BITS); + return true; + } + } while (b!=0 && ++tries <= 4); // limit tries to reduce possible contention + return false; +} + +// Find least 1-bit in a chunk and try to clear it atomically +// set `*pidx` to the bit index (0 <= *pidx < MI_BCHUNK_BITS) on success. +// This is used to find free slices and abandoned pages and should be efficient. +// todo: try neon version +bool mi_bchunk_try_find_and_clear(mi_bchunk_t* chunk, size_t* pidx) { + #if MI_OPT_SIMD && defined(__AVX2__) && (MI_BCHUNK_BITS==256) && (MI_BFIELD_SIZE==8) + for(int tries=0; tries<4; tries++) { // paranoia: at most 4 tries + const __m256i vec = _mm256_load_si256((const __m256i*)chunk->bfields); + const __m256i vcmp = _mm256_cmpeq_epi64(vec, mi_mm256_zero()); // (elem64 == 0 ? 0xFF : 0) + const uint32_t mask = ~_mm256_movemask_epi8(vcmp); // mask of most significant bit of each byte (so each 8 bits are all set or clear) + // mask is inverted, so each 8-bits is 0xFF iff the corresponding elem64 has a bit set (and thus can be cleared) + if (mask==0) return false; + mi_assert_internal((_tzcnt_u32(mask)%8) == 0); // tzcnt == 0, 8, 16, or 24 + const size_t chunk_idx = _tzcnt_u32(mask) / 8; + if (mi_bchunk_try_find_and_clear_at(chunk, chunk_idx, pidx)) return true; + // try again + // note: there must be an atomic release/acquire in between or otherwise the registers may not be reloaded + // we add an explicit memory barrier as older gcc compilers do not reload the registers even with an atomic acquire (issue #1206) + #if defined(__GNUC__) + __asm __volatile ("" : : "g"(chunk) : "memory"); + #endif + } + #elif MI_OPT_SIMD && defined(__AVX2__) && (MI_BCHUNK_BITS==512) && (MI_BFIELD_SIZE==8) + for(int tries=0; tries<4; tries++) { // paranoia: at most 4 tries + size_t chunk_idx = 0; + #if 0 + // one vector at a time + __m256i vec = _mm256_load_si256((const __m256i*)chunk->bfields); + if (mi_mm256_is_zero(vec)) { + chunk_idx += 4; + vec = _mm256_load_si256(((const __m256i*)chunk->bfields) + 1); + } + const __m256i vcmp = _mm256_cmpeq_epi64(vec, mi_mm256_zero()); // (elem64 == 0 ? 0xFF : 0) + const uint32_t mask = ~_mm256_movemask_epi8(vcmp); // mask of most significant bit of each byte (so each 8 bits are all set or clear) + // mask is inverted, so each 8-bits is 0xFF iff the corresponding elem64 has a bit set (and thus can be cleared) + if (mask==0) return false; + mi_assert_internal((_tzcnt_u32(mask)%8) == 0); // tzcnt == 0, 8, 16, or 24 + chunk_idx += _tzcnt_u32(mask) / 8; + #else + // a cache line is 64b so we can just as well load all at the same time + const __m256i vec1 = _mm256_load_si256((const __m256i*)chunk->bfields); + const __m256i vec2 = _mm256_load_si256(((const __m256i*)chunk->bfields)+1); + const __m256i cmpv = mi_mm256_zero(); + const __m256i vcmp1 = _mm256_cmpeq_epi64(vec1, cmpv); // (elem64 == 0 ? 0xFF : 0) + const __m256i vcmp2 = _mm256_cmpeq_epi64(vec2, cmpv); // (elem64 == 0 ? 0xFF : 0) + const uint32_t mask1 = ~_mm256_movemask_epi8(vcmp1); // mask of most significant bit of each byte (so each 8 bits are all set or clear) + const uint32_t mask2 = ~_mm256_movemask_epi8(vcmp2); // mask of most significant bit of each byte (so each 8 bits are all set or clear) + const uint64_t mask = ((uint64_t)mask2 << 32) | mask1; + // mask is inverted, so each 8-bits is 0xFF iff the corresponding elem64 has a bit set (and thus can be cleared) + if (mask==0) return false; + mi_assert_internal((_tzcnt_u64(mask)%8) == 0); // tzcnt == 0, 8, 16, 24 , .. + chunk_idx = mi_ctz(mask) / 8; + #endif + if (mi_bchunk_try_find_and_clear_at(chunk, chunk_idx, pidx)) return true; + // try again + // note: there must be an atomic release/acquire in between or otherwise the registers may not be reloaded + // we add an explicit memory barrier as older gcc compilers do not reload the registers even with an atomic acquire (issue #1206) + #if defined(__GNUC__) + __asm __volatile ("" : : "g"(chunk) : "memory"); + #endif + } + #elif MI_OPT_SIMD && (MI_BCHUNK_BITS==512) && MI_ARCH_ARM64 + for(int tries=0; tries<4; tries++) { // paranoia: at most 4 tries + // a cache line is 64b so we can just as well load all at the same time (?) + const uint64x2_t vzero1_lo = vceqzq_u64(vld1q_u64((uint64_t*)chunk->bfields)); // 2x64 bit is_zero + const uint64x2_t vzero1_hi = vceqzq_u64(vld1q_u64((uint64_t*)chunk->bfields + 2)); // 2x64 bit is_zero + const uint64x2_t vzero2_lo = vceqzq_u64(vld1q_u64((uint64_t*)chunk->bfields + 4)); // 2x64 bit is_zero + const uint64x2_t vzero2_hi = vceqzq_u64(vld1q_u64((uint64_t*)chunk->bfields + 6)); // 2x64 bit is_zero + const uint32x4_t vzero1 = vuzp1q_u32(vreinterpretq_u32_u64(vzero1_lo),vreinterpretq_u32_u64(vzero1_hi)); // unzip even elements: narrow to 4x32 bit is_zero () + const uint32x4_t vzero2 = vuzp1q_u32(vreinterpretq_u32_u64(vzero2_lo),vreinterpretq_u32_u64(vzero2_hi)); // unzip even elements: narrow to 4x32 bit is_zero () + const uint32x4_t vzero1x = vreinterpretq_u32_u64(vshrq_n_u64(vreinterpretq_u64_u32(vzero1), 24)); // shift-right 2x32bit elem by 24: lo 16 bits contain the 2 lo bytes + const uint32x4_t vzero2x = vreinterpretq_u32_u64(vshrq_n_u64(vreinterpretq_u64_u32(vzero2), 24)); + const uint16x8_t vzero12 = vreinterpretq_u16_u32(vuzp1q_u32(vzero1x,vzero2x)); // unzip even 32-bit elements into one vector + const uint8x8_t vzero = vmovn_u16(vzero12); // narrow the bottom 16-bits + const uint64_t mask = ~vget_lane_u64(vreinterpret_u64_u8(vzero), 0); // 1 byte for each bfield (0xFF => bfield has a bit set) + if (mask==0) return false; + mi_assert_internal((mi_ctz(mask)%8) == 0); // tzcnt == 0, 8, 16, 24 , .. + const size_t chunk_idx = mi_ctz(mask) / 8; + if (mi_bchunk_try_find_and_clear_at(chunk, chunk_idx, pidx)) return true; + // try again + // note: there must be an atomic release/acquire in between or otherwise the registers may not be reloaded + // we add an explicit memory barrier as older gcc compilers do not reload the registers even with an atomic acquire (issue #1206) + #if defined(__GNUC__) + __asm __volatile ("" : : "g"(chunk) : "memory"); + #endif + } + #else + for (int i = 0; i < MI_BCHUNK_FIELDS; i++) { + if (mi_bchunk_try_find_and_clear_at(chunk, i, pidx)) return true; + } + #endif + return false; +} + +bool mi_bchunk_try_find_and_clear_1(mi_bchunk_t* chunk, size_t n, size_t* pidx, bool* did_temp_clear_bits) { + mi_assert_internal(n==1); MI_UNUSED(n); MI_UNUSED(did_temp_clear_bits); + return mi_bchunk_try_find_and_clear(chunk, pidx); +} + +mi_decl_maybe_unused static inline bool mi_bchunk_try_find_and_clear8_at(mi_bchunk_t* chunk, size_t chunk_idx, size_t* pidx, bool* did_temp_clear_bits) { + _Atomic(mi_bfield_t)* const bfield = &chunk->bfields[chunk_idx]; + mi_bfield_t b = mi_atomic_load_relaxed(bfield); + if (b==0) return false; + int tries = 0; + do { + // has_set8 has low bit in each byte set if the byte in x == 0xFF + const mi_bfield_t has_set8 = + ((~b - MI_BFIELD_LO_BIT8) & // high bit set if byte in x is 0xFF or < 0x7F + (b & MI_BFIELD_HI_BIT8)) // high bit set if byte in x is >= 0x80 + >> 7; // shift high bit to low bit + size_t bitidx; + if (mi_bfield_find_least_bit(has_set8, &bitidx)) { // find least 1-bit + mi_assert_internal(bitidx <= (MI_BFIELD_BITS - 8)); + mi_assert_internal((bitidx%8)==0); + if mi_likely(mi_bfield_atomic_try_clear8_optimistic(bfield,bitidx,&b,did_temp_clear_bits)) { // try to clear atomically (and update b) + *pidx = (chunk_idx*MI_BFIELD_BITS) + bitidx; + mi_assert_internal(*pidx + 8 <= MI_BCHUNK_BITS); + return true; + } + } + else { + return false; + } + } while(b!=0 && ++tries <= 4); // limit tries to reduce possible contention + return false; +} + +// find least aligned byte in a chunk with all bits set, and try unset it atomically +// set `*pidx` to its bit index (0 <= *pidx < MI_BCHUNK_BITS) on success. +// Used to find medium size pages in the free blocks. +// todo: try neon version +static mi_decl_noinline bool mi_bchunk_try_find_and_clear8(mi_bchunk_t* chunk, size_t* pidx, bool* did_temp_clear_bits) { + #if MI_OPT_SIMD && defined(__AVX2__) && (MI_BCHUNK_BITS==512) + for(int tries=0; tries<4; tries++) { // paranoia: at most 4 tries + // since a cache-line is 64b, load all at once + const __m256i vec1 = _mm256_load_si256((const __m256i*)chunk->bfields); + const __m256i vec2 = _mm256_load_si256((const __m256i*)chunk->bfields+1); + const __m256i cmpv = mi_mm256_ones(); + const __m256i vcmp1 = _mm256_cmpeq_epi8(vec1, cmpv); // (byte == ~0 ? 0xFF : 0) + const __m256i vcmp2 = _mm256_cmpeq_epi8(vec2, cmpv); // (byte == ~0 ? 0xFF : 0) + const uint32_t mask1 = _mm256_movemask_epi8(vcmp1); // mask of most significant bit of each byte + const uint32_t mask2 = _mm256_movemask_epi8(vcmp2); // mask of most significant bit of each byte + const uint64_t mask = ((uint64_t)mask2 << 32) | mask1; + // mask is inverted, so each bit is 0xFF iff the corresponding byte has a bit set (and thus can be cleared) + if (mask==0) return false; + const size_t bidx = _tzcnt_u64(mask); // byte-idx of the byte in the chunk + const size_t chunk_idx = bidx / 8; + const size_t idx = (bidx % 8)*8; + mi_assert_internal(chunk_idx < MI_BCHUNK_FIELDS); + if mi_likely(mi_bfield_atomic_try_clear8_optimistic(&chunk->bfields[chunk_idx], idx, NULL, did_temp_clear_bits)) { // clear it atomically + *pidx = (chunk_idx*MI_BFIELD_BITS) + idx; + mi_assert_internal(*pidx + 8 <= MI_BCHUNK_BITS); + return true; + } + // try again + // note: there must be an atomic release/acquire in between or otherwise the registers may not be reloaded + // we add an explicit memory barrier as older gcc compilers do not reload the registers even with an atomic acquire (issue #1206) + #if defined(__GNUC__) + __asm __volatile ("" : : "g"(chunk) : "memory"); + #endif + } + #else + for (int i = 0; i < MI_BCHUNK_FIELDS; i++) { + if (mi_bchunk_try_find_and_clear8_at(chunk, i, pidx, did_temp_clear_bits)) return true; + } + #endif + return false; +} + +bool mi_bchunk_try_find_and_clear_8(mi_bchunk_t* chunk, size_t n, size_t* pidx, bool* did_temp_clear_bits) { + mi_assert_internal(n==8); MI_UNUSED(n); MI_UNUSED(did_temp_clear_bits); + return mi_bchunk_try_find_and_clear8(chunk, pidx, did_temp_clear_bits); +} + + +// find a sequence of `n` bits in a chunk with `0 < n <= MI_BFIELD_BITS` with all bits set, +// and try to clear them atomically. +// set `*pidx` to its bit index (0 <= *pidx <= MI_BCHUNK_BITS - n) on success. +// will cross bfield boundaries. +mi_decl_noinline bool mi_bchunk_try_find_and_clearNX(mi_bchunk_t* chunk, size_t n, size_t* pidx, bool* did_temp_clear_bits) { + if (n == 0 || n > MI_BFIELD_BITS) return false; + const mi_bfield_t mask = mi_bfield_mask(n, 0); + // for all fields in the chunk + for (int i = 0; i < MI_BCHUNK_FIELDS; i++) { + mi_bfield_t b0 = mi_atomic_load_relaxed(&chunk->bfields[i]); + mi_bfield_t b = b0; + size_t idx; + + // is there a range inside the field? + while (mi_bfield_find_least_bit(b, &idx)) { // find least 1-bit + if (idx + n > MI_BFIELD_BITS) break; // too short: maybe cross over, or continue with the next field + + const size_t bmask = mask<>idx == mask); + if ((b&bmask) == bmask) { // found a match with all bits set, try clearing atomically + if mi_likely(mi_bfield_atomic_try_clear_mask_optimistic(&chunk->bfields[i], bmask, &b0, did_temp_clear_bits)) { // updates b0 with the previous value + *pidx = (i*MI_BFIELD_BITS) + idx; + mi_assert_internal(*pidx < MI_BCHUNK_BITS); + mi_assert_internal(*pidx + n <= MI_BCHUNK_BITS); + return true; + } + else { + // if we failed to atomically commit, reload b and try again from the start + b = b0; // = mi_atomic_load_acquire(&chunk->bfields[i]); + } + } + else { + // advance by clearing the least run of ones, for example, with n>=4, idx=2: + // b = 1111 1101 1010 1100 + // .. + (1< 0) { + const size_t pre = mi_bfield_ctz(~mi_atomic_load_relaxed(&chunk->bfields[i+1])); + if (post + pre >= n) { + // it fits -- try to claim it atomically + const size_t cidx = (i*MI_BFIELD_BITS) + (MI_BFIELD_BITS - post); + if (mi_bchunk_try_clearNX(chunk, cidx, n, NULL, did_temp_clear_bits)) { + // we cleared all atomically + *pidx = cidx; + mi_assert_internal(*pidx < MI_BCHUNK_BITS); + mi_assert_internal(*pidx + n <= MI_BCHUNK_BITS); + return true; + } + } + } + } + } + return false; +} + +// find a sequence of `n` bits in a chunk with `n <= MI_BCHUNK_BITS` with all bits set, +// and try to clear them atomically. +// set `*pidx` to its bit index (0 <= *pidx <= MI_BCHUNK_BITS - n) on success. +// This can cross bfield boundaries. +mi_decl_noinline bool mi_bchunk_try_find_and_clearNC(mi_bchunk_t* chunk, size_t n, size_t* pidx, bool* did_temp_clear_bits) { + if (n == 0 || n > MI_BCHUNK_BITS) return false; // cannot be more than a chunk + + // we first scan ahead to see if there is a range of `n` set bits, and only then try to clear atomically + mi_assert_internal(n>0); + const size_t skip_count = (n-1)/MI_BFIELD_BITS; + size_t cidx; + for (size_t i = 0; i < MI_BCHUNK_FIELDS - skip_count; i++) + { + size_t m = n; // bits to go + + // first field + mi_bfield_t b = mi_atomic_load_relaxed(&chunk->bfields[i]); + size_t ones = mi_bfield_clz(~b); + + cidx = (i*MI_BFIELD_BITS) + (MI_BFIELD_BITS - ones); // start index + if (ones >= m) { + // we found enough bits already! + m = 0; + } + else if (ones > 0) { + // keep scanning further fields until we have enough bits + m -= ones; + size_t j = 1; // field count from i + while (i+j < MI_BCHUNK_FIELDS) { + mi_assert_internal(m > 0); + b = mi_atomic_load_relaxed(&chunk->bfields[i+j]); + ones = mi_bfield_ctz(~b); + if (ones >= m) { + // we found enough bits + m = 0; + break; + } + else if (ones == MI_BFIELD_BITS) { + // not enough yet, proceed to the next field + j++; + m -= MI_BFIELD_BITS; + } + else { + // the range was not enough, start from scratch + i = i + j - 1; // no need to re-scan previous fields, except the last one (with clz this time) + mi_assert_internal(m>0); + break; + } + } + } + + // did we find a range? + if (m==0) { + if (mi_bchunk_try_clearN(chunk, cidx, n, NULL, did_temp_clear_bits)) { + // we cleared all atomically + *pidx = cidx; + mi_assert_internal(*pidx < MI_BCHUNK_BITS); + mi_assert_internal(*pidx + n <= MI_BCHUNK_BITS); + return true; + } + // note: if we fail for a small `n` on the first field, we don't rescan that field (as `i` is incremented) + } + // otherwise continue searching + } + return false; +} + + + +// ------- mi_bchunk_clear_once_set --------------------------------------- + +void mi_bchunk_clear_once_set(mi_subproc_t* subproc, mi_bchunk_t* chunk, size_t cidx) { + mi_assert_internal(cidx < MI_BCHUNK_BITS); + const size_t i = cidx / MI_BFIELD_BITS; + const size_t idx = cidx % MI_BFIELD_BITS; + mi_bfield_atomic_clear_once_set(subproc, &chunk->bfields[i], idx); +} + + +// ------- mi_bitmap_all_are_clear --------------------------------------- + + +// are all bits in a bitmap chunk clear? +bool mi_bchunk_all_are_clear_relaxed(mi_bchunk_t* chunk) { + #if MI_OPT_SIMD && defined(__AVX2__) && (MI_BCHUNK_BITS==256) + const __m256i vec = _mm256_load_si256((const __m256i*)chunk->bfields); + return mi_mm256_is_zero(vec); + #elif MI_OPT_SIMD && defined(__AVX2__) && (MI_BCHUNK_BITS==512) + // a 64b cache-line contains the entire chunk anyway so load both at once + const __m256i vec1 = _mm256_load_si256((const __m256i*)chunk->bfields); + const __m256i vec2 = _mm256_load_si256(((const __m256i*)chunk->bfields)+1); + return (mi_mm256_is_zero(_mm256_or_si256(vec1,vec2))); + #elif MI_OPT_SIMD && (MI_BCHUNK_BITS==512) && MI_ARCH_ARM64 + const uint64x2_t v0 = vld1q_u64((uint64_t*)chunk->bfields); + const uint64x2_t v1 = vld1q_u64((uint64_t*)chunk->bfields + 2); + const uint64x2_t v2 = vld1q_u64((uint64_t*)chunk->bfields + 4); + const uint64x2_t v3 = vld1q_u64((uint64_t*)chunk->bfields + 6); + const uint64x2_t v = vorrq_u64(vorrq_u64(v0,v1),vorrq_u64(v2,v3)); + return (vmaxvq_u32(vreinterpretq_u32_u64(v)) == 0); + #else + for (int i = 0; i < MI_BCHUNK_FIELDS; i++) { + if (mi_atomic_load_relaxed(&chunk->bfields[i]) != 0) return false; + } + return true; + #endif +} + +// are all bits in a bitmap chunk set? +bool mi_bchunk_all_are_set_relaxed(mi_bchunk_t* chunk) { +#if MI_OPT_SIMD && defined(__AVX2__) && (MI_BCHUNK_BITS==256) + const __m256i vec = _mm256_load_si256((const __m256i*)chunk->bfields); + return mi_mm256_is_ones(vec); +#elif MI_OPT_SIMD && defined(__AVX2__) && (MI_BCHUNK_BITS==512) + // a 64b cache-line contains the entire chunk anyway so load both at once + const __m256i vec1 = _mm256_load_si256((const __m256i*)chunk->bfields); + const __m256i vec2 = _mm256_load_si256(((const __m256i*)chunk->bfields)+1); + return (mi_mm256_is_ones(_mm256_and_si256(vec1, vec2))); +#elif MI_OPT_SIMD && (MI_BCHUNK_BITS==512) && MI_ARCH_ARM64 + const uint64x2_t v0 = vld1q_u64((uint64_t*)chunk->bfields); + const uint64x2_t v1 = vld1q_u64((uint64_t*)chunk->bfields + 2); + const uint64x2_t v2 = vld1q_u64((uint64_t*)chunk->bfields + 4); + const uint64x2_t v3 = vld1q_u64((uint64_t*)chunk->bfields + 6); + const uint64x2_t v = vandq_u64(vandq_u64(v0,v1),vandq_u64(v2,v3)); + return (vminvq_u32(vreinterpretq_u32_u64(v)) == 0xFFFFFFFFUL); +#else + for (int i = 0; i < MI_BCHUNK_FIELDS; i++) { + if (~mi_atomic_load_relaxed(&chunk->bfields[i]) != 0) return false; + } + return true; +#endif +} + + +bool mi_bchunk_bsr(mi_bchunk_t* chunk, size_t* pidx) { + for (size_t i = MI_BCHUNK_FIELDS; i > 0; ) { + i--; + mi_bfield_t b = mi_atomic_load_relaxed(&chunk->bfields[i]); + size_t idx; + if (mi_bsr(b, &idx)) { + *pidx = (i*MI_BFIELD_BITS) + idx; + return true; + } + } + return false; +} + +bool mi_bchunk_bsr_inv(mi_bchunk_t* chunk, size_t* pidx) { + for (size_t i = MI_BCHUNK_FIELDS; i > 0; ) { + i--; + mi_bfield_t b = mi_atomic_load_relaxed(&chunk->bfields[i]); + size_t idx; + if (mi_bsr(~b, &idx)) { + *pidx = (i*MI_BFIELD_BITS) + idx; + return true; + } + } + return false; +} + +size_t mi_bchunk_popcount(mi_bchunk_t* chunk) { + size_t popcount = 0; + for (size_t i = 0; i < MI_BCHUNK_FIELDS; i++) { + const mi_bfield_t b = mi_atomic_load_relaxed(&chunk->bfields[i]); + popcount += mi_bfield_popcount(b); + } + return popcount; +} + + +// Set a sequence of `n` bits in the bitmap (and can cross chunks). Not atomic so only use if local to a thread. +void mi_bchunks_unsafe_setN(mi_bchunk_t* chunks, mi_bchunkmap_t* cmap, size_t idx, size_t n) { + mi_assert_internal(n>0); + + // start chunk and index + size_t chunk_idx = idx / MI_BCHUNK_BITS; + const size_t cidx = idx % MI_BCHUNK_BITS; + const size_t ccount = ((cidx + n - 1) / MI_BCHUNK_BITS) + 1; + + // first update the chunkmap + mi_bchunk_setN(cmap, chunk_idx, ccount, NULL); + + // first chunk + size_t m = MI_BCHUNK_BITS - cidx; + if (m > n) { m = n; } + mi_bchunk_setN(&chunks[chunk_idx], cidx, m, NULL); + + // n can be large so use memset for efficiency for all in-between chunks + chunk_idx++; + n -= m; + const size_t mid_chunks = n / MI_BCHUNK_BITS; + if (mid_chunks > 0) { + mi_memset(&chunks[chunk_idx], ~0, mid_chunks * MI_BCHUNK_SIZE); + chunk_idx += mid_chunks; + n -= (mid_chunks * MI_BCHUNK_BITS); + } + + // last chunk + if (n > 0) { + mi_assert_internal(n < MI_BCHUNK_BITS); + mi_bchunk_setN(&chunks[chunk_idx], 0, n, NULL); + } +} diff --git a/Objects/mimalloc/arena/bitmap-chunk.h b/Objects/mimalloc/arena/bitmap-chunk.h new file mode 100644 index 000000000000000..c5b3c4b206696d8 --- /dev/null +++ b/Objects/mimalloc/arena/bitmap-chunk.h @@ -0,0 +1,93 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2019-2026 Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +#pragma once +#ifndef MI_BITMAP_CHUNK_H +#define MI_BITMAP_CHUNK_H + +// Type definitions for bitmap chunks +#include "bitmap.h" + +// Exported functions for bitmap chunks +size_t mi_bfield_popcount(mi_bfield_t x); +mi_bfield_t mi_bfield_clear_least_bit(mi_bfield_t x); +bool mi_bfield_find_least_bit(mi_bfield_t x, size_t* idx); +bool mi_bfield_find_highest_bit(mi_bfield_t x, size_t* idx); +bool mi_bfield_foreach_bit(mi_bfield_t* x, size_t* idx); +mi_bfield_t mi_bfield_mask(size_t bit_count, size_t shiftl); + +bool mi_bchunk_set(mi_bchunk_t* chunk, size_t cidx, size_t* already_set); +bool mi_bchunk_setN(mi_bchunk_t* chunk, size_t cidx, size_t n, size_t* already_set); +bool mi_bchunk_clear(mi_bchunk_t* chunk, size_t cidx, bool* all_clear); +bool mi_bchunk_clearN(mi_bchunk_t* chunk, size_t cidx, size_t n, bool* maybe_all_clear); +size_t mi_bchunk_popcountN(mi_bchunk_t* chunk, size_t cidx, size_t n); +bool mi_bchunk_is_xsetN(mi_xset_t set, const mi_bchunk_t* chunk, size_t cidx, size_t n); + +bool mi_bchunk_try_clearN(mi_bchunk_t* chunk, size_t cidx, size_t n, bool* maybe_all_clear, bool* did_temp_clear_bits); +bool mi_bchunk_try_find_and_clear(mi_bchunk_t* chunk, size_t* pidx); +bool mi_bchunk_try_find_and_clear_1(mi_bchunk_t* chunk, size_t n, size_t* pidx, bool* did_temp_clear_bits); +bool mi_bchunk_try_find_and_clear_8(mi_bchunk_t* chunk, size_t n, size_t* pidx, bool* did_temp_clear_bits); +mi_decl_noinline bool mi_bchunk_try_find_and_clearNX(mi_bchunk_t* chunk, size_t n, size_t* pidx, bool* did_temp_clear_bits); +mi_decl_noinline bool mi_bchunk_try_find_and_clearNC(mi_bchunk_t* chunk, size_t n, size_t* pidx, bool* did_temp_clear_bits); + +void mi_bchunk_clear_once_set(mi_subproc_t* subproc, mi_bchunk_t* chunk, size_t cidx); +bool mi_bchunk_all_are_clear_relaxed(mi_bchunk_t* chunk); +bool mi_bchunk_all_are_set_relaxed(mi_bchunk_t* chunk); +bool mi_bchunk_bsr(mi_bchunk_t* chunk, size_t* pidx); +bool mi_bchunk_bsr_inv(mi_bchunk_t* chunk, size_t* pidx); +size_t mi_bchunk_popcount(mi_bchunk_t* chunk); +void mi_bchunks_unsafe_setN(mi_bchunk_t* chunks, mi_bchunkmap_t* cmap, size_t idx, size_t n); + +/* -------------------------------------------------------------------------------- + Iterate through a bfield +-------------------------------------------------------------------------------- */ + +// Cycle iteration through a bitfield. This is used to space out threads +// so there is less chance of contention. When searching for a free page we +// like to first search only the accessed part (so we reuse better). This +// high point is called the `cycle`. +// +// We then iterate through the bitfield as: +// first: [start, cycle> +// then : [0, start> +// then : [cycle, MI_BFIELD_BITS> +// +// The start is determined usually as `tseq % cycle` to have each thread +// start at a different spot. +// - We use `popcount` to improve branch prediction (maybe not needed? can we simplify?) +// - The `cycle_mask` is the part `[start, cycle>`. +#define mi_bfield_iterate(bfield,start,cycle,name_idx,SUF) { \ + mi_assert_internal(start <= cycle); \ + mi_assert_internal(start < MI_BFIELD_BITS); \ + mi_assert_internal(cycle <= MI_BFIELD_BITS); \ + const mi_bfield_t _cycle_mask##SUF = mi_bfield_mask(cycle - start, start); \ + size_t _bcount##SUF = mi_bfield_popcount(bfield); \ + mi_bfield_t _b##SUF = bfield & _cycle_mask##SUF; /* process [start, cycle> first*/\ + while(_bcount##SUF > 0) { \ + _bcount##SUF--;\ + if (_b##SUF==0) { _b##SUF = bfield & ~_cycle_mask##SUF; } /* process [0,start> + [cycle, MI_BFIELD_BITS> next */ \ + /* size_t name_idx; */ \ + const bool _found##SUF = mi_bfield_find_least_bit(_b##SUF,&name_idx); \ + _b##SUF = mi_bfield_clear_least_bit(_b##SUF); /* clear early so `continue` works */ \ + mi_assert_internal(_found##SUF); MI_UNUSED(_found##SUF); \ + { \ + +#define mi_bfield_iterate_end(SUF) \ + } \ + } \ +} + + +#define mi_bfield_cycle_iterate(bfield,tseq,cycle,name_idx,SUF) { \ + const size_t _start##SUF = (uint32_t)(tseq) % (uint32_t)(cycle); /* or: 0 to always search from the start? */\ + mi_bfield_iterate(bfield,_start##SUF,cycle,name_idx,SUF) + +#define mi_bfield_cycle_iterate_end(SUF) \ + mi_bfield_iterate_end(SUF); \ +} + +#endif diff --git a/Objects/mimalloc/arena/bitmap.c b/Objects/mimalloc/arena/bitmap.c new file mode 100644 index 000000000000000..d1c9228df008492 --- /dev/null +++ b/Objects/mimalloc/arena/bitmap.c @@ -0,0 +1,918 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2019-2026 Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +/* ---------------------------------------------------------------------------- +Concurrent bitmap that can set/reset sequences of bits atomically +---------------------------------------------------------------------------- */ + +#include "mimalloc.h" +#include "mimalloc/util/libc.h" +#include "mimalloc/util/stats.h" +#include "bitmap-chunk.h" +#include "bitmap.h" + +/* -------------------------------------------------------------------------------- + bitmap chunkmap +-------------------------------------------------------------------------------- */ + +static void mi_bitmap_chunkmap_set(mi_bitmap_t* bitmap, size_t chunk_idx) { + mi_assert(chunk_idx < mi_bitmap_chunk_count(bitmap)); + mi_bchunk_set(&bitmap->chunkmap, chunk_idx, NULL); +} + +static bool mi_bitmap_chunkmap_try_clear(mi_bitmap_t* bitmap, size_t chunk_idx) { + mi_assert(chunk_idx < mi_bitmap_chunk_count(bitmap)); + // check if the corresponding chunk is all clear + if (!mi_bchunk_all_are_clear_relaxed(&bitmap->chunks[chunk_idx])) return false; + // clear the chunkmap bit + mi_bchunk_clear(&bitmap->chunkmap, chunk_idx, NULL); + // .. but a concurrent set may have happened in between our all-clear test and the clearing of the + // bit in the mask. We check again to catch this situation. + if (!mi_bchunk_all_are_clear_relaxed(&bitmap->chunks[chunk_idx])) { + mi_bchunk_set(&bitmap->chunkmap, chunk_idx, NULL); + return false; + } + return true; +} + + +/* -------------------------------------------------------------------------------- + bitmap +-------------------------------------------------------------------------------- */ + +size_t mi_bitmap_size(size_t bit_count, size_t* pchunk_count) { + mi_assert_internal((bit_count % MI_BCHUNK_BITS) == 0); + bit_count = mi_align_up(bit_count, MI_BCHUNK_BITS); + mi_assert_internal(bit_count <= MI_BITMAP_MAX_BIT_COUNT); + mi_assert_internal(bit_count > 0); + const size_t chunk_count = bit_count / MI_BCHUNK_BITS; + mi_assert_internal(chunk_count >= 1); + const size_t size = offsetof(mi_bitmap_t,chunks) + (chunk_count * MI_BCHUNK_SIZE); + mi_assert_internal( (size%MI_BCHUNK_SIZE) == 0 ); + if (pchunk_count != NULL) { *pchunk_count = chunk_count; } + return size; +} + + +// initialize a bitmap to all unset; avoid a mem_zero if `already_zero` is true +// returns the size of the bitmap +size_t mi_bitmap_init(mi_bitmap_t* bitmap, size_t bit_count, bool already_zero) { + size_t chunk_count; + const size_t size = mi_bitmap_size(bit_count, &chunk_count); + if (!already_zero) { + mi_memzero_aligned(bitmap, size); + } + mi_atomic_store_release(&bitmap->chunk_count, chunk_count); + mi_assert_internal(mi_atomic_load_relaxed(&bitmap->chunk_count) <= MI_BITMAP_MAX_CHUNK_COUNT); + return size; +} + + +// Set a sequence of `n` bits in the bitmap (and can cross chunks). Not atomic so only use if local to a thread. +void mi_bitmap_unsafe_setN(mi_bitmap_t* bitmap, size_t idx, size_t n) { + mi_assert_internal(n>0); + mi_assert_internal(idx + n <= mi_bitmap_max_bits(bitmap)); + mi_bchunks_unsafe_setN(&bitmap->chunks[0], &bitmap->chunkmap, idx, n); +} + + + + +// ------- mi_bitmap_xset --------------------------------------- + +// Set a sequence of `n` bits in the bitmap; returns `true` if atomically transitioned from 0's to 1's (or 1's to 0's). +bool mi_bitmap_setN(mi_bitmap_t* bitmap, size_t idx, size_t n, size_t* palready_set) { + mi_assert_internal(n>0); + const size_t maxbits = mi_bitmap_max_bits(bitmap); + mi_assert_internal(idx + n <= maxbits); + if (idx+n > maxbits) { // paranoia + if (idx >= maxbits) return false; + n = maxbits - idx; + } + + // iterate through the chunks + size_t chunk_idx = idx / MI_BCHUNK_BITS; + size_t cidx = idx % MI_BCHUNK_BITS; + bool were_allclear = true; + size_t already_set = 0; + while (n > 0) { + const size_t m = (cidx + n > MI_BCHUNK_BITS ? MI_BCHUNK_BITS - cidx : n); + size_t _already_set = 0; + were_allclear = mi_bchunk_setN(&bitmap->chunks[chunk_idx], cidx, m, &_already_set) && were_allclear; + already_set += _already_set; + mi_bitmap_chunkmap_set(bitmap, chunk_idx); // set afterwards + mi_assert_internal(m <= n); + n -= m; + cidx = 0; + chunk_idx++; + } + if (palready_set != NULL) { *palready_set = already_set; } + return were_allclear; +} + +// Clear a sequence of `n` bits in the bitmap; returns `true` if atomically transitioned from 1's to 0's. +bool mi_bitmap_clearN(mi_bitmap_t* bitmap, size_t idx, size_t n) { + mi_assert_internal(n>0); + const size_t maxbits = mi_bitmap_max_bits(bitmap); + mi_assert_internal(idx + n <= maxbits); + if (idx+n > maxbits) { // paranoia + if (idx >= maxbits) return false; + n = maxbits - idx; + } + + // iterate through the chunks + size_t chunk_idx = idx / MI_BCHUNK_BITS; + size_t cidx = idx % MI_BCHUNK_BITS; + bool were_allset = true; + while (n > 0) { + const size_t m = (cidx + n > MI_BCHUNK_BITS ? MI_BCHUNK_BITS - cidx : n); + bool maybe_all_clear = false; + were_allset = mi_bchunk_clearN(&bitmap->chunks[chunk_idx], cidx, m, &maybe_all_clear) && were_allset; + if (maybe_all_clear) { mi_bitmap_chunkmap_try_clear(bitmap, chunk_idx); } + mi_assert_internal(m <= n); + n -= m; + cidx = 0; + chunk_idx++; + } + return were_allset; +} + +// Count bits set in a range of `n` bits. +size_t mi_bitmap_popcountN( mi_bitmap_t* bitmap, size_t idx, size_t n) { + mi_assert_internal(n>0); + const size_t maxbits = mi_bitmap_max_bits(bitmap); + mi_assert_internal(idx + n <= maxbits); + if (idx+n > maxbits) { // paranoia + if (idx >= maxbits) return 0; + n = maxbits - idx; + } + + // iterate through the chunks + size_t chunk_idx = idx / MI_BCHUNK_BITS; + size_t cidx = idx % MI_BCHUNK_BITS; + size_t popcount = 0; + while (n > 0) { + const size_t m = (cidx + n > MI_BCHUNK_BITS ? MI_BCHUNK_BITS - cidx : n); + popcount += mi_bchunk_popcountN(&bitmap->chunks[chunk_idx], cidx, m); + mi_assert_internal(m <= n); + n -= m; + cidx = 0; + chunk_idx++; + } + return popcount; +} + + +// Set/clear a bit in the bitmap; returns `true` if atomically transitioned from 0 to 1 (or 1 to 0) +bool mi_bitmap_set(mi_bitmap_t* bitmap, size_t idx) { + return mi_bitmap_setN(bitmap, idx, 1, NULL); +} + +bool mi_bitmap_clear(mi_bitmap_t* bitmap, size_t idx) { + return mi_bitmap_clearN(bitmap, idx, 1); +} + + + +// ------- mi_bitmap_is_xset --------------------------------------- + +// Is a sequence of n bits already all set/cleared? +bool mi_bitmap_is_xsetN(mi_xset_t set, mi_bitmap_t* bitmap, size_t idx, size_t n) { + mi_assert_internal(n>0); + const size_t maxbits = mi_bitmap_max_bits(bitmap); + mi_assert_internal(idx + n <= maxbits); + if (idx+n > maxbits) { // paranoia + if (idx >= maxbits) return false; + n = maxbits - idx; + } + + // iterate through the chunks + size_t chunk_idx = idx / MI_BCHUNK_BITS; + size_t cidx = idx % MI_BCHUNK_BITS; + bool xset = true; + while (n > 0 && xset) { + const size_t m = (cidx + n > MI_BCHUNK_BITS ? MI_BCHUNK_BITS - cidx : n); + xset = mi_bchunk_is_xsetN(set, &bitmap->chunks[chunk_idx], cidx, m) && xset; + mi_assert_internal(m <= n); + n -= m; + cidx = 0; + chunk_idx++; + } + return xset; +} + +bool mi_bitmap_is_all_clear(mi_bitmap_t* bitmap) { + return mi_bitmap_is_xsetN(MI_BIT_CLEAR, bitmap, 0, mi_bitmap_max_bits(bitmap)); +} + +/* -------------------------------------------------------------------------------- + mi_bitmap_find + (used to find free pages) +-------------------------------------------------------------------------------- */ + +typedef bool (mi_bitmap_visit_fun_t)(mi_bitmap_t* bitmap, size_t chunk_idx, size_t n, size_t* idx, void* arg1, void* arg2); + +// Go through the bitmap and for every sequence of `n` set bits, call the visitor function. +// If it returns `true` stop the search. +static inline bool mi_bitmap_find(mi_bitmap_t* bitmap, size_t tseq, size_t n, size_t* pidx, mi_bitmap_visit_fun_t* on_find, void* arg1, void* arg2) +{ + const size_t chunkmap_max = mi_divide_up(mi_bitmap_chunk_count(bitmap), MI_BFIELD_BITS); + for (size_t i = 0; i < chunkmap_max; i++) { + // and for each chunkmap entry we iterate over its bits to find the chunks + const mi_bfield_t cmap_entry = mi_atomic_load_relaxed(&bitmap->chunkmap.bfields[i]); + size_t hi; + if (mi_bfield_find_highest_bit(cmap_entry, &hi)) { + size_t eidx = 0; + mi_bfield_cycle_iterate(cmap_entry, tseq%8, hi+1, eidx, Y) // reduce the tseq to 8 bins to reduce using extra memory (see `mstress`) + { + mi_assert_internal(eidx <= MI_BFIELD_BITS); + const size_t chunk_idx = i*MI_BFIELD_BITS + eidx; + mi_assert_internal(chunk_idx < mi_bitmap_chunk_count(bitmap)); + if ((*on_find)(bitmap, chunk_idx, n, pidx, arg1, arg2)) { + return true; + } + } + mi_bfield_cycle_iterate_end(Y); + } + } + return false; +} + + +/* -------------------------------------------------------------------------------- + Bitmap: try_find_and_claim -- used to allocate abandoned pages + note: the compiler will fully inline the indirect function call +-------------------------------------------------------------------------------- */ + +typedef struct mi_claim_fun_data_s { + mi_arena_t* arena; +} mi_claim_fun_data_t; + +static bool mi_bitmap_try_find_and_claim_visit(mi_bitmap_t* bitmap, size_t chunk_idx, size_t n, size_t* pidx, void* arg1, void* arg2) +{ + mi_assert_internal(n==1); MI_UNUSED(n); + mi_claim_fun_t* claim_fun = (mi_claim_fun_t*)arg1; + mi_claim_fun_data_t* claim_data = (mi_claim_fun_data_t*)arg2; + size_t cidx; + if mi_likely(mi_bchunk_try_find_and_clear(&bitmap->chunks[chunk_idx], &cidx)) { + const size_t slice_index = (chunk_idx * MI_BCHUNK_BITS) + cidx; + mi_assert_internal(slice_index < mi_bitmap_max_bits(bitmap)); + bool keep_set = true; + if ((*claim_fun)(slice_index, claim_data->arena, &keep_set)) { + // success! + mi_assert_internal(!keep_set); + *pidx = slice_index; + return true; + } + else { + // failed to claim it, set abandoned mapping again (unless the page was freed and keep_set will be false) + if (keep_set) { + const bool wasclear = mi_bchunk_set(&bitmap->chunks[chunk_idx], cidx, NULL); + mi_bitmap_chunkmap_set(bitmap, chunk_idx); + mi_assert_internal(wasclear); MI_UNUSED(wasclear); + } + } + } + else { + // we may find that all are cleared only on a second iteration but that is ok as + // the chunkmap is a conservative approximation. + mi_bitmap_chunkmap_try_clear(bitmap, chunk_idx); + } + return false; +} + +// Find a set bit in the bitmap and try to atomically clear it and claim it. +// (Used to find pages in the pages_abandoned bitmaps.) +mi_decl_nodiscard bool mi_bitmap_try_find_and_claim(mi_bitmap_t* bitmap, size_t tseq, size_t* pidx, + mi_claim_fun_t* claim, mi_arena_t* arena ) +{ + mi_claim_fun_data_t claim_data = { arena }; + return mi_bitmap_find(bitmap, tseq, 1, pidx, &mi_bitmap_try_find_and_claim_visit, (void*)claim, &claim_data); +} + + +bool mi_bitmap_bsr(mi_bitmap_t* bitmap, size_t* idx) { + const size_t chunkmap_max = mi_divide_up(mi_bitmap_chunk_count(bitmap), MI_BFIELD_BITS); + for (size_t i = chunkmap_max; i > 0; ) { + i--; + mi_bfield_t cmap = mi_atomic_load_relaxed(&bitmap->chunkmap.bfields[i]); + size_t cmap_idx; + if (mi_bsr(cmap,&cmap_idx)) { + // from highest chunk to lowest (scan all in case the cmap entry was stale) + for (size_t j = cmap_idx+1; j>0; ) { + j--; + const size_t chunk_idx = (i*MI_BFIELD_BITS) + j; + size_t cidx; + if (mi_bchunk_bsr(&bitmap->chunks[chunk_idx], &cidx)) { + *idx = (chunk_idx * MI_BCHUNK_BITS) + cidx; + return true; + } + } + } + } + return false; +} + +// Return count of all set bits in a bitmap. +size_t mi_bitmap_popcount(mi_bitmap_t* bitmap) { + // for all chunkmap entries + size_t popcount = 0; + const size_t chunkmap_max = mi_divide_up(mi_bitmap_chunk_count(bitmap), MI_BFIELD_BITS); + for (size_t i = 0; i < chunkmap_max; i++) { + mi_bfield_t cmap_entry = mi_atomic_load_relaxed(&bitmap->chunkmap.bfields[i]); + size_t cmap_idx; + // for each chunk (corresponding to a set bit in a chunkmap entry) + while (mi_bfield_foreach_bit(&cmap_entry, &cmap_idx)) { + const size_t chunk_idx = i*MI_BFIELD_BITS + cmap_idx; + // count bits in a chunk + popcount += mi_bchunk_popcount(&bitmap->chunks[chunk_idx]); + } + } + return popcount; +} + + + +// Clear a bit once it is set. +void mi_bitmap_clear_once_set(mi_subproc_t* subproc, mi_bitmap_t* bitmap, size_t idx) { + mi_assert_internal(idx < mi_bitmap_max_bits(bitmap)); + const size_t chunk_idx = idx / MI_BCHUNK_BITS; + const size_t cidx = idx % MI_BCHUNK_BITS; + mi_assert_internal(chunk_idx < mi_bitmap_chunk_count(bitmap)); + mi_bchunk_clear_once_set(subproc, &bitmap->chunks[chunk_idx], cidx); +} + + +// Visit all set bits in a bitmap. +// todo: optimize further? maybe use avx512 to directly get all indices using a mask_compressstore? +bool _mi_bitmap_forall_set(mi_bitmap_t* bitmap, mi_forall_set_fun_t* visit, mi_arena_t* arena, void* arg) { + // for all chunkmap entries + const size_t chunkmap_max = mi_divide_up(mi_bitmap_chunk_count(bitmap), MI_BFIELD_BITS); + for(size_t i = 0; i < chunkmap_max; i++) { + mi_bfield_t cmap_entry = mi_atomic_load_relaxed(&bitmap->chunkmap.bfields[i]); + size_t cmap_idx; + // for each chunk (corresponding to a set bit in a chunkmap entry) + while (mi_bfield_foreach_bit(&cmap_entry, &cmap_idx)) { + const size_t chunk_idx = i*MI_BFIELD_BITS + cmap_idx; + // for each chunk field + mi_bchunk_t* const chunk = &bitmap->chunks[chunk_idx]; + for (size_t j = 0; j < MI_BCHUNK_FIELDS; j++) { + const size_t base_idx = (chunk_idx*MI_BCHUNK_BITS) + (j*MI_BFIELD_BITS); + mi_bfield_t b = mi_atomic_load_relaxed(&chunk->bfields[j]); + size_t bidx; + while (mi_bfield_foreach_bit(&b, &bidx)) { + const size_t idx = base_idx + bidx; + if (!visit(idx, 1, arena, arg)) return false; + } + } + } + } + return true; +} + +// Visit all set bits in a bitmap but try to return ranges (within bfields) if possible. +// Also clear those ranges atomically. +// Used by purging to purge larger ranges when possible +// todo: optimize further? maybe use avx512 to directly get all indices using a mask_compressstore? +bool _mi_bitmap_forall_setc_ranges(mi_bitmap_t* bitmap, mi_forall_set_fun_t* visit, mi_arena_t* arena, void* arg) { + // for all chunkmap entries + const size_t chunkmap_max = mi_divide_up(mi_bitmap_chunk_count(bitmap), MI_BFIELD_BITS); + for (size_t i = 0; i < chunkmap_max; i++) { + mi_bfield_t cmap_entry = mi_atomic_load_relaxed(&bitmap->chunkmap.bfields[i]); + size_t cmap_idx; + // for each chunk (corresponding to a set bit in a chunkmap entry) + while (mi_bfield_foreach_bit(&cmap_entry, &cmap_idx)) { + const size_t chunk_idx = i*MI_BFIELD_BITS + cmap_idx; + // for each chunk field + mi_bchunk_t* const chunk = &bitmap->chunks[chunk_idx]; + for (size_t j = 0; j < MI_BCHUNK_FIELDS; j++) { + const size_t base_idx = (chunk_idx*MI_BCHUNK_BITS) + (j*MI_BFIELD_BITS); + mi_bfield_t b = mi_atomic_exchange_relaxed(&chunk->bfields[j], (mi_bfield_t)0); + #if MI_DEBUG > 1 + const size_t bpopcount = mi_popcount(b); + size_t rngcount = 0; + #endif + size_t bidx; + while (mi_bfield_find_least_bit(b, &bidx)) { + size_t rng = mi_ctz(~(b>>bidx)); // all the set bits from bidx + #if MI_DEBUG > 1 + rngcount += rng; + #endif + const size_t idx = base_idx + bidx; + mi_assert_internal(rng>=1 && rng<=MI_BFIELD_BITS); + mi_assert_internal((idx % MI_BFIELD_BITS) + rng <= MI_BFIELD_BITS); + mi_assert_internal((idx / MI_BCHUNK_BITS) < mi_bitmap_chunk_count(bitmap)); + // clear rng bits in b + b = b & ~mi_bfield_mask(rng, bidx); + if (!visit(idx, rng, arena, arg)) { + // break early: reset the non-visited bits + if (b!=0) { + mi_atomic_or_relaxed(&chunk->bfields[j], b); + } + return false; + } + } + mi_assert_internal(rngcount == bpopcount); + } + } + } + return true; +} + +// Visit all set bits in a bitmap but try to return ranges (within bfields) if possible, +// but only in chunks of at least `rngslices` slices (that are also aligned at `rngslices`) +// and clear those ranges atomically. +// However, the `rngslices` are capped at `MI_BFIELD_BITS` at most. +// Used by purging to purge larger ranges when possible. With transparent huge pages we only +// want to purge whole huge pages (2 MiB) at a time which is what the `rngslices` parameter achieves. +bool _mi_bitmap_forall_setc_rangesn(mi_bitmap_t* bitmap, size_t rngslices, mi_forall_set_fun_t* visit, mi_arena_t* arena, void* arg) +{ + // use the generic routine for `rngslices<=1` (as that one finds longest ranges at a time) + if (rngslices<=1) { + return _mi_bitmap_forall_setc_ranges(bitmap, visit, arena, arg); + } + // mi_assert_internal(rngslices <= MI_BFIELD_BITS); + if (rngslices > MI_BFIELD_BITS) { rngslices = MI_BFIELD_BITS; } // cap at MI_BFIELD_BITS at most + + // for all chunkmap entries + const size_t chunkmap_max = mi_divide_up(mi_bitmap_chunk_count(bitmap), MI_BFIELD_BITS); + for (size_t i = 0; i < chunkmap_max; i++) { + mi_bfield_t cmap_entry = mi_atomic_load_relaxed(&bitmap->chunkmap.bfields[i]); + size_t cmap_idx; + // for each chunk (corresponding to a set bit in a chunkmap entry) + while (mi_bfield_foreach_bit(&cmap_entry, &cmap_idx)) { + const size_t chunk_idx = i*MI_BFIELD_BITS + cmap_idx; + // for each chunk field + mi_bchunk_t* const chunk = &bitmap->chunks[chunk_idx]; + for (size_t j = 0; j < MI_BCHUNK_FIELDS; j++) { + const size_t base_idx = (chunk_idx*MI_BCHUNK_BITS) + (j*MI_BFIELD_BITS); + mi_bfield_t b = mi_atomic_exchange_relaxed(&chunk->bfields[j], (mi_bfield_t)0); // atomic clear + mi_bfield_t skipped = 0; // but track which bits we skip so we can restore them + size_t shift; + for(shift = 0; rngslices + shift <= MI_BFIELD_BITS; shift += rngslices) { // per `rngslices` to keep alignment + const mi_bfield_t rngmask = mi_bfield_mask(rngslices, shift); + if ((b & rngmask) == rngmask) { + const size_t idx = base_idx + shift; + if (!visit(idx, rngslices, arena, arg)) { + // break early: restore non-visited entries + mi_bfield_t notyet_visited = 0; + if (rngslices + shift < MI_BFIELD_BITS) { + notyet_visited = (b & (~(mi_bfield_t)0 << (shift + rngslices))); + } + mi_assert_internal((notyet_visited & skipped) == 0); + if ((notyet_visited | skipped) != 0) { + mi_atomic_or_relaxed(&chunk->bfields[j], notyet_visited | skipped); + } + return false; + } + } + else { + skipped = skipped | (b & rngmask); + } + } + if (shift < MI_BFIELD_BITS) { + // there are some non-visited top bits when `MI_BFIELD_BITS % rngslices != 0`. + mi_assert_internal(MI_BFIELD_BITS % rngslices != 0); + skipped = skipped | (b & (~(mi_bfield_t)0 << shift)); + } + if (skipped != 0) { + // restore non-visited entries + mi_atomic_or_relaxed(&chunk->bfields[j], skipped); + } + } + } + } + return true; +} + + +/* -------------------------------------------------------------------------------- + binned bitmap's +-------------------------------------------------------------------------------- */ + + +size_t mi_bbitmap_size(size_t bit_count, size_t* pchunk_count) { + // mi_assert_internal((bit_count % MI_BCHUNK_BITS) == 0); + bit_count = mi_align_up(bit_count, MI_BCHUNK_BITS); + mi_assert_internal(bit_count <= MI_BITMAP_MAX_BIT_COUNT); + mi_assert_internal(bit_count > 0); + const size_t chunk_count = bit_count / MI_BCHUNK_BITS; + mi_assert_internal(chunk_count >= 1); + const size_t size = offsetof(mi_bbitmap_t,chunks) + (chunk_count * MI_BCHUNK_SIZE); + mi_assert_internal( (size%MI_BCHUNK_SIZE) == 0 ); + if (pchunk_count != NULL) { *pchunk_count = chunk_count; } + return size; +} + +// initialize a bitmap to all unset; avoid a mem_zero if `already_zero` is true +// returns the size of the bitmap +size_t mi_bbitmap_init(mi_subproc_t* subproc, mi_bbitmap_t* bbitmap, size_t bit_count, bool already_zero) { + size_t chunk_count; + const size_t size = mi_bbitmap_size(bit_count, &chunk_count); + if (!already_zero) { + mi_memzero_aligned(bbitmap, size); + } + mi_atomic_store_release(&bbitmap->chunk_count, chunk_count); + mi_assert_internal(mi_atomic_load_relaxed(&bbitmap->chunk_count) <= MI_BITMAP_MAX_CHUNK_COUNT); + bbitmap->subproc = subproc; + return size; +} + +void mi_bbitmap_unsafe_setN(mi_bbitmap_t* bbitmap, size_t idx, size_t n) { + mi_assert_internal(n>0); + mi_assert_internal(idx + n <= mi_bbitmap_max_bits(bbitmap)); + mi_bchunks_unsafe_setN(&bbitmap->chunks[0], &bbitmap->chunkmap, idx, n); +} + +bool mi_bbitmap_bsr_inv(mi_bbitmap_t* bbitmap, size_t* idx) { + // scan for highest zero bit in the bitmap + // note: we cannot use the chunkmap since that only conservatively denotes if there might be a set bit in a chuck + // todo: bbitmap_init rounds up the bitcount to BCHUNK_BITS and we should skip the top-padding! + const size_t chunk_count = mi_bbitmap_chunk_count(bbitmap); + for(size_t i = chunk_count; i > 0; ) { + i--; + size_t cidx; + if (mi_bchunk_bsr_inv(&bbitmap->chunks[i], &cidx)) { + *idx = (i * MI_BCHUNK_BITS) + cidx; + return true; + } + } + return false; +} + + +/* -------------------------------------------------------------------------------- + binned bitmap used to track free slices +-------------------------------------------------------------------------------- */ + +// Assign a specific size bin to a chunk +static void mi_bbitmap_set_chunk_bin(mi_bbitmap_t* bbitmap, size_t chunk_idx, mi_chunkbin_t bin) { + mi_assert_internal(chunk_idx < mi_bbitmap_chunk_count(bbitmap)); + for (mi_chunkbin_t ibin = MI_CBIN_SMALL; ibin < MI_CBIN_NONE; ibin = mi_chunkbin_inc(ibin)) { + if (ibin == bin) { + const bool was_clear = mi_bchunk_set(& bbitmap->chunkmap_bins[ibin], chunk_idx, NULL); + if (was_clear) { mi_subproc_stat_increase(bbitmap->subproc, chunk_bins[ibin],1); } + } + else { + const bool was_set = mi_bchunk_clear(&bbitmap->chunkmap_bins[ibin], chunk_idx, NULL); + if (was_set) { mi_subproc_stat_decrease(bbitmap->subproc,chunk_bins[ibin],1); } + } + } +} + +mi_chunkbin_t mi_bbitmap_debug_get_bin(const mi_bchunkmap_t* chunkmap_bins, size_t chunk_idx) { + for (mi_chunkbin_t ibin = MI_CBIN_SMALL; ibin < MI_CBIN_NONE; ibin = mi_chunkbin_inc(ibin)) { + if (mi_bchunk_is_xsetN(MI_BIT_SET, &chunkmap_bins[ibin], chunk_idx, 1)) { + return ibin; + } + } + return MI_CBIN_NONE; +} + +// Track the index of the highest chunk that is accessed. +static void mi_bbitmap_chunkmap_set_max(mi_bbitmap_t* bbitmap, size_t chunk_idx) { + size_t oldmax = mi_atomic_load_relaxed(&bbitmap->chunk_max_accessed); + if mi_unlikely(chunk_idx > oldmax) { + mi_atomic_cas_strong_relaxed(&bbitmap->chunk_max_accessed, &oldmax, chunk_idx); + } +} + +// Set a bit in the chunkmap +static void mi_bbitmap_chunkmap_set(mi_bbitmap_t* bbitmap, size_t chunk_idx, bool check_all_set) { + mi_assert(chunk_idx < mi_bbitmap_chunk_count(bbitmap)); + if (check_all_set) { + if (mi_bchunk_all_are_set_relaxed(&bbitmap->chunks[chunk_idx])) { + // all slices are free in this chunk: return back to the NONE bin + mi_bbitmap_set_chunk_bin(bbitmap, chunk_idx, MI_CBIN_NONE); + } + } + mi_bchunk_set(&bbitmap->chunkmap, chunk_idx, NULL); + mi_bbitmap_chunkmap_set_max(bbitmap, chunk_idx); +} + +static bool mi_bbitmap_chunkmap_try_clear(mi_bbitmap_t* bbitmap, size_t chunk_idx) { + mi_assert(chunk_idx < mi_bbitmap_chunk_count(bbitmap)); + // check if the corresponding chunk is all clear + if (!mi_bchunk_all_are_clear_relaxed(&bbitmap->chunks[chunk_idx])) return false; + // clear the chunkmap bit + mi_bchunk_clear(&bbitmap->chunkmap, chunk_idx, NULL); + // .. but a concurrent set may have happened in between our all-clear test and the clearing of the + // bit in the mask. We check again to catch this situation. (note: mi_bchunk_clear must be acq-rel) + if (!mi_bchunk_all_are_clear_relaxed(&bbitmap->chunks[chunk_idx])) { + mi_bchunk_set(&bbitmap->chunkmap, chunk_idx, NULL); + return false; + } + mi_bbitmap_chunkmap_set_max(bbitmap, chunk_idx); + return true; +} + + +/* -------------------------------------------------------------------------------- + mi_bbitmap_setN, try_clearN, and is_xsetN + (used to find free pages) +-------------------------------------------------------------------------------- */ + +// Set a sequence of `n` bits in the bitmap; returns `true` if atomically transitioned from 0's to 1's (or 1's to 0's). +bool mi_bbitmap_setN(mi_bbitmap_t* bbitmap, size_t idx, size_t n) { + mi_assert_internal(n>0); + const size_t maxbits = mi_bbitmap_max_bits(bbitmap); + mi_assert_internal(idx + n <= maxbits); + if (idx+n > maxbits) { // paranoia + if (idx >= maxbits) return false; + n = maxbits - idx; + } + + // iterate through the chunks + size_t chunk_idx = idx / MI_BCHUNK_BITS; + size_t cidx = idx % MI_BCHUNK_BITS; + bool were_allclear = true; + while (n > 0) { + const size_t m = (cidx + n > MI_BCHUNK_BITS ? MI_BCHUNK_BITS - cidx : n); + were_allclear = mi_bchunk_setN(&bbitmap->chunks[chunk_idx], cidx, m, NULL) && were_allclear; + mi_bbitmap_chunkmap_set(bbitmap, chunk_idx, true); // set afterwards + mi_assert_internal(m <= n); + n -= m; + cidx = 0; + chunk_idx++; + } + return were_allclear; +} + +// ------- mi_bbitmap_try_clearNC --------------------------------------- + +// Try to clear `n` bits at `idx` where `n <= MI_BCHUNK_BITS`. +bool mi_bbitmap_try_clearNC(mi_bbitmap_t* bbitmap, size_t idx, size_t n) { + mi_assert_internal(n>0); + mi_assert_internal(n<=MI_BCHUNK_BITS); + mi_assert_internal(idx + n <= mi_bbitmap_max_bits(bbitmap)); + + const size_t chunk_idx = idx / MI_BCHUNK_BITS; + const size_t cidx = idx % MI_BCHUNK_BITS; + mi_assert_internal(cidx + n <= MI_BCHUNK_BITS); // don't cross chunks (for now) + mi_assert_internal(chunk_idx < mi_bbitmap_chunk_count(bbitmap)); + if (cidx + n > MI_BCHUNK_BITS) return false; + bool maybe_all_clear = false; + bool did_temp_clear_bits = false; + const bool cleared = mi_bchunk_try_clearN(&bbitmap->chunks[chunk_idx], cidx, n, &maybe_all_clear, &did_temp_clear_bits); + if (cleared && maybe_all_clear) { + mi_assert_internal(!did_temp_clear_bits); + mi_bbitmap_chunkmap_try_clear(bbitmap, chunk_idx); + } else if (did_temp_clear_bits) { + // may have raced with a clearer (in on_find) so set the chunkmap bit conservatively + mi_bbitmap_chunkmap_set(bbitmap, chunk_idx, false); + } + // note: we don't set the size class for an explicit try_clearN (only used by purging) + return cleared; +} + + + +// ------- mi_bbitmap_is_xset --------------------------------------- + +// Is a sequence of n bits already all set/cleared? +bool mi_bbitmap_is_xsetN(mi_xset_t set, mi_bbitmap_t* bbitmap, size_t idx, size_t n) { + mi_assert_internal(n>0); + const size_t maxbits = mi_bbitmap_max_bits(bbitmap); + mi_assert_internal(idx + n <= maxbits); + if (idx+n > maxbits) { // paranoia + if (idx >= maxbits) return false; + n = maxbits - idx; + } + + // iterate through the chunks + size_t chunk_idx = idx / MI_BCHUNK_BITS; + size_t cidx = idx % MI_BCHUNK_BITS; + bool xset = true; + while (n > 0 && xset) { + const size_t m = (cidx + n > MI_BCHUNK_BITS ? MI_BCHUNK_BITS - cidx : n); + xset = mi_bchunk_is_xsetN(set, &bbitmap->chunks[chunk_idx], cidx, m) && xset; + mi_assert_internal(m <= n); + n -= m; + cidx = 0; + chunk_idx++; + } + return xset; +} + + + + +/* -------------------------------------------------------------------------------- + mi_bbitmap_find + (used to find free pages) +-------------------------------------------------------------------------------- */ + +typedef bool (mi_bchunk_try_find_and_clear_fun_t)(mi_bchunk_t* chunk, size_t n, size_t* idx, bool* did_temp_clear_bits); + +// Go through the bbitmap and for every sequence of `n` set bits, call the visitor function. +// If it returns `true` stop the search. +// +// This is used for finding free blocks and it is important to be efficient (with 2-level bitscan) +// but also reduce fragmentation (through size bins). +static bool mi_bbitmap_try_find_and_clear_generic(mi_bbitmap_t* bbitmap, size_t tseq, size_t n, size_t* pidx, mi_bchunk_try_find_and_clear_fun_t* on_find) +{ + // we space out threads to reduce contention + const size_t cmap_max_count = mi_divide_up(mi_bbitmap_chunk_count(bbitmap),MI_BFIELD_BITS); + const size_t chunk_acc = mi_atomic_load_relaxed(&bbitmap->chunk_max_accessed); + const size_t cmap_acc = chunk_acc / MI_BFIELD_BITS; + const size_t cmap_acc_bits = 1 + (chunk_acc % MI_BFIELD_BITS); + + // create a mask over the chunkmap entries to iterate over them efficiently + mi_assert_internal(MI_BFIELD_BITS >= MI_BCHUNK_FIELDS); + const mi_bfield_t cmap_mask = mi_bfield_mask(cmap_max_count,0); + const size_t cmap_cycle = cmap_acc+1; + const mi_chunkbin_t bbin = mi_chunkbin_of(n); + // visit each cmap entry + size_t cmap_idx = 0; + mi_bfield_cycle_iterate(cmap_mask, tseq, cmap_cycle, cmap_idx, X) + { + // and for each chunkmap entry we iterate over its bits to find the chunks + const mi_bfield_t cmap_entry = mi_atomic_load_relaxed(&bbitmap->chunkmap.bfields[cmap_idx]); + const size_t cmap_entry_cycle = (cmap_idx != cmap_acc ? MI_BFIELD_BITS : cmap_acc_bits); + if (cmap_entry == 0) { + continue; + } + + // get size bin masks + mi_bfield_t cmap_bins[MI_CBIN_COUNT] = { 0 }; + cmap_bins[MI_CBIN_NONE] = cmap_entry; + for (mi_chunkbin_t ibin = MI_CBIN_SMALL; ibin < MI_CBIN_NONE; ibin = mi_chunkbin_inc(ibin)) { + const mi_bfield_t cmap_bin = mi_atomic_load_relaxed(&bbitmap->chunkmap_bins[ibin].bfields[cmap_idx]); + cmap_bins[ibin] = cmap_bin & cmap_entry; + cmap_bins[MI_CBIN_NONE] &= ~cmap_bin; // clear bits that are in an assigned size bin + } + + // consider only chunks for a particular size bin at a time + // this picks the best bin only within a cmap entry (~ 1GiB address space), but avoids multiple + // iterations through all entries. + mi_assert_internal(bbin < MI_CBIN_NONE); + for (mi_chunkbin_t ibin = MI_CBIN_SMALL; ibin <= MI_CBIN_NONE; + // skip from bbin to NONE (so, say, a SMALL will never be placed in a OTHER, MEDIUM, or LARGE chunk to reduce fragmentation) + ibin = (ibin == bbin ? MI_CBIN_NONE : mi_chunkbin_inc(ibin))) + { + mi_assert_internal(ibin < MI_CBIN_COUNT); + const mi_bfield_t cmap_bin = cmap_bins[ibin]; + size_t eidx = 0; + mi_bfield_cycle_iterate(cmap_bin, tseq, cmap_entry_cycle, eidx, Y) + { + // assertion doesn't quite hold as the max_accessed may be out-of-date + // mi_assert_internal(cmap_entry_cycle > eidx || ibin == MI_CBIN_NONE); + + // get the chunk + const size_t chunk_idx = cmap_idx*MI_BFIELD_BITS + eidx; + mi_bchunk_t* chunk = &bbitmap->chunks[chunk_idx]; + + size_t cidx; + bool did_temp_clear_bits = false; + const bool found = (*on_find)(chunk, n, &cidx, &did_temp_clear_bits); + if (did_temp_clear_bits) { + // a concurrent find_and_claim may have cleared the chunkmap bit, restore it now + mi_bbitmap_chunkmap_set(bbitmap, chunk_idx, false); + } + if (found) { + if (cidx==0 && ibin == MI_CBIN_NONE) { // only the first block determines the size bin + // this chunk is now reserved for the `bbin` size class + mi_bbitmap_set_chunk_bin(bbitmap, chunk_idx, bbin); + } + *pidx = (chunk_idx * MI_BCHUNK_BITS) + cidx; + mi_assert_internal(*pidx + n <= mi_bbitmap_max_bits(bbitmap)); + return true; + } + else { + // note: we may find that all are cleared only on a second iteration (when we fail to clear any bits) + // but that is ok as the chunkmap is a conservative approximation. + // todo: should _on_find_ return a boolean if there is a chance all are clear to avoid calling `try_clear?` + // probably not as we already only call `try_clear` once we fail to clear any bits. + if (!did_temp_clear_bits) { + mi_bbitmap_chunkmap_try_clear(bbitmap, chunk_idx); + } + } + } + mi_bfield_cycle_iterate_end(Y); + } + } + mi_bfield_cycle_iterate_end(X); + return false; +} + +/* -------------------------------------------------------------------------------- + mi_bbitmap_try_find_and_clear -- used to find free pages + note: the compiler can inline the indirect function calls in combined-source builds +-------------------------------------------------------------------------------- */ + +bool mi_bbitmap_try_find_and_clear(mi_bbitmap_t* bbitmap, size_t tseq, size_t* pidx) { + return mi_bbitmap_try_find_and_clear_generic(bbitmap, tseq, 1, pidx, &mi_bchunk_try_find_and_clear_1); +} + +bool mi_bbitmap_try_find_and_clear8(mi_bbitmap_t* bbitmap, size_t tseq, size_t* pidx) { + return mi_bbitmap_try_find_and_clear_generic(bbitmap, tseq, 8, pidx, &mi_bchunk_try_find_and_clear_8); +} + +// bool mi_bbitmap_try_find_and_clearX(mi_bbitmap_t* bbitmap, size_t tseq, size_t* pidx) { +// return mi_bbitmap_try_find_and_clear_generic(bbitmap, tseq, MI_BFIELD_BITS, pidx, &mi_bchunk_try_find_and_clear_X); +// } + +bool mi_bbitmap_try_find_and_clearNX(mi_bbitmap_t* bbitmap, size_t tseq, size_t n, size_t* pidx) { + mi_assert_internal(n<=MI_BFIELD_BITS); + return mi_bbitmap_try_find_and_clear_generic(bbitmap, tseq, n, pidx, &mi_bchunk_try_find_and_clearNX); +} + +bool mi_bbitmap_try_find_and_clearNC(mi_bbitmap_t* bbitmap, size_t tseq, size_t n, size_t* pidx) { + mi_assert_internal(n<=MI_BCHUNK_BITS); + return mi_bbitmap_try_find_and_clear_generic(bbitmap, tseq, n, pidx, &mi_bchunk_try_find_and_clearNC); +} + + +/* -------------------------------------------------------------------------------- + mi_bbitmap_try_find_and_clear for huge objects spanning multiple chunks +-------------------------------------------------------------------------------- */ + +// Try to atomically clear `n` bits starting at `chunk_idx` where `n` can span over multiple chunks +static bool mi_bbitmap_try_clearN_(mi_bbitmap_t* bbitmap, size_t chunk_idx, size_t n) { + mi_assert_internal((chunk_idx * MI_BCHUNK_BITS) + n <= mi_bbitmap_max_bits(bbitmap)); + size_t m = n; // bits to go + size_t count = 0; // chunk count + while (m > 0) { + mi_bchunk_t* chunk = &bbitmap->chunks[chunk_idx + count]; + bool did_temp_clear_bits = false; + if (!mi_bchunk_try_clearN(chunk, 0, (m > MI_BCHUNK_BITS ? MI_BCHUNK_BITS : m), NULL, &did_temp_clear_bits)) { + if (did_temp_clear_bits) { + // may have raced with a clearer (in on_find) so set the chunkmap bit conservatively + mi_bbitmap_chunkmap_set(bbitmap, chunk_idx + count, false); + } + goto rollback; + } + m = (m <= MI_BCHUNK_BITS ? 0 : m - MI_BCHUNK_BITS); + count++; + } + return true; + +rollback: + // we only need to reset chunks the we just fully cleared + while (count > 0) { + count--; + mi_bchunk_t* chunk = &bbitmap->chunks[chunk_idx + count]; + mi_bchunk_setN(chunk, 0, MI_BCHUNK_BITS, NULL); + // since we may race with clearing, we need to set the chunkmap conservatively + mi_bbitmap_chunkmap_set(bbitmap, chunk_idx + count, false); + } + return false; +} + +// Go through the bbitmap to find a sequence of `n` bits and clear them atomically where `n > MI_ARENA_MAX_CHUNK_OBJ_SIZE` +// Since these are very large object allocations we always search from the start and only consider starting at the start +// of a chunk (for fragmentation and efficiency). +// Todo: for now we try to find full empty chunks to cover `n` but we can allow a partial chunk at the end +// Todo: This scans directly through the chunks -- we might want to consult the cmap as well? +bool mi_bbitmap_try_find_and_clearN_(mi_bbitmap_t* bbitmap, size_t tseq, size_t n, size_t* pidx) { + MI_UNUSED(tseq); + mi_assert(n > 0); if (n==0) { return false; } + + const size_t chunk_max = mi_bbitmap_chunk_count(bbitmap); + const size_t chunk_req = mi_divide_up(n, MI_BCHUNK_BITS); // minimal number of chunks needed + if (chunk_max < chunk_req) { return false; } + + // iterate through the chunks + size_t chunk_idx = 0; + while (chunk_idx <= chunk_max - chunk_req) + { + size_t count = 0; // chunk count + do { + mi_assert_internal(chunk_idx + count < chunk_max); + mi_bchunk_t* const chunk = &bbitmap->chunks[chunk_idx + count]; + if (!mi_bchunk_all_are_set_relaxed(chunk)) { + break; + } + else { + count++; + } + } + while (count < chunk_req); + + // did we find a suitable range? + if (count == chunk_req) { + // now try to claim it! + if (mi_bbitmap_try_clearN_(bbitmap, chunk_idx, n)) { + *pidx = (chunk_idx * MI_BCHUNK_BITS); + for (size_t i = 0; i < count; i++) { + mi_bbitmap_set_chunk_bin(bbitmap, chunk_idx + i, MI_CBIN_HUGE); + } + mi_assert_internal(*pidx + n <= mi_bbitmap_max_bits(bbitmap)); + return true; + } + else { + // contended: we reset count to retry from the first + // (we still skip the first chunk to guarantee progress) + count = 0; + } + } + + // keep searching but skip the scanned range + chunk_idx += count+1; + } + return false; +} diff --git a/Objects/mimalloc/arena/bitmap.h b/Objects/mimalloc/arena/bitmap.h new file mode 100644 index 000000000000000..fab4ec2bccc5fdc --- /dev/null +++ b/Objects/mimalloc/arena/bitmap.h @@ -0,0 +1,346 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2019-2024 Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +/* ---------------------------------------------------------------------------- +Concurrent bitmap that can set/reset sequences of bits atomically +---------------------------------------------------------------------------- */ +#pragma once +#ifndef MI_BITMAP_H +#define MI_BITMAP_H + +#include "mimalloc/types.h" + +/* -------------------------------------------------------------------------------- + Atomic bitmaps with release/acquire guarantees: + + `mi_bfield_t`: is a single machine word that can efficiently be bit counted (usually `size_t`) + each bit usually represents a single MI_ARENA_SLICE_SIZE in an arena (64 KiB). + We need 16K bits to represent a 1GiB arena. + + `mi_bchunk_t`: a chunk of bfield's of a total of MI_BCHUNK_BITS (= 512 on 64-bit, 256 on 32-bit) + allocations never span across chunks -- so MI_ARENA_MAX_OBJ_SIZE is the number + of bits in a chunk times the MI_ARENA_SLICE_SIZE (512 * 64KiB = 32 MiB). + These chunks are cache-aligned and we can use AVX2/AVX512/NEON/SVE/SVE2/etc. instructions + to scan for bits (perhaps) more efficiently. + + We allocate byte-sized ranges aligned to bytes in the bfield, and bfield-sized + ranges aligned to a bfield. + + Searching linearly through the chunks would be too slow (16K bits per GiB). + Instead we add a "chunkmap" to do a two-level search (more or less a btree of depth 2). + + `mi_bchunkmap_t` (== `mi_bchunk_t`): for each chunk we track if it has (potentially) any bit set. + The chunkmap has 1 bit per chunk that is set if the chunk potentially has a bit set. + This is used to avoid scanning every chunk. (and thus strictly an optimization) + It is conservative: it is fine to set a bit in the chunk map even if the chunk turns out + to have no bits set. It is also allowed to briefly have a clear bit even if the + chunk has bits set -- as long as we guarantee that the bit will be set later on; + (this allows us to set the chunkmap bit right after we set a bit in the corresponding chunk). + + However, when we clear a bit in a chunk, and the chunk is indeed all clear, we + cannot safely clear the bit corresponding to the chunk in the chunkmap since it + may race with another thread setting a bit in the same chunk. Therefore, when + clearing, we first test if a chunk is clear, then clear the chunkmap bit, and + then test again to catch any set bits that we may have missed. + + Since the chunkmap may thus be briefly out-of-sync, this means that we may sometimes + not find a free page even though it's there (but we accept this as we avoid taking + full locks). (Another way to do this is to use an epoch but we like to avoid that complexity + for now). + + `mi_bitmap_t`: a bitmap with N chunks. A bitmap has a chunkmap of MI_BCHUNK_BITS (512) + and thus has at most 512 chunks (=2^18 bits x 64 KiB slices = 16 GiB max arena size). + The minimum is 1 chunk which is a 32 MiB arena. + + For now, the implementation assumes MI_HAS_FAST_BITSCAN and uses trailing-zero-count + and pop-count (but we think it can be adapted work reasonably well on older hardware too) +--------------------------------------------------------------------------------------------- */ + +// A word-size bit field. +typedef size_t mi_bfield_t; + +#define MI_BFIELD_BITS_SHIFT (MI_SIZE_SHIFT+3) +#define MI_BFIELD_BITS (1 << MI_BFIELD_BITS_SHIFT) +#define MI_BFIELD_SIZE (MI_BFIELD_BITS/8) +#define MI_BFIELD_LO_BIT8 (((~(mi_bfield_t)0))/0xFF) // 0x01010101 .. +#define MI_BFIELD_HI_BIT8 (MI_BFIELD_LO_BIT8 << 7) // 0x80808080 .. + +#define MI_BCHUNK_SIZE (MI_BCHUNK_BITS / 8) +#define MI_BCHUNK_FIELDS (MI_BCHUNK_BITS / MI_BFIELD_BITS) // 8 on both 64- and 32-bit + + +// some compilers (msvc in C mode) cannot have expressions in the alignment attribute +#if MI_BCHUNK_SIZE==64 +#define mi_decl_bchunk_align mi_decl_align(64) +#elif MI_BCHUNK_SIZE==32 +#define mi_decl_bchunk_align mi_decl_align(32) +#else +#define mi_decl_bchunk_align mi_decl_align(MI_BCHUNK_SIZE) +#endif + + +// A bitmap chunk contains 512 bits on 64-bit (256 on 32-bit) +typedef mi_decl_bchunk_align struct mi_bchunk_s { + _Atomic(mi_bfield_t) bfields[MI_BCHUNK_FIELDS]; +} mi_bchunk_t; + + +// The chunkmap has one bit per corresponding chunk that is set if the chunk potentially has bits set. +// The chunkmap is itself a chunk. +typedef mi_bchunk_t mi_bchunkmap_t; + +#define MI_BCHUNKMAP_BITS MI_BCHUNK_BITS + +#define MI_BITMAP_MAX_CHUNK_COUNT (MI_BCHUNKMAP_BITS) +#define MI_BITMAP_MIN_CHUNK_COUNT (1) +// #if MI_SIZE_BITS > 32 +// #define MI_BITMAP_DEFAULT_CHUNK_COUNT (128) // 4 GiB on 64-bit -- this is for the page map +// #else +#define MI_BITMAP_DEFAULT_CHUNK_COUNT (1) +// #endif +#define MI_BITMAP_MAX_BIT_COUNT (MI_BITMAP_MAX_CHUNK_COUNT * MI_BCHUNK_BITS) // 16 GiB arena +#define MI_BITMAP_MIN_BIT_COUNT (MI_BITMAP_MIN_CHUNK_COUNT * MI_BCHUNK_BITS) // 32 MiB arena +#define MI_BITMAP_DEFAULT_BIT_COUNT (MI_BITMAP_DEFAULT_CHUNK_COUNT * MI_BCHUNK_BITS) // 2 GiB arena + + +// An atomic bitmap +typedef mi_decl_bchunk_align struct mi_bitmap_s { + _Atomic(size_t) chunk_count; // total count of chunks (0 < N <= MI_BCHUNKMAP_BITS) + size_t _padding[MI_BCHUNK_SIZE/MI_SIZE_SIZE - 1]; // suppress warning on msvc + mi_bchunkmap_t chunkmap; + mi_bchunk_t chunks[MI_BITMAP_DEFAULT_CHUNK_COUNT]; // usually dynamic MI_BITMAP_MAX_CHUNK_COUNT +} mi_bitmap_t; + + +static inline size_t mi_bitmap_chunk_count(const mi_bitmap_t* bitmap) { + return mi_atomic_load_relaxed(&((mi_bitmap_t*)bitmap)->chunk_count); +} + +static inline size_t mi_bitmap_max_bits(const mi_bitmap_t* bitmap) { + return (mi_bitmap_chunk_count(bitmap) * MI_BCHUNK_BITS); +} + + + +/* -------------------------------------------------------------------------------- + Atomic bitmap operations +-------------------------------------------------------------------------------- */ + +// Many operations are generic over setting or clearing the bit sequence: we use `mi_xset_t` for this (true if setting, false if clearing) +typedef bool mi_xset_t; +#define MI_BIT_SET (true) +#define MI_BIT_CLEAR (false) + + +// Required size of a bitmap to represent `bit_count` bits. +size_t mi_bitmap_size(size_t bit_count, size_t* chunk_count); + +// Initialize a bitmap to all clear; avoid a mem_zero if `already_zero` is true +// returns the size of the bitmap. +size_t mi_bitmap_init(mi_bitmap_t* bitmap, size_t bit_count, bool already_zero); + +// Set/clear a sequence of `n` bits in the bitmap (and can cross chunks). +// Not atomic so only use if still local to a thread. +void mi_bitmap_unsafe_setN(mi_bitmap_t* bitmap, size_t idx, size_t n); + + +// Set a bit in the bitmap; returns `true` if it atomically transitioned from 0 to 1 +bool mi_bitmap_set(mi_bitmap_t* bitmap, size_t idx); + +// Clear a bit in the bitmap; returns `true` if it atomically transitioned from 1 to 0 +bool mi_bitmap_clear(mi_bitmap_t* bitmap, size_t idx); + +// Set a sequence of `n` bits in the bitmap; returns `true` if atomically transitioned from all 0's to 1's +// If `already_set` is not NULL, it is set to count of bits were already all set. +// (this is used for correct statistics if commiting over a partially committed area) +bool mi_bitmap_setN(mi_bitmap_t* bitmap, size_t idx, size_t n, size_t* already_set); + +// Clear a sequence of `n` bits in the bitmap; returns `true` if atomically transitioned from all 1's to 0's +bool mi_bitmap_clearN(mi_bitmap_t* bitmap, size_t idx, size_t n); + + +// Is a sequence of n bits already all set/cleared? +bool mi_bitmap_is_xsetN(mi_xset_t set, mi_bitmap_t* bitmap, size_t idx, size_t n); + +// Is the bitmap completely clear? +bool mi_bitmap_is_all_clear(mi_bitmap_t* bitmap); + +// Is a sequence of n bits already set? +// (Used to check if a memory range is already committed) +static inline bool mi_bitmap_is_setN(mi_bitmap_t* bitmap, size_t idx, size_t n) { + return mi_bitmap_is_xsetN(MI_BIT_SET, bitmap, idx, n); +} + +// Is a sequence of n bits already clear? +static inline bool mi_bitmap_is_clearN(mi_bitmap_t* bitmap, size_t idx, size_t n) { + return mi_bitmap_is_xsetN(MI_BIT_CLEAR, bitmap, idx, n); +} + +static inline bool mi_bitmap_is_set(mi_bitmap_t* bitmap, size_t idx) { + return mi_bitmap_is_setN(bitmap, idx, 1); +} + +static inline bool mi_bitmap_is_clear(mi_bitmap_t* bitmap, size_t idx) { + return mi_bitmap_is_clearN(bitmap, idx, 1); +} + +// Called once a bit is cleared to see if the memory slice can be claimed. +typedef bool (mi_claim_fun_t)(size_t slice_index, mi_arena_t* arena, bool* keep_set); + +// Find a set bits in the bitmap, atomically clear it, and check if `claim` returns true. +// If not claimed, continue on (potentially setting the bit again depending on `keep_set`). +// Returns true on success, and in that case sets the index: `0 <= *pidx <= MI_BITMAP_MAX_BITS-n`. +mi_decl_nodiscard bool mi_bitmap_try_find_and_claim(mi_bitmap_t* bitmap, size_t tseq, size_t* pidx, + mi_claim_fun_t* claim, mi_arena_t* arena ); + + +// Atomically clear a bit but only if it is set. Will block otherwise until the bit is set. +// This is used to delay free-ing a page that it at the same time being considered to be +// allocated from `mi_arena_try_abandoned` (and is in the `claim` function of `mi_bitmap_try_find_and_claim`). +void mi_bitmap_clear_once_set(mi_subproc_t* subproc, mi_bitmap_t* bitmap, size_t idx); + + +// If a bit is set in the bitmap, return `true` and set `idx` to the index of the highest bit. +// Otherwise return `false` (and `*idx` is undefined). +// Used for unloading arena's +bool mi_bitmap_bsr(mi_bitmap_t* bitmap, size_t* idx); + +// Return count of all set bits in a bitmap. +size_t mi_bitmap_popcount(mi_bitmap_t* bitmap); + + +typedef bool (mi_forall_set_fun_t)(size_t slice_index, size_t slice_count, mi_arena_t* arena, void* arg2); + +// Visit all set bits in a bitmap (`slice_count == 1`) +bool _mi_bitmap_forall_set(mi_bitmap_t* bitmap, mi_forall_set_fun_t* visit, mi_arena_t* arena, void* arg); + +// Visit all set bits in a bitmap with larger ranges if possible (`slice_count >= 1`) +// Ranges will never cross chunk boundaries though (and `slice_count <= MI_BCHUNK_BITS`) +bool _mi_bitmap_forall_setc_ranges(mi_bitmap_t* bitmap, mi_forall_set_fun_t* visit, mi_arena_t* arena, void* arg); + +// Visit all set bits in a bitmap with at least `rngslices` at a time (and aligned to `rngslices`). +// This is used by purging to not break up transparent huge pages for example. +// Ranges will never cross chunk boundaries (and `slice_count <= MI_BCHUNK_BITS`). +bool _mi_bitmap_forall_setc_rangesn(mi_bitmap_t* bitmap, size_t rngslices, mi_forall_set_fun_t* visit, mi_arena_t* arena, void* arg); + +// Count all set bits in given range in the bitmap. +size_t mi_bitmap_popcountN( mi_bitmap_t* bitmap, size_t idx, size_t n); + +/* ---------------------------------------------------------------------------- + Binned concurrent bitmap + Assigns a size class to each chunk such that small blocks don't cause too + much fragmentation since we keep chunks for larger blocks separate. +---------------------------------------------------------------------------- */ + +// mi_chunkbin_t is defined in mimalloc-stats.h + +static inline mi_chunkbin_t mi_chunkbin_inc(mi_chunkbin_t bbin) { + mi_assert_internal(bbin < MI_CBIN_COUNT); + return (mi_chunkbin_t)((int)bbin + 1); +} + +static inline mi_chunkbin_t mi_chunkbin_dec(mi_chunkbin_t bbin) { + mi_assert_internal(bbin > MI_CBIN_NONE); + return (mi_chunkbin_t)((int)bbin - 1); +} + +static inline mi_chunkbin_t mi_chunkbin_of(size_t slice_count) { + if (slice_count==1) return MI_CBIN_SMALL; + if (slice_count==8) return MI_CBIN_MEDIUM; + #if MI_ENABLE_LARGE_PAGES + if (slice_count==MI_BFIELD_BITS) return MI_CBIN_LARGE; + #endif + if (slice_count > MI_BCHUNK_BITS) return MI_CBIN_HUGE; + return MI_CBIN_OTHER; +} + +// An atomic "binned" bitmap for the free slices where we keep chunks reserved for particular size classes +typedef mi_decl_bchunk_align struct mi_bbitmap_s { + _Atomic(size_t) chunk_count; // total count of chunks (0 < N <= MI_BCHUNKMAP_BITS) + _Atomic(size_t) chunk_max_accessed; // max chunk index that was once cleared or set + mi_subproc_t* subproc; // constant, for stats + #if (MI_BCHUNK_SIZE / MI_SIZE_SIZE) > 3 + size_t _padding[MI_BCHUNK_SIZE/MI_SIZE_SIZE - 3]; // suppress warning on msvc by aligning manually + #endif + mi_bchunkmap_t chunkmap; + mi_bchunkmap_t chunkmap_bins[MI_CBIN_COUNT - 1]; // chunkmaps with bit set if the chunk is in that size class (excluding MI_CBIN_NONE) + mi_bchunk_t chunks[MI_BITMAP_DEFAULT_CHUNK_COUNT]; // usually dynamic MI_BITMAP_MAX_CHUNK_COUNT +} mi_bbitmap_t; + + +static inline size_t mi_bbitmap_chunk_count(const mi_bbitmap_t* bbitmap) { + return mi_atomic_load_relaxed(&((mi_bbitmap_t*)bbitmap)->chunk_count); +} + +static inline size_t mi_bbitmap_max_bits(const mi_bbitmap_t* bbitmap) { + return (mi_bbitmap_chunk_count(bbitmap) * MI_BCHUNK_BITS); +} + +mi_chunkbin_t mi_bbitmap_debug_get_bin(const mi_bchunk_t* chunkmap_bins, size_t chunk_idx); + +size_t mi_bbitmap_size(size_t bit_count, size_t* chunk_count); + +// If a bit is clear in the bitmap, return `true` and set `idx` to the index of the highest bit that was clear. +// Otherwise return `false` (and `*idx` is undefined). +// Used for debug output. +bool mi_bbitmap_bsr_inv(mi_bbitmap_t* bbitmap, size_t* idx); + +// Initialize a bitmap to all clear; avoid a mem_zero if `already_zero` is true +// returns the size of the bitmap. +size_t mi_bbitmap_init(mi_subproc_t* subproc, mi_bbitmap_t* bbitmap, size_t bit_count, bool already_zero); + +// Set/clear a sequence of `n` bits in the bitmap (and can cross chunks). +// Not atomic so only use if still local to a thread. +void mi_bbitmap_unsafe_setN(mi_bbitmap_t* bbitmap, size_t idx, size_t n); + + +// Set a sequence of `n` bits in the bbitmap; returns `true` if atomically transitioned from all 0's to 1's +bool mi_bbitmap_setN(mi_bbitmap_t* bbitmap, size_t idx, size_t n); + + +// Is a sequence of n bits already all set/cleared? +bool mi_bbitmap_is_xsetN(mi_xset_t set, mi_bbitmap_t* bbitmap, size_t idx, size_t n); + +// Is a sequence of n bits already set? +// (Used to check if a memory range is already committed) +static inline bool mi_bbitmap_is_setN(mi_bbitmap_t* bbitmap, size_t idx, size_t n) { + return mi_bbitmap_is_xsetN(MI_BIT_SET, bbitmap, idx, n); +} + +// Is a sequence of n bits already clear? +static inline bool mi_bbitmap_is_clearN(mi_bbitmap_t* bbitmap, size_t idx, size_t n) { + return mi_bbitmap_is_xsetN(MI_BIT_CLEAR, bbitmap, idx, n); +} + + +// Try to atomically transition `n` bits from all set to all clear. Returns `true` on succes. +// `n` cannot cross chunk boundaries, where `n <= MI_CHUNK_BITS`. +bool mi_bbitmap_try_clearNC(mi_bbitmap_t* bbitmap, size_t idx, size_t n); + + +// Specialized versions for common bit sequence sizes +bool mi_bbitmap_try_find_and_clear(mi_bbitmap_t* bbitmap, size_t tseq, size_t* pidx); // 1-bit +bool mi_bbitmap_try_find_and_clear8(mi_bbitmap_t* bbitmap, size_t tseq, size_t* pidx); // 8-bits +// bool mi_bbitmap_try_find_and_clearX(mi_bbitmap_t* bbitmap, size_t tseq, size_t* pidx); // MI_BFIELD_BITS +bool mi_bbitmap_try_find_and_clearNX(mi_bbitmap_t* bbitmap, size_t tseq, size_t n, size_t* pidx); // < MI_BFIELD_BITS +bool mi_bbitmap_try_find_and_clearNC(mi_bbitmap_t* bbitmap, size_t tseq, size_t n, size_t* pidx); // > MI_BFIELD_BITS <= MI_BCHUNK_BITS +bool mi_bbitmap_try_find_and_clearN_(mi_bbitmap_t* bbitmap, size_t tseq, size_t n, size_t* pidx); // > MI_BCHUNK_BITS + +// Find a sequence of `n` bits in the bbitmap with all bits set, and try to atomically clear all. +// Returns true on success, and in that case sets the index: `0 <= *pidx <= MI_BITMAP_MAX_BITS-n`. +mi_decl_nodiscard static inline bool mi_bbitmap_try_find_and_clearN(mi_bbitmap_t* bbitmap, size_t tseq, size_t n, size_t* pidx) { + if (n==1) return mi_bbitmap_try_find_and_clear(bbitmap, tseq, pidx); // small pages + if (n==8) return mi_bbitmap_try_find_and_clear8(bbitmap, tseq, pidx); // medium pages + // if (n==MI_BFIELD_BITS) return mi_bbitmap_try_find_and_clearX(bbitmap, tseq, pidx); // large pages + if (n==0) return false; + if (n<=MI_BFIELD_BITS) return mi_bbitmap_try_find_and_clearNX(bbitmap, tseq, n, pidx); + if (n<=MI_BCHUNK_BITS) return mi_bbitmap_try_find_and_clearNC(bbitmap, tseq, n, pidx); + return mi_bbitmap_try_find_and_clearN_(bbitmap, tseq, n, pidx); +} + + +#endif // MI_BITMAP_H diff --git a/Objects/mimalloc/arena/readme.md b/Objects/mimalloc/arena/readme.md new file mode 100644 index 000000000000000..efd6caeddf5afb3 --- /dev/null +++ b/Objects/mimalloc/arena/readme.md @@ -0,0 +1,10 @@ +# Memory Arenas and Concurrent Bitmaps + +- [`arena.c`](arena.c): Arena creation, reservation, traversal, and destruction. +- [`arena-alloc.c`](arena-alloc.c): Allocation, freeing, commitment, and purging of arena slices. +- [`arena-page.c`](arena-page.c): Allocation, abandonment, reclamation, and freeing of pages backed by arena slices. +- [`arena.h`](arena.h): Private arena declarations, traversal callbacks, and layout helpers. +- [`bitmap.c`](bitmap.c): Concurrent bitmap searches and range operations; these bitmaps are used to manage the slices in an arena. +- [`bitmap-chunk.c`](bitmap-chunk.c): Bitmaps consist of 512-bit chunks; this provides the low-level atomic bit-field and bitmap-chunk operations. +- [`bitmap.h`](bitmap.h): Bitmap types, constants, and operation declarations. +- [`bitmap-chunk.h`](bitmap-chunk.h): Private declarations and iteration helpers for bitmap fields and chunks. diff --git a/Objects/mimalloc/bitmap.c b/Objects/mimalloc/bitmap.c deleted file mode 100644 index 31830756f58c7c0..000000000000000 --- a/Objects/mimalloc/bitmap.c +++ /dev/null @@ -1,432 +0,0 @@ -/* ---------------------------------------------------------------------------- -Copyright (c) 2019-2023 Microsoft Research, Daan Leijen -This is free software; you can redistribute it and/or modify it under the -terms of the MIT license. A copy of the license can be found in the file -"LICENSE" at the root of this distribution. ------------------------------------------------------------------------------*/ - -/* ---------------------------------------------------------------------------- -Concurrent bitmap that can set/reset sequences of bits atomically, -represented as an array of fields where each field is a machine word (`size_t`) - -There are two api's; the standard one cannot have sequences that cross -between the bitmap fields (and a sequence must be <= MI_BITMAP_FIELD_BITS). - -The `_across` postfixed functions do allow sequences that can cross over -between the fields. (This is used in arena allocation) ----------------------------------------------------------------------------- */ - -#include "mimalloc.h" -#include "mimalloc/internal.h" -#include "bitmap.h" - -/* ----------------------------------------------------------- - Bitmap definition ------------------------------------------------------------ */ - -// The bit mask for a given number of blocks at a specified bit index. -static inline size_t mi_bitmap_mask_(size_t count, size_t bitidx) { - mi_assert_internal(count + bitidx <= MI_BITMAP_FIELD_BITS); - mi_assert_internal(count > 0); - if (count >= MI_BITMAP_FIELD_BITS) return MI_BITMAP_FIELD_FULL; - if (count == 0) return 0; - return ((((size_t)1 << count) - 1) << bitidx); -} - - -/* ----------------------------------------------------------- - Claim a bit sequence atomically ------------------------------------------------------------ */ - -// Try to atomically claim a sequence of `count` bits in a single -// field at `idx` in `bitmap`. Returns `true` on success. -inline bool _mi_bitmap_try_find_claim_field(mi_bitmap_t bitmap, size_t idx, const size_t count, mi_bitmap_index_t* bitmap_idx) -{ - mi_assert_internal(bitmap_idx != NULL); - mi_assert_internal(count <= MI_BITMAP_FIELD_BITS); - mi_assert_internal(count > 0); - mi_bitmap_field_t* field = &bitmap[idx]; - size_t map = mi_atomic_load_relaxed(field); - if (map==MI_BITMAP_FIELD_FULL) return false; // short cut - - // search for 0-bit sequence of length count - const size_t mask = mi_bitmap_mask_(count, 0); - const size_t bitidx_max = MI_BITMAP_FIELD_BITS - count; - -#ifdef MI_HAVE_FAST_BITSCAN - size_t bitidx = mi_ctz(~map); // quickly find the first zero bit if possible -#else - size_t bitidx = 0; // otherwise start at 0 -#endif - size_t m = (mask << bitidx); // invariant: m == mask shifted by bitidx - - // scan linearly for a free range of zero bits - while (bitidx <= bitidx_max) { - const size_t mapm = (map & m); - if (mapm == 0) { // are the mask bits free at bitidx? - mi_assert_internal((m >> bitidx) == mask); // no overflow? - const size_t newmap = (map | m); - mi_assert_internal((newmap^map) >> bitidx == mask); - if (!mi_atomic_cas_strong_acq_rel(field, &map, newmap)) { // TODO: use weak cas here? - // no success, another thread claimed concurrently.. keep going (with updated `map`) - continue; - } - else { - // success, we claimed the bits! - *bitmap_idx = mi_bitmap_index_create(idx, bitidx); - return true; - } - } - else { - // on to the next bit range -#ifdef MI_HAVE_FAST_BITSCAN - mi_assert_internal(mapm != 0); - const size_t shift = (count == 1 ? 1 : (MI_INTPTR_BITS - mi_clz(mapm) - bitidx)); - mi_assert_internal(shift > 0 && shift <= count); -#else - const size_t shift = 1; -#endif - bitidx += shift; - m <<= shift; - } - } - // no bits found - return false; -} - -// Find `count` bits of 0 and set them to 1 atomically; returns `true` on success. -// Starts at idx, and wraps around to search in all `bitmap_fields` fields. -// `count` can be at most MI_BITMAP_FIELD_BITS and will never cross fields. -bool _mi_bitmap_try_find_from_claim(mi_bitmap_t bitmap, const size_t bitmap_fields, const size_t start_field_idx, const size_t count, mi_bitmap_index_t* bitmap_idx) { - size_t idx = start_field_idx; - for (size_t visited = 0; visited < bitmap_fields; visited++, idx++) { - if (idx >= bitmap_fields) { idx = 0; } // wrap - if (_mi_bitmap_try_find_claim_field(bitmap, idx, count, bitmap_idx)) { - return true; - } - } - return false; -} - -// Like _mi_bitmap_try_find_from_claim but with an extra predicate that must be fulfilled -bool _mi_bitmap_try_find_from_claim_pred(mi_bitmap_t bitmap, const size_t bitmap_fields, - const size_t start_field_idx, const size_t count, - mi_bitmap_pred_fun_t pred_fun, void* pred_arg, - mi_bitmap_index_t* bitmap_idx) { - size_t idx = start_field_idx; - for (size_t visited = 0; visited < bitmap_fields; visited++, idx++) { - if (idx >= bitmap_fields) idx = 0; // wrap - if (_mi_bitmap_try_find_claim_field(bitmap, idx, count, bitmap_idx)) { - if (pred_fun == NULL || pred_fun(*bitmap_idx, pred_arg)) { - return true; - } - // predicate returned false, unclaim and look further - _mi_bitmap_unclaim(bitmap, bitmap_fields, count, *bitmap_idx); - } - } - return false; -} - -// Set `count` bits at `bitmap_idx` to 0 atomically -// Returns `true` if all `count` bits were 1 previously. -bool _mi_bitmap_unclaim(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx) { - const size_t idx = mi_bitmap_index_field(bitmap_idx); - const size_t bitidx = mi_bitmap_index_bit_in_field(bitmap_idx); - const size_t mask = mi_bitmap_mask_(count, bitidx); - mi_assert_internal(bitmap_fields > idx); MI_UNUSED(bitmap_fields); - // mi_assert_internal((bitmap[idx] & mask) == mask); - const size_t prev = mi_atomic_and_acq_rel(&bitmap[idx], ~mask); - return ((prev & mask) == mask); -} - - -// Set `count` bits at `bitmap_idx` to 1 atomically -// Returns `true` if all `count` bits were 0 previously. `any_zero` is `true` if there was at least one zero bit. -bool _mi_bitmap_claim(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx, bool* any_zero) { - const size_t idx = mi_bitmap_index_field(bitmap_idx); - const size_t bitidx = mi_bitmap_index_bit_in_field(bitmap_idx); - const size_t mask = mi_bitmap_mask_(count, bitidx); - mi_assert_internal(bitmap_fields > idx); MI_UNUSED(bitmap_fields); - //mi_assert_internal(any_zero != NULL || (bitmap[idx] & mask) == 0); - size_t prev = mi_atomic_or_acq_rel(&bitmap[idx], mask); - if (any_zero != NULL) { *any_zero = ((prev & mask) != mask); } - return ((prev & mask) == 0); -} - -// Returns `true` if all `count` bits were 1. `any_ones` is `true` if there was at least one bit set to one. -static bool mi_bitmap_is_claimedx(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx, bool* any_ones) { - const size_t idx = mi_bitmap_index_field(bitmap_idx); - const size_t bitidx = mi_bitmap_index_bit_in_field(bitmap_idx); - const size_t mask = mi_bitmap_mask_(count, bitidx); - mi_assert_internal(bitmap_fields > idx); MI_UNUSED(bitmap_fields); - const size_t field = mi_atomic_load_relaxed(&bitmap[idx]); - if (any_ones != NULL) { *any_ones = ((field & mask) != 0); } - return ((field & mask) == mask); -} - -// Try to set `count` bits at `bitmap_idx` from 0 to 1 atomically. -// Returns `true` if successful when all previous `count` bits were 0. -bool _mi_bitmap_try_claim(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx) { - const size_t idx = mi_bitmap_index_field(bitmap_idx); - const size_t bitidx = mi_bitmap_index_bit_in_field(bitmap_idx); - const size_t mask = mi_bitmap_mask_(count, bitidx); - mi_assert_internal(bitmap_fields > idx); MI_UNUSED(bitmap_fields); - size_t expected = mi_atomic_load_relaxed(&bitmap[idx]); - do { - if ((expected & mask) != 0) return false; - } - while (!mi_atomic_cas_strong_acq_rel(&bitmap[idx], &expected, expected | mask)); - mi_assert_internal((expected & mask) == 0); - return true; -} - - -bool _mi_bitmap_is_claimed(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx) { - return mi_bitmap_is_claimedx(bitmap, bitmap_fields, count, bitmap_idx, NULL); -} - -bool _mi_bitmap_is_any_claimed(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx) { - bool any_ones; - mi_bitmap_is_claimedx(bitmap, bitmap_fields, count, bitmap_idx, &any_ones); - return any_ones; -} - - -//-------------------------------------------------------------------------- -// the `_across` functions work on bitmaps where sequences can cross over -// between the fields. This is used in arena allocation -//-------------------------------------------------------------------------- - -// Try to atomically claim a sequence of `count` bits starting from the field -// at `idx` in `bitmap` and crossing into subsequent fields. Returns `true` on success. -// Only needs to consider crossing into the next fields (see `mi_bitmap_try_find_from_claim_across`) -static bool mi_bitmap_try_find_claim_field_across(mi_bitmap_t bitmap, size_t bitmap_fields, size_t idx, const size_t count, const size_t retries, mi_bitmap_index_t* bitmap_idx) -{ - mi_assert_internal(bitmap_idx != NULL); - - // check initial trailing zeros - mi_bitmap_field_t* field = &bitmap[idx]; - size_t map = mi_atomic_load_relaxed(field); - const size_t initial = mi_clz(map); // count of initial zeros starting at idx - mi_assert_internal(initial <= MI_BITMAP_FIELD_BITS); - if (initial == 0) return false; - if (initial >= count) return _mi_bitmap_try_find_claim_field(bitmap, idx, count, bitmap_idx); // no need to cross fields (this case won't happen for us) - if (_mi_divide_up(count - initial, MI_BITMAP_FIELD_BITS) >= (bitmap_fields - idx)) return false; // not enough entries - - // scan ahead - size_t found = initial; - size_t mask = 0; // mask bits for the final field - while(found < count) { - field++; - map = mi_atomic_load_relaxed(field); - const size_t mask_bits = (found + MI_BITMAP_FIELD_BITS <= count ? MI_BITMAP_FIELD_BITS : (count - found)); - mi_assert_internal(mask_bits > 0 && mask_bits <= MI_BITMAP_FIELD_BITS); - mask = mi_bitmap_mask_(mask_bits, 0); - if ((map & mask) != 0) return false; // some part is already claimed - found += mask_bits; - } - mi_assert_internal(field < &bitmap[bitmap_fields]); - - // we found a range of contiguous zeros up to the final field; mask contains mask in the final field - // now try to claim the range atomically - mi_bitmap_field_t* const final_field = field; - const size_t final_mask = mask; - mi_bitmap_field_t* const initial_field = &bitmap[idx]; - const size_t initial_idx = MI_BITMAP_FIELD_BITS - initial; - const size_t initial_mask = mi_bitmap_mask_(initial, initial_idx); - - // initial field - size_t newmap; - field = initial_field; - map = mi_atomic_load_relaxed(field); - do { - newmap = (map | initial_mask); - if ((map & initial_mask) != 0) { goto rollback; }; - } while (!mi_atomic_cas_strong_acq_rel(field, &map, newmap)); - - // intermediate fields - while (++field < final_field) { - newmap = MI_BITMAP_FIELD_FULL; - map = 0; - if (!mi_atomic_cas_strong_acq_rel(field, &map, newmap)) { goto rollback; } - } - - // final field - mi_assert_internal(field == final_field); - map = mi_atomic_load_relaxed(field); - do { - newmap = (map | final_mask); - if ((map & final_mask) != 0) { goto rollback; } - } while (!mi_atomic_cas_strong_acq_rel(field, &map, newmap)); - - // claimed! - *bitmap_idx = mi_bitmap_index_create(idx, initial_idx); - return true; - -rollback: - // roll back intermediate fields - // (we just failed to claim `field` so decrement first) - while (--field > initial_field) { - newmap = 0; - map = MI_BITMAP_FIELD_FULL; - mi_assert_internal(mi_atomic_load_relaxed(field) == map); - mi_atomic_store_release(field, newmap); - } - if (field == initial_field) { // (if we failed on the initial field, `field + 1 == initial_field`) - map = mi_atomic_load_relaxed(field); - do { - mi_assert_internal((map & initial_mask) == initial_mask); - newmap = (map & ~initial_mask); - } while (!mi_atomic_cas_strong_acq_rel(field, &map, newmap)); - } - // retry? (we make a recursive call instead of goto to be able to use const declarations) - if (retries <= 2) { - return mi_bitmap_try_find_claim_field_across(bitmap, bitmap_fields, idx, count, retries+1, bitmap_idx); - } - else { - return false; - } -} - - -// Find `count` bits of zeros and set them to 1 atomically; returns `true` on success. -// Starts at idx, and wraps around to search in all `bitmap_fields` fields. -bool _mi_bitmap_try_find_from_claim_across(mi_bitmap_t bitmap, const size_t bitmap_fields, const size_t start_field_idx, const size_t count, mi_bitmap_index_t* bitmap_idx) { - mi_assert_internal(count > 0); - if (count <= 2) { - // we don't bother with crossover fields for small counts - return _mi_bitmap_try_find_from_claim(bitmap, bitmap_fields, start_field_idx, count, bitmap_idx); - } - - // visit the fields - size_t idx = start_field_idx; - for (size_t visited = 0; visited < bitmap_fields; visited++, idx++) { - if (idx >= bitmap_fields) { idx = 0; } // wrap - // first try to claim inside a field - if (count <= MI_BITMAP_FIELD_BITS) { - if (_mi_bitmap_try_find_claim_field(bitmap, idx, count, bitmap_idx)) { - return true; - } - } - // if that fails, then try to claim across fields - if (mi_bitmap_try_find_claim_field_across(bitmap, bitmap_fields, idx, count, 0, bitmap_idx)) { - return true; - } - } - return false; -} - -// Helper for masks across fields; returns the mid count, post_mask may be 0 -static size_t mi_bitmap_mask_across(mi_bitmap_index_t bitmap_idx, size_t bitmap_fields, size_t count, size_t* pre_mask, size_t* mid_mask, size_t* post_mask) { - MI_UNUSED(bitmap_fields); - const size_t bitidx = mi_bitmap_index_bit_in_field(bitmap_idx); - if mi_likely(bitidx + count <= MI_BITMAP_FIELD_BITS) { - *pre_mask = mi_bitmap_mask_(count, bitidx); - *mid_mask = 0; - *post_mask = 0; - mi_assert_internal(mi_bitmap_index_field(bitmap_idx) < bitmap_fields); - return 0; - } - else { - const size_t pre_bits = MI_BITMAP_FIELD_BITS - bitidx; - mi_assert_internal(pre_bits < count); - *pre_mask = mi_bitmap_mask_(pre_bits, bitidx); - count -= pre_bits; - const size_t mid_count = (count / MI_BITMAP_FIELD_BITS); - *mid_mask = MI_BITMAP_FIELD_FULL; - count %= MI_BITMAP_FIELD_BITS; - *post_mask = (count==0 ? 0 : mi_bitmap_mask_(count, 0)); - mi_assert_internal(mi_bitmap_index_field(bitmap_idx) + mid_count + (count==0 ? 0 : 1) < bitmap_fields); - return mid_count; - } -} - -// Set `count` bits at `bitmap_idx` to 0 atomically -// Returns `true` if all `count` bits were 1 previously. -bool _mi_bitmap_unclaim_across(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx) { - size_t idx = mi_bitmap_index_field(bitmap_idx); - size_t pre_mask; - size_t mid_mask; - size_t post_mask; - size_t mid_count = mi_bitmap_mask_across(bitmap_idx, bitmap_fields, count, &pre_mask, &mid_mask, &post_mask); - bool all_one = true; - mi_bitmap_field_t* field = &bitmap[idx]; - size_t prev = mi_atomic_and_acq_rel(field++, ~pre_mask); // clear first part - if ((prev & pre_mask) != pre_mask) all_one = false; - while(mid_count-- > 0) { - prev = mi_atomic_and_acq_rel(field++, ~mid_mask); // clear mid part - if ((prev & mid_mask) != mid_mask) all_one = false; - } - if (post_mask!=0) { - prev = mi_atomic_and_acq_rel(field, ~post_mask); // clear end part - if ((prev & post_mask) != post_mask) all_one = false; - } - return all_one; -} - -// Set `count` bits at `bitmap_idx` to 1 atomically -// Returns `true` if all `count` bits were 0 previously. `any_zero` is `true` if there was at least one zero bit. -bool _mi_bitmap_claim_across(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx, bool* pany_zero) { - size_t idx = mi_bitmap_index_field(bitmap_idx); - size_t pre_mask; - size_t mid_mask; - size_t post_mask; - size_t mid_count = mi_bitmap_mask_across(bitmap_idx, bitmap_fields, count, &pre_mask, &mid_mask, &post_mask); - bool all_zero = true; - bool any_zero = false; - _Atomic(size_t)*field = &bitmap[idx]; - size_t prev = mi_atomic_or_acq_rel(field++, pre_mask); - if ((prev & pre_mask) != 0) all_zero = false; - if ((prev & pre_mask) != pre_mask) any_zero = true; - while (mid_count-- > 0) { - prev = mi_atomic_or_acq_rel(field++, mid_mask); - if ((prev & mid_mask) != 0) all_zero = false; - if ((prev & mid_mask) != mid_mask) any_zero = true; - } - if (post_mask!=0) { - prev = mi_atomic_or_acq_rel(field, post_mask); - if ((prev & post_mask) != 0) all_zero = false; - if ((prev & post_mask) != post_mask) any_zero = true; - } - if (pany_zero != NULL) { *pany_zero = any_zero; } - return all_zero; -} - - -// Returns `true` if all `count` bits were 1. -// `any_ones` is `true` if there was at least one bit set to one. -static bool mi_bitmap_is_claimedx_across(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx, bool* pany_ones) { - size_t idx = mi_bitmap_index_field(bitmap_idx); - size_t pre_mask; - size_t mid_mask; - size_t post_mask; - size_t mid_count = mi_bitmap_mask_across(bitmap_idx, bitmap_fields, count, &pre_mask, &mid_mask, &post_mask); - bool all_ones = true; - bool any_ones = false; - mi_bitmap_field_t* field = &bitmap[idx]; - size_t prev = mi_atomic_load_relaxed(field++); - if ((prev & pre_mask) != pre_mask) all_ones = false; - if ((prev & pre_mask) != 0) any_ones = true; - while (mid_count-- > 0) { - prev = mi_atomic_load_relaxed(field++); - if ((prev & mid_mask) != mid_mask) all_ones = false; - if ((prev & mid_mask) != 0) any_ones = true; - } - if (post_mask!=0) { - prev = mi_atomic_load_relaxed(field); - if ((prev & post_mask) != post_mask) all_ones = false; - if ((prev & post_mask) != 0) any_ones = true; - } - if (pany_ones != NULL) { *pany_ones = any_ones; } - return all_ones; -} - -bool _mi_bitmap_is_claimed_across(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx) { - return mi_bitmap_is_claimedx_across(bitmap, bitmap_fields, count, bitmap_idx, NULL); -} - -bool _mi_bitmap_is_any_claimed_across(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx) { - bool any_ones; - mi_bitmap_is_claimedx_across(bitmap, bitmap_fields, count, bitmap_idx, &any_ones); - return any_ones; -} diff --git a/Objects/mimalloc/bitmap.h b/Objects/mimalloc/bitmap.h deleted file mode 100644 index d60f6dd3cb8e274..000000000000000 --- a/Objects/mimalloc/bitmap.h +++ /dev/null @@ -1,115 +0,0 @@ -/* ---------------------------------------------------------------------------- -Copyright (c) 2019-2023 Microsoft Research, Daan Leijen -This is free software; you can redistribute it and/or modify it under the -terms of the MIT license. A copy of the license can be found in the file -"LICENSE" at the root of this distribution. ------------------------------------------------------------------------------*/ - -/* ---------------------------------------------------------------------------- -Concurrent bitmap that can set/reset sequences of bits atomically, -represented as an array of fields where each field is a machine word (`size_t`) - -There are two api's; the standard one cannot have sequences that cross -between the bitmap fields (and a sequence must be <= MI_BITMAP_FIELD_BITS). -(this is used in region allocation) - -The `_across` postfixed functions do allow sequences that can cross over -between the fields. (This is used in arena allocation) ----------------------------------------------------------------------------- */ -#pragma once -#ifndef MI_BITMAP_H -#define MI_BITMAP_H - -/* ----------------------------------------------------------- - Bitmap definition ------------------------------------------------------------ */ - -#define MI_BITMAP_FIELD_BITS (8*MI_SIZE_SIZE) -#define MI_BITMAP_FIELD_FULL (~((size_t)0)) // all bits set - -// An atomic bitmap of `size_t` fields -typedef _Atomic(size_t) mi_bitmap_field_t; -typedef mi_bitmap_field_t* mi_bitmap_t; - -// A bitmap index is the index of the bit in a bitmap. -typedef size_t mi_bitmap_index_t; - -// Create a bit index. -static inline mi_bitmap_index_t mi_bitmap_index_create(size_t idx, size_t bitidx) { - mi_assert_internal(bitidx < MI_BITMAP_FIELD_BITS); - return (idx*MI_BITMAP_FIELD_BITS) + bitidx; -} - -// Create a bit index. -static inline mi_bitmap_index_t mi_bitmap_index_create_from_bit(size_t full_bitidx) { - return mi_bitmap_index_create(full_bitidx / MI_BITMAP_FIELD_BITS, full_bitidx % MI_BITMAP_FIELD_BITS); -} - -// Get the field index from a bit index. -static inline size_t mi_bitmap_index_field(mi_bitmap_index_t bitmap_idx) { - return (bitmap_idx / MI_BITMAP_FIELD_BITS); -} - -// Get the bit index in a bitmap field -static inline size_t mi_bitmap_index_bit_in_field(mi_bitmap_index_t bitmap_idx) { - return (bitmap_idx % MI_BITMAP_FIELD_BITS); -} - -// Get the full bit index -static inline size_t mi_bitmap_index_bit(mi_bitmap_index_t bitmap_idx) { - return bitmap_idx; -} - -/* ----------------------------------------------------------- - Claim a bit sequence atomically ------------------------------------------------------------ */ - -// Try to atomically claim a sequence of `count` bits in a single -// field at `idx` in `bitmap`. Returns `true` on success. -bool _mi_bitmap_try_find_claim_field(mi_bitmap_t bitmap, size_t idx, const size_t count, mi_bitmap_index_t* bitmap_idx); - -// Starts at idx, and wraps around to search in all `bitmap_fields` fields. -// For now, `count` can be at most MI_BITMAP_FIELD_BITS and will never cross fields. -bool _mi_bitmap_try_find_from_claim(mi_bitmap_t bitmap, const size_t bitmap_fields, const size_t start_field_idx, const size_t count, mi_bitmap_index_t* bitmap_idx); - -// Like _mi_bitmap_try_find_from_claim but with an extra predicate that must be fulfilled -typedef bool (mi_cdecl *mi_bitmap_pred_fun_t)(mi_bitmap_index_t bitmap_idx, void* pred_arg); -bool _mi_bitmap_try_find_from_claim_pred(mi_bitmap_t bitmap, const size_t bitmap_fields, const size_t start_field_idx, const size_t count, mi_bitmap_pred_fun_t pred_fun, void* pred_arg, mi_bitmap_index_t* bitmap_idx); - -// Set `count` bits at `bitmap_idx` to 0 atomically -// Returns `true` if all `count` bits were 1 previously. -bool _mi_bitmap_unclaim(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx); - -// Try to set `count` bits at `bitmap_idx` from 0 to 1 atomically. -// Returns `true` if successful when all previous `count` bits were 0. -bool _mi_bitmap_try_claim(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx); - -// Set `count` bits at `bitmap_idx` to 1 atomically -// Returns `true` if all `count` bits were 0 previously. `any_zero` is `true` if there was at least one zero bit. -bool _mi_bitmap_claim(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx, bool* any_zero); - -bool _mi_bitmap_is_claimed(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx); -bool _mi_bitmap_is_any_claimed(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx); - - -//-------------------------------------------------------------------------- -// the `_across` functions work on bitmaps where sequences can cross over -// between the fields. This is used in arena allocation -//-------------------------------------------------------------------------- - -// Find `count` bits of zeros and set them to 1 atomically; returns `true` on success. -// Starts at idx, and wraps around to search in all `bitmap_fields` fields. -bool _mi_bitmap_try_find_from_claim_across(mi_bitmap_t bitmap, const size_t bitmap_fields, const size_t start_field_idx, const size_t count, mi_bitmap_index_t* bitmap_idx); - -// Set `count` bits at `bitmap_idx` to 0 atomically -// Returns `true` if all `count` bits were 1 previously. -bool _mi_bitmap_unclaim_across(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx); - -// Set `count` bits at `bitmap_idx` to 1 atomically -// Returns `true` if all `count` bits were 0 previously. `any_zero` is `true` if there was at least one zero bit. -bool _mi_bitmap_claim_across(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx, bool* pany_zero); - -bool _mi_bitmap_is_claimed_across(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx); -bool _mi_bitmap_is_any_claimed_across(mi_bitmap_t bitmap, size_t bitmap_fields, size_t count, mi_bitmap_index_t bitmap_idx); - -#endif diff --git a/Objects/mimalloc/heap.c b/Objects/mimalloc/heap.c deleted file mode 100644 index 5fbfb82baa02040..000000000000000 --- a/Objects/mimalloc/heap.c +++ /dev/null @@ -1,696 +0,0 @@ -/*---------------------------------------------------------------------------- -Copyright (c) 2018-2021, Microsoft Research, Daan Leijen -This is free software; you can redistribute it and/or modify it under the -terms of the MIT license. A copy of the license can be found in the file -"LICENSE" at the root of this distribution. ------------------------------------------------------------------------------*/ - -#include "mimalloc.h" -#include "mimalloc/internal.h" -#include "mimalloc/atomic.h" -#include "mimalloc/prim.h" // mi_prim_get_default_heap - -#include // memset, memcpy - -#if defined(_MSC_VER) && (_MSC_VER < 1920) -#pragma warning(disable:4204) // non-constant aggregate initializer -#endif - -/* ----------------------------------------------------------- - Helpers ------------------------------------------------------------ */ - -// return `true` if ok, `false` to break -typedef bool (heap_page_visitor_fun)(mi_heap_t* heap, mi_page_queue_t* pq, mi_page_t* page, void* arg1, void* arg2); - -// Visit all pages in a heap; returns `false` if break was called. -static bool mi_heap_visit_pages(mi_heap_t* heap, heap_page_visitor_fun* fn, void* arg1, void* arg2) -{ - if (heap==NULL || heap->page_count==0) return true; - - // visit all pages - #if MI_DEBUG>1 - size_t total = heap->page_count; - size_t count = 0; - #endif - - for (size_t i = 0; i <= MI_BIN_FULL; i++) { - mi_page_queue_t* pq = &heap->pages[i]; - mi_page_t* page = pq->first; - while(page != NULL) { - mi_page_t* next = page->next; // save next in case the page gets removed from the queue - mi_assert_internal(mi_page_heap(page) == heap); - #if MI_DEBUG>1 - count++; - #endif - if (!fn(heap, pq, page, arg1, arg2)) return false; - page = next; // and continue - } - } - mi_assert_internal(count == total); - return true; -} - - -#if MI_DEBUG>=2 -static bool mi_heap_page_is_valid(mi_heap_t* heap, mi_page_queue_t* pq, mi_page_t* page, void* arg1, void* arg2) { - MI_UNUSED(arg1); - MI_UNUSED(arg2); - MI_UNUSED(pq); - mi_assert_internal(mi_page_heap(page) == heap); - mi_segment_t* segment = _mi_page_segment(page); - mi_assert_internal(segment->thread_id == heap->thread_id); - mi_assert_expensive(_mi_page_is_valid(page)); - return true; -} -#endif -#if MI_DEBUG>=3 -static bool mi_heap_is_valid(mi_heap_t* heap) { - mi_assert_internal(heap!=NULL); - mi_heap_visit_pages(heap, &mi_heap_page_is_valid, NULL, NULL); - return true; -} -#endif - - - - -/* ----------------------------------------------------------- - "Collect" pages by migrating `local_free` and `thread_free` - lists and freeing empty pages. This is done when a thread - stops (and in that case abandons pages if there are still - blocks alive) ------------------------------------------------------------ */ - -typedef enum mi_collect_e { - MI_NORMAL, - MI_FORCE, - MI_ABANDON -} mi_collect_t; - - -static bool mi_heap_page_collect(mi_heap_t* heap, mi_page_queue_t* pq, mi_page_t* page, void* arg_collect, void* arg2 ) { - MI_UNUSED(arg2); - MI_UNUSED(heap); - mi_assert_internal(mi_heap_page_is_valid(heap, pq, page, NULL, NULL)); - mi_collect_t collect = *((mi_collect_t*)arg_collect); - _mi_page_free_collect(page, collect >= MI_FORCE); - if (mi_page_all_free(page)) { - // no more used blocks, free the page. - // note: this will free retired pages as well. - bool freed = _PyMem_mi_page_maybe_free(page, pq, collect >= MI_FORCE); - if (!freed && collect == MI_ABANDON) { - // _PyMem_mi_page_maybe_free may have moved the page to a different - // page queue, so we need to re-fetch the correct queue. - uint8_t bin = (mi_page_is_in_full(page) ? MI_BIN_FULL : _mi_bin(page->xblock_size)); - _mi_page_abandon(page, &heap->pages[bin]); - } - } - else if (collect == MI_ABANDON) { - // still used blocks but the thread is done; abandon the page - _mi_page_abandon(page, pq); - } - return true; // don't break -} - -static bool mi_heap_page_never_delayed_free(mi_heap_t* heap, mi_page_queue_t* pq, mi_page_t* page, void* arg1, void* arg2) { - MI_UNUSED(arg1); - MI_UNUSED(arg2); - MI_UNUSED(heap); - MI_UNUSED(pq); - _mi_page_use_delayed_free(page, MI_NEVER_DELAYED_FREE, false); - return true; // don't break -} - -static void mi_heap_collect_ex(mi_heap_t* heap, mi_collect_t collect) -{ - if (heap==NULL || !mi_heap_is_initialized(heap)) return; - - const bool force = collect >= MI_FORCE; - _mi_deferred_free(heap, force); - - // gh-112532: we may be called from a thread that is not the owner of the heap - bool is_main_thread = _mi_is_main_thread() && heap->thread_id == _mi_thread_id(); - - // note: never reclaim on collect but leave it to threads that need storage to reclaim - const bool force_main = - #ifdef NDEBUG - collect == MI_FORCE - #else - collect >= MI_FORCE - #endif - && is_main_thread && mi_heap_is_backing(heap) && !heap->no_reclaim; - - if (force_main) { - // the main thread is abandoned (end-of-program), try to reclaim all abandoned segments. - // if all memory is freed by now, all segments should be freed. - _mi_abandoned_reclaim_all(heap, &heap->tld->segments); - } - - // if abandoning, mark all pages to no longer add to delayed_free - if (collect == MI_ABANDON) { - mi_heap_visit_pages(heap, &mi_heap_page_never_delayed_free, NULL, NULL); - } - - // free all current thread delayed blocks. - // (if abandoning, after this there are no more thread-delayed references into the pages.) - _mi_heap_delayed_free_all(heap); - - // collect retired pages - _mi_heap_collect_retired(heap, force); - - // free pages that were delayed with QSBR - _PyMem_mi_heap_collect_qsbr(heap); - - // collect all pages owned by this thread - mi_heap_visit_pages(heap, &mi_heap_page_collect, &collect, NULL); - mi_assert_internal( collect != MI_ABANDON || mi_atomic_load_ptr_acquire(mi_block_t,&heap->thread_delayed_free) == NULL ); - - // collect abandoned segments (in particular, purge expired parts of segments in the abandoned segment list) - // note: forced purge can be quite expensive if many threads are created/destroyed so we do not force on abandonment - _mi_abandoned_collect(heap, collect == MI_FORCE /* force? */, &heap->tld->segments); - - // collect segment local caches - if (force) { - _mi_segment_thread_collect(&heap->tld->segments); - } - - // collect regions on program-exit (or shared library unload) - if (force && is_main_thread && mi_heap_is_backing(heap)) { - _mi_thread_data_collect(); // collect thread data cache - _mi_arena_collect(true /* force purge */, &heap->tld->stats); - } -} - -void _mi_heap_collect_abandon(mi_heap_t* heap) { - mi_heap_collect_ex(heap, MI_ABANDON); -} - -void mi_heap_collect(mi_heap_t* heap, bool force) mi_attr_noexcept { - mi_heap_collect_ex(heap, (force ? MI_FORCE : MI_NORMAL)); -} - -void mi_collect(bool force) mi_attr_noexcept { - mi_heap_collect(mi_prim_get_default_heap(), force); -} - - -/* ----------------------------------------------------------- - Heap new ------------------------------------------------------------ */ - -mi_heap_t* mi_heap_get_default(void) { - mi_thread_init(); - return mi_prim_get_default_heap(); -} - -static bool mi_heap_is_default(const mi_heap_t* heap) { - return (heap == mi_prim_get_default_heap()); -} - - -mi_heap_t* mi_heap_get_backing(void) { - mi_heap_t* heap = mi_heap_get_default(); - mi_assert_internal(heap!=NULL); - mi_heap_t* bheap = heap->tld->heap_backing; - mi_assert_internal(bheap!=NULL); - mi_assert_internal(bheap->thread_id == _mi_thread_id()); - return bheap; -} - -void _mi_heap_init_ex(mi_heap_t* heap, mi_tld_t* tld, mi_arena_id_t arena_id, bool no_reclaim, uint8_t tag) -{ - _mi_memcpy_aligned(heap, &_mi_heap_empty, sizeof(mi_heap_t)); - heap->tld = tld; - heap->thread_id = _mi_thread_id(); - heap->arena_id = arena_id; - if (heap == tld->heap_backing) { - _mi_random_init(&heap->random); - } - else { - _mi_random_split(&tld->heap_backing->random, &heap->random); - } - heap->cookie = _mi_heap_random_next(heap) | 1; - heap->keys[0] = _mi_heap_random_next(heap); - heap->keys[1] = _mi_heap_random_next(heap); - heap->no_reclaim = no_reclaim; - heap->tag = tag; - // push on the thread local heaps list - heap->next = heap->tld->heaps; - heap->tld->heaps = heap; -} - -mi_decl_nodiscard mi_heap_t* mi_heap_new_in_arena(mi_arena_id_t arena_id) { - mi_heap_t* bheap = mi_heap_get_backing(); - mi_heap_t* heap = mi_heap_malloc_tp(bheap, mi_heap_t); // todo: OS allocate in secure mode? - if (heap == NULL) return NULL; - // don't reclaim abandoned pages or otherwise destroy is unsafe - _mi_heap_init_ex(heap, bheap->tld, arena_id, true, 0); - return heap; -} - -mi_decl_nodiscard mi_heap_t* mi_heap_new(void) { - return mi_heap_new_in_arena(_mi_arena_id_none()); -} - -bool _mi_heap_memid_is_suitable(mi_heap_t* heap, mi_memid_t memid) { - return _mi_arena_memid_is_suitable(memid, heap->arena_id); -} - -uintptr_t _mi_heap_random_next(mi_heap_t* heap) { - return _mi_random_next(&heap->random); -} - -// zero out the page queues -static void mi_heap_reset_pages(mi_heap_t* heap) { - mi_assert_internal(heap != NULL); - mi_assert_internal(mi_heap_is_initialized(heap)); - // TODO: copy full empty heap instead? - memset(&heap->pages_free_direct, 0, sizeof(heap->pages_free_direct)); - _mi_memcpy_aligned(&heap->pages, &_mi_heap_empty.pages, sizeof(heap->pages)); - heap->thread_delayed_free = NULL; - heap->page_count = 0; -} - -// called from `mi_heap_destroy` and `mi_heap_delete` to free the internal heap resources. -static void mi_heap_free(mi_heap_t* heap) { - mi_assert(heap != NULL); - mi_assert_internal(mi_heap_is_initialized(heap)); - if (heap==NULL || !mi_heap_is_initialized(heap)) return; - if (mi_heap_is_backing(heap)) return; // dont free the backing heap - - // reset default - if (mi_heap_is_default(heap)) { - _mi_heap_set_default_direct(heap->tld->heap_backing); - } - - // remove ourselves from the thread local heaps list - // linear search but we expect the number of heaps to be relatively small - mi_heap_t* prev = NULL; - mi_heap_t* curr = heap->tld->heaps; - while (curr != heap && curr != NULL) { - prev = curr; - curr = curr->next; - } - mi_assert_internal(curr == heap); - if (curr == heap) { - if (prev != NULL) { prev->next = heap->next; } - else { heap->tld->heaps = heap->next; } - } - mi_assert_internal(heap->tld->heaps != NULL); - - // and free the used memory - mi_free(heap); -} - - -/* ----------------------------------------------------------- - Heap destroy ------------------------------------------------------------ */ - -static bool _mi_heap_page_destroy(mi_heap_t* heap, mi_page_queue_t* pq, mi_page_t* page, void* arg1, void* arg2) { - MI_UNUSED(arg1); - MI_UNUSED(arg2); - MI_UNUSED(heap); - MI_UNUSED(pq); - - // ensure no more thread_delayed_free will be added - _mi_page_use_delayed_free(page, MI_NEVER_DELAYED_FREE, false); - - // stats - const size_t bsize = mi_page_block_size(page); - if (bsize > MI_MEDIUM_OBJ_SIZE_MAX) { - if (bsize <= MI_LARGE_OBJ_SIZE_MAX) { - mi_heap_stat_decrease(heap, large, bsize); - } - else { - mi_heap_stat_decrease(heap, huge, bsize); - } - } -#if (MI_STAT) - _mi_page_free_collect(page, false); // update used count - const size_t inuse = page->used; - if (bsize <= MI_LARGE_OBJ_SIZE_MAX) { - mi_heap_stat_decrease(heap, normal, bsize * inuse); -#if (MI_STAT>1) - mi_heap_stat_decrease(heap, normal_bins[_mi_bin(bsize)], inuse); -#endif - } - mi_heap_stat_decrease(heap, malloc, bsize * inuse); // todo: off for aligned blocks... -#endif - - /// pretend it is all free now - mi_assert_internal(mi_page_thread_free(page) == NULL); - page->used = 0; - - // and free the page - // mi_page_free(page,false); - page->next = NULL; - page->prev = NULL; - _mi_segment_page_free(page,false /* no force? */, &heap->tld->segments); - - return true; // keep going -} - -void _mi_heap_destroy_pages(mi_heap_t* heap) { - mi_heap_visit_pages(heap, &_mi_heap_page_destroy, NULL, NULL); - mi_heap_reset_pages(heap); -} - -#if MI_TRACK_HEAP_DESTROY -static bool mi_cdecl mi_heap_track_block_free(const mi_heap_t* heap, const mi_heap_area_t* area, void* block, size_t block_size, void* arg) { - MI_UNUSED(heap); MI_UNUSED(area); MI_UNUSED(arg); MI_UNUSED(block_size); - mi_track_free_size(block,mi_usable_size(block)); - return true; -} -#endif - -void mi_heap_destroy(mi_heap_t* heap) { - mi_assert(heap != NULL); - mi_assert(mi_heap_is_initialized(heap)); - mi_assert(heap->no_reclaim); - mi_assert_expensive(mi_heap_is_valid(heap)); - if (heap==NULL || !mi_heap_is_initialized(heap)) return; - if (!heap->no_reclaim) { - // don't free in case it may contain reclaimed pages - mi_heap_delete(heap); - } - else { - // track all blocks as freed - #if MI_TRACK_HEAP_DESTROY - mi_heap_visit_blocks(heap, true, mi_heap_track_block_free, NULL); - #endif - // free all pages - _mi_heap_destroy_pages(heap); - mi_heap_free(heap); - } -} - -// forcefully destroy all heaps in the current thread -void _mi_heap_unsafe_destroy_all(void) { - mi_heap_t* bheap = mi_heap_get_backing(); - mi_heap_t* curr = bheap->tld->heaps; - while (curr != NULL) { - mi_heap_t* next = curr->next; - if (curr->no_reclaim) { - mi_heap_destroy(curr); - } - else { - _mi_heap_destroy_pages(curr); - } - curr = next; - } -} - -/* ----------------------------------------------------------- - Safe Heap delete ------------------------------------------------------------ */ - -// Transfer the pages from one heap to the other -static void mi_heap_absorb(mi_heap_t* heap, mi_heap_t* from) { - mi_assert_internal(heap!=NULL); - if (from==NULL || from->page_count == 0) return; - - // reduce the size of the delayed frees - _mi_heap_delayed_free_partial(from); - - // transfer all pages by appending the queues; this will set a new heap field - // so threads may do delayed frees in either heap for a while. - // note: appending waits for each page to not be in the `MI_DELAYED_FREEING` state - // so after this only the new heap will get delayed frees - for (size_t i = 0; i <= MI_BIN_FULL; i++) { - mi_page_queue_t* pq = &heap->pages[i]; - mi_page_queue_t* append = &from->pages[i]; - size_t pcount = _mi_page_queue_append(heap, pq, append); - heap->page_count += pcount; - from->page_count -= pcount; - } - mi_assert_internal(from->page_count == 0); - - // and do outstanding delayed frees in the `from` heap - // note: be careful here as the `heap` field in all those pages no longer point to `from`, - // turns out to be ok as `_mi_heap_delayed_free` only visits the list and calls a - // the regular `_mi_free_delayed_block` which is safe. - _mi_heap_delayed_free_all(from); - #if !defined(_MSC_VER) || (_MSC_VER > 1900) // somehow the following line gives an error in VS2015, issue #353 - mi_assert_internal(mi_atomic_load_ptr_relaxed(mi_block_t,&from->thread_delayed_free) == NULL); - #endif - - // and reset the `from` heap - mi_heap_reset_pages(from); -} - -// Safe delete a heap without freeing any still allocated blocks in that heap. -void mi_heap_delete(mi_heap_t* heap) -{ - mi_assert(heap != NULL); - mi_assert(mi_heap_is_initialized(heap)); - mi_assert_expensive(mi_heap_is_valid(heap)); - if (heap==NULL || !mi_heap_is_initialized(heap)) return; - - if (!mi_heap_is_backing(heap)) { - // transfer still used pages to the backing heap - mi_heap_absorb(heap->tld->heap_backing, heap); - } - else { - // the backing heap abandons its pages - _mi_heap_collect_abandon(heap); - } - mi_assert_internal(heap->page_count==0); - mi_heap_free(heap); -} - -mi_heap_t* mi_heap_set_default(mi_heap_t* heap) { - mi_assert(heap != NULL); - mi_assert(mi_heap_is_initialized(heap)); - if (heap==NULL || !mi_heap_is_initialized(heap)) return NULL; - mi_assert_expensive(mi_heap_is_valid(heap)); - mi_heap_t* old = mi_prim_get_default_heap(); - _mi_heap_set_default_direct(heap); - return old; -} - - - - -/* ----------------------------------------------------------- - Analysis ------------------------------------------------------------ */ - -// static since it is not thread safe to access heaps from other threads. -static mi_heap_t* mi_heap_of_block(const void* p) { - if (p == NULL) return NULL; - mi_segment_t* segment = _mi_ptr_segment(p); - bool valid = (_mi_ptr_cookie(segment) == segment->cookie); - mi_assert_internal(valid); - if mi_unlikely(!valid) return NULL; - return mi_page_heap(_mi_segment_page_of(segment,p)); -} - -bool mi_heap_contains_block(mi_heap_t* heap, const void* p) { - mi_assert(heap != NULL); - if (heap==NULL || !mi_heap_is_initialized(heap)) return false; - return (heap == mi_heap_of_block(p)); -} - - -static bool mi_heap_page_check_owned(mi_heap_t* heap, mi_page_queue_t* pq, mi_page_t* page, void* p, void* vfound) { - MI_UNUSED(heap); - MI_UNUSED(pq); - bool* found = (bool*)vfound; - mi_segment_t* segment = _mi_page_segment(page); - void* start = _mi_page_start(segment, page, NULL); - void* end = (uint8_t*)start + (page->capacity * mi_page_block_size(page)); - *found = (p >= start && p < end); - return (!*found); // continue if not found -} - -bool mi_heap_check_owned(mi_heap_t* heap, const void* p) { - mi_assert(heap != NULL); - if (heap==NULL || !mi_heap_is_initialized(heap)) return false; - if (((uintptr_t)p & (MI_INTPTR_SIZE - 1)) != 0) return false; // only aligned pointers - bool found = false; - mi_heap_visit_pages(heap, &mi_heap_page_check_owned, (void*)p, &found); - return found; -} - -bool mi_check_owned(const void* p) { - return mi_heap_check_owned(mi_prim_get_default_heap(), p); -} - -/* ----------------------------------------------------------- - Visit all heap blocks and areas - Todo: enable visiting abandoned pages, and - enable visiting all blocks of all heaps across threads ------------------------------------------------------------ */ - -// Separate struct to keep `mi_page_t` out of the public interface -typedef struct mi_heap_area_ex_s { - mi_heap_area_t area; - mi_page_t* page; -} mi_heap_area_ex_t; - -static void mi_fast_divisor(size_t divisor, size_t* magic, size_t* shift) { - mi_assert_internal(divisor > 0 && divisor <= UINT32_MAX); - *shift = MI_INTPTR_BITS - mi_clz(divisor - 1); - *magic = (size_t)(((1ULL << 32) * ((1ULL << *shift) - divisor)) / divisor + 1); -} - -static size_t mi_fast_divide(size_t n, size_t magic, size_t shift) { - mi_assert_internal(n <= UINT32_MAX); - return ((((uint64_t) n * magic) >> 32) + n) >> shift; -} - -bool _mi_heap_area_visit_blocks(const mi_heap_area_t* area, mi_page_t *page, mi_block_visit_fun* visitor, void* arg) { - mi_assert(area != NULL); - if (area==NULL) return true; - mi_assert(page != NULL); - if (page == NULL) return true; - - mi_assert_internal(page->local_free == NULL); - if (page->used == 0) return true; - - const size_t bsize = mi_page_block_size(page); - const size_t ubsize = mi_page_usable_block_size(page); // without padding - size_t psize; - uint8_t* pstart = _mi_page_start(_mi_page_segment(page), page, &psize); - mi_heap_t* heap = mi_page_heap(page); - - if (page->capacity == 1) { - // optimize page with one block - mi_assert_internal(page->used == 1 && page->free == NULL); - return visitor(heap, area, pstart, ubsize, arg); - } - - if (page->used == page->capacity) { - // optimize full pages - uint8_t* block = pstart; - for (size_t i = 0; i < page->capacity; i++) { - if (!visitor(heap, area, block, ubsize, arg)) return false; - block += bsize; - } - return true; - } - - // create a bitmap of free blocks. - #define MI_MAX_BLOCKS (MI_SMALL_PAGE_SIZE / sizeof(void*)) - uintptr_t free_map[MI_MAX_BLOCKS / MI_INTPTR_BITS]; - size_t bmapsize = (page->capacity + MI_INTPTR_BITS - 1) / MI_INTPTR_BITS; - memset(free_map, 0, bmapsize * sizeof(uintptr_t)); - - if (page->capacity % MI_INTPTR_BITS != 0) { - size_t shift = (page->capacity % MI_INTPTR_BITS); - uintptr_t mask = (UINTPTR_MAX << shift); - free_map[bmapsize-1] = mask; - } - - // fast repeated division by the block size - size_t magic, shift; - mi_fast_divisor(bsize, &magic, &shift); - - #if MI_DEBUG>1 - size_t free_count = 0; - #endif - for (mi_block_t* block = page->free; block != NULL; block = mi_block_next(page,block)) { - #if MI_DEBUG>1 - free_count++; - #endif - mi_assert_internal((uint8_t*)block >= pstart && (uint8_t*)block < (pstart + psize)); - size_t offset = (uint8_t*)block - pstart; - mi_assert_internal(offset % bsize == 0); - size_t blockidx = mi_fast_divide(offset, magic, shift); - mi_assert_internal(blockidx == offset / bsize); - mi_assert_internal(blockidx < MI_MAX_BLOCKS); - size_t bitidx = (blockidx / MI_INTPTR_BITS); - size_t bit = blockidx - (bitidx * MI_INTPTR_BITS); - free_map[bitidx] |= ((uintptr_t)1 << bit); - } - mi_assert_internal(page->capacity == (free_count + page->used)); - - // walk through all blocks skipping the free ones - #if MI_DEBUG>1 - size_t used_count = 0; - #endif - uint8_t* block = pstart; - for (size_t i = 0; i < bmapsize; i++) { - if (free_map[i] == 0) { - // every block is in use - for (size_t j = 0; j < MI_INTPTR_BITS; j++) { - #if MI_DEBUG>1 - used_count++; - #endif - if (!visitor(heap, area, block, ubsize, arg)) return false; - block += bsize; - } - } - else { - uintptr_t m = ~free_map[i]; - while (m) { - #if MI_DEBUG>1 - used_count++; - #endif - size_t bitidx = mi_ctz(m); - if (!visitor(heap, area, block + (bitidx * bsize), ubsize, arg)) return false; - m &= m - 1; - } - block += bsize * MI_INTPTR_BITS; - } - } - mi_assert_internal(page->used == used_count); - return true; -} - -typedef bool (mi_heap_area_visit_fun)(const mi_heap_t* heap, const mi_heap_area_ex_t* area, void* arg); - -void _mi_heap_area_init(mi_heap_area_t* area, mi_page_t* page) { - _mi_page_free_collect(page,true); - const size_t bsize = mi_page_block_size(page); - const size_t ubsize = mi_page_usable_block_size(page); - area->reserved = page->reserved * bsize; - area->committed = page->capacity * bsize; - area->blocks = _mi_page_start(_mi_page_segment(page), page, NULL); - area->used = page->used; // number of blocks in use (#553) - area->block_size = ubsize; - area->full_block_size = bsize; -} - -static bool mi_heap_visit_areas_page(mi_heap_t* heap, mi_page_queue_t* pq, mi_page_t* page, void* vfun, void* arg) { - MI_UNUSED(heap); - MI_UNUSED(pq); - mi_heap_area_visit_fun* fun = (mi_heap_area_visit_fun*)vfun; - mi_heap_area_ex_t xarea; - xarea.page = page; - _mi_heap_area_init(&xarea.area, page); - return fun(heap, &xarea, arg); -} - -// Visit all heap pages as areas -static bool mi_heap_visit_areas(const mi_heap_t* heap, mi_heap_area_visit_fun* visitor, void* arg) { - if (visitor == NULL) return false; - return mi_heap_visit_pages((mi_heap_t*)heap, &mi_heap_visit_areas_page, (void*)(visitor), arg); // note: function pointer to void* :-{ -} - -// Just to pass arguments -typedef struct mi_visit_blocks_args_s { - bool visit_blocks; - mi_block_visit_fun* visitor; - void* arg; -} mi_visit_blocks_args_t; - -static bool mi_heap_area_visitor(const mi_heap_t* heap, const mi_heap_area_ex_t* xarea, void* arg) { - mi_visit_blocks_args_t* args = (mi_visit_blocks_args_t*)arg; - if (!args->visitor(heap, &xarea->area, NULL, xarea->area.block_size, args->arg)) return false; - if (args->visit_blocks) { - return _mi_heap_area_visit_blocks(&xarea->area, xarea->page, args->visitor, args->arg); - } - else { - return true; - } -} - -// Visit all blocks in a heap -bool mi_heap_visit_blocks(const mi_heap_t* heap, bool visit_blocks, mi_block_visit_fun* visitor, void* arg) { - mi_visit_blocks_args_t args = { visit_blocks, visitor, arg }; - _mi_heap_delayed_free_partial((mi_heap_t *)heap); - return mi_heap_visit_areas(heap, &mi_heap_area_visitor, &args); -} diff --git a/Objects/mimalloc/heap/heap.c b/Objects/mimalloc/heap/heap.c new file mode 100644 index 000000000000000..e49d778455c6dd6 --- /dev/null +++ b/Objects/mimalloc/heap/heap.c @@ -0,0 +1,351 @@ +/*---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +#include "mimalloc.h" +#include "mimalloc/prim/prim-tls.h" // _mi_heap_theap +#include "mimalloc/util/stats.h" +#include "mimalloc/alloc/free.h" // mi_free_subproc_safe +#include "mimalloc/alloc/page.h" +#include "mimalloc/arena/arena-page.h" +#include "mimalloc/arena/arena.h" +#include "mimalloc/heap/heap.h" + + +/* ----------------------------------------------------------- + Heap's +----------------------------------------------------------- */ + +mi_theap_t* mi_heap_theap(mi_heap_t* heap) { + return _mi_heap_theap(heap); // in prim.h +} + +void mi_heap_set_numa_affinity(mi_heap_t* heap, int numa_node) { + if (heap==NULL) { heap = mi_heap_main(); } + heap->numa_node = (numa_node < 0 ? -1 : numa_node % _mi_os_numa_node_count()); +} + +void mi_heap_set_debug_fill_preserve(mi_heap_t* heap, size_t preserve) { + if (heap==NULL) { heap = mi_heap_main(); } + if (preserve > MI_MiB) { preserve = MI_MiB; } // fills are limited to 1 MiB anyway + mi_atomic_store_relaxed(&heap->debug_fill_preserve, preserve); +} + +void mi_heap_stats_merge_to_subproc(mi_heap_t* heap) { + if (heap==NULL) { heap = mi_heap_main(); } + _mi_stats_merge_into(&heap->subproc->stats, &heap->stats); +} + +void mi_heap_stats_merge_to_main(mi_heap_t* heap) { + if (heap==NULL) return; + _mi_stats_merge_into(&mi_heap_get_heap_main(heap)->stats, &heap->stats); +} + +bool _mi_heap_theap_set(mi_heap_t* heap, mi_theap_t* theap) { + mi_assert_internal((uintptr_t)theap == 1 || mi_theap_heap(theap)==heap); + mi_assert_internal(!_mi_is_empty_theap(theap)); + mi_assert_internal(heap->theap != 0); + return _mi_thread_local_set(heap->theap,theap); +} + +// mi_theap_t* _mi_heap_theap_get_peek(const mi_heap_t* heap) { +// mi_theap_t* theap; +// mi_assert_internal(heap->theap != 0); +// if mi_likely(heap->theap!=0) { // paranoia +// theap = (mi_theap_t*)_mi_thread_local_get(heap->theap); +// } +// else { +// _mi_error_message(EFAULT, "no thread-local reserved for heap (%p)\n", heap); +// return NULL; +// } +// mi_assert_internal(!_mi_is_empty_theap(theap)); +// mi_assert_internal(theap->heap == heap); // this goes wrong if using main heaps across subprocesses (as all share the same key) +// return theap; +// } + + +static mi_decl_noinline mi_theap_t* mi_heap_init_theap(const mi_heap_t* const_heap) +{ + mi_heap_t* heap = (mi_heap_t*)const_heap; + mi_assert_internal(heap!=NULL); + + // initialize thread first in case this is the main heap + // (which may allocate the default theap already for the main heap) + if (!_mi_thread_is_initialized()) { + mi_thread_init(); + } + + // get the thread local theap + mi_theap_t* theap = (mi_theap_t*)_mi_thread_local_get(heap->theap); + + // create a fresh theap? + if (theap==NULL) { + // allocate a fresh theap + theap = _mi_theap_create(heap, mi_theap_get_default()->tld); // sets the theap thread local + if (theap==NULL) { + _mi_error_message(EFAULT, "unable to allocate memory for a thread local heap\n"); + return NULL; + } + _mi_heap_theap_set(heap, theap); + mi_assert_internal(theap == (mi_theap_t*)_mi_thread_local_get(heap->theap)); + } + return theap; +} + + +// get (and possibly create) the theap belonging to a heap +mi_decl_cold mi_theap_t* _mi_heap_theap_get_or_init(const mi_heap_t* heap) +{ + mi_assert_internal(heap->theap != 0); + mi_theap_t* theap = (mi_theap_t*)_mi_thread_local_get(heap->theap); + if mi_unlikely(theap==NULL) { + theap = mi_heap_init_theap(heap); + if (theap==NULL) { return (mi_theap_t*)&_mi_theap_empty_wrong; } // this will return NULL from alloc/alloc-generic.c:_mi_malloc_generic + } + _mi_theap_cached_set(theap); + return theap; +} + +void _mi_heap_init(mi_heap_t* heap, mi_thread_local_t theap_slot, mi_subproc_t* subproc, mi_arena_id_t exclusive_arena_id, bool use_qsbr) +{ + // init fields + heap->theap = theap_slot; + heap->subproc = subproc; + heap->heap_seq = mi_atomic_increment_relaxed(&subproc->heap_total_count); + heap->exclusive_arena = _mi_arena_from_id(exclusive_arena_id); + heap->reclaim_on_free = mi_option_get(mi_option_page_reclaim_on_free); + heap->numa_node = -1; // no initial affinity + #if MI_QSBR + heap->qsbr_enabled = use_qsbr; + if (use_qsbr) { + if (heap->reclaim_on_free > 0) { heap->reclaim_on_free = 0; } + mi_heap_qsbr_init(&heap->qsbr); + } + #else + mi_assert_internal(!use_qsbr); + MI_UNUSED(use_qsbr); + #endif + heap->profiler = mi_atomic_load_ptr_acquire(mi_profiler_t,&subproc->profiler); + mi_stats_header_init(&heap->stats); + mi_lock_init(&heap->theaps_lock); + mi_lock_init(&heap->os_abandoned_pages_lock); + mi_lock_init(&heap->arena_pages_lock); + + // push onto the subproc heaps + mi_lock(&heap->subproc->heaps_lock) { + mi_heap_t* head = heap->subproc->heaps; + heap->prev = NULL; + heap->next = head; + if (head!=NULL) { head->prev = heap; } + heap->subproc->heaps = heap; + } + mi_atomic_increment_relaxed(&subproc->heap_count); + mi_subproc_stat_increase(subproc, heaps, 1); + mi_assert_internal(mi_is_heap_main(heap) ? heap->theap == mi_thread_local_key_fast : heap->theap != 0); +} + +mi_heap_t* _mi_heap_new_for_subproc(mi_subproc_t* subproc, mi_arena_id_t exclusive_arena_id, bool is_main_heap, bool use_qsbr) { + mi_assert_internal(is_main_heap ? (subproc->heap_main == NULL && subproc->parent != NULL) : subproc->heap_main != NULL); + // heap data is allocated in the current subproc + mi_heap_t* const heap_main = (is_main_heap ? subproc->parent->heap_main : subproc->heap_main); + // todo: allocate heap data in the exclusive arena ? + mi_heap_t* const heap = (mi_heap_t*)mi_heap_zalloc( heap_main, sizeof(mi_heap_t) ); + if (heap==NULL) return NULL; + // reserve a thread local slot for this heap (see also issue #1230) + mi_thread_local_t theap_slot = (is_main_heap ? mi_thread_local_key_fast : _mi_thread_local_create()); + if (theap_slot == 0) { + _mi_error_message(EFAULT, "unable to dynamically create a thread local for a heap\n"); + mi_free(heap); + return NULL; + } + if (is_main_heap) { + mi_assert_internal(subproc->heap_main == NULL); + subproc->heap_main = heap; + } + _mi_heap_init(heap, theap_slot, subproc, exclusive_arena_id, use_qsbr); + return heap; +} + +mi_heap_t* mi_heap_new_in_arena(mi_arena_id_t exclusive_arena_id) { + // `mi_heap_new` may be the very first mimalloc call in a process, in which case the + // main heap does not exist yet and `_mi_heap_new_for_subproc` would allocate from a NULL `subproc->heap_main`. + mi_thread_init(); + return _mi_heap_new_for_subproc(_mi_subproc(), exclusive_arena_id, false, false); +} + +mi_heap_t* mi_heap_new(void) { + return mi_heap_new_in_arena(0); +} + +mi_heap_t* mi_heap_new_qsbr(void) { + #if MI_QSBR + mi_thread_init(); + return _mi_heap_new_for_subproc(_mi_subproc(), 0, false, true); + #else + _mi_error_message(ENOSYS, "mi_heap_new_qsbr requires a build with QSBR support\n"); + return NULL; + #endif +} + +// free all theaps belonging to this heap (without deleting their pages as we do this arena wise for efficiency) +static void mi_heap_free_theaps(mi_heap_t* heap) { + // This can run concurrently with a thread that terminates (see `heap/init.c:mi_thread_theaps_done`), + // and we need to ensure we free theaps atomically. + + // We first detach our theaps list from any thread local lists + _mi_heap_detach_theaps(heap); + + // Now we can safely free the theaps + mi_lock(&heap->theaps_lock) { // paranoia + mi_theap_t* theap = heap->theaps; + heap->theaps = NULL; + while(theap != NULL) { + mi_theap_t* next = theap->hnext; + theap->hnext = NULL; + theap->hprev = NULL; + mi_assert_internal(theap->tld==NULL); + // abandon any owned pages into the heap-wide registries so heap delete/destroy can visit them + _mi_theap_abandon_owned_pages(theap); + // clear the heap so a stale `_mi_theap_cached` entry in another thread no longer matches + // (a new heap can be allocated at the same address and would otherwise pick up this freed theap) + mi_atomic_store_ptr_release(mi_heap_t, &theap->heap, NULL); + // merge stats into the owning heap stats + _mi_stats_merge_into(&heap->stats, &theap->stats); + // and free + _mi_theap_decref(theap); // a cached entry can still point to the theap + theap = next; + } + } + + // set the theap thread local to NULL (so _mi_page_associated_theap does not read from a freed theap (through delete pages -> page_update_stats)) + if (!mi_is_process_heap_main(heap)) { + _mi_thread_local_free(heap->theap); + heap->theap = 0; + } +} + +// free the heap resources (assuming the pages are already moved/destroyed, and all theaps have been freed) +static void mi_heap_free(mi_heap_t* heap, bool acquire_heaps_lock) { + mi_assert_internal(heap!=NULL); // && !mi_is_process_heap_main(heap)); + + // free all arena pages infos + const bool is_main = mi_is_heap_main(heap); + if (!is_main) { // pages for the main heap are pre-allocated in the arenas + mi_lock(&heap->arena_pages_lock) { + for (size_t i = 0; i < MI_MAX_ARENAS; i++) { + mi_arena_pages_t* arena_pages = mi_atomic_load_ptr_relaxed(mi_arena_pages_t, &heap->arena_pages[i]); + if (arena_pages!=NULL) { + mi_atomic_store_ptr_relaxed(mi_arena_pages_t, &heap->arena_pages[i], NULL); + _mi_free_subproc_safe(arena_pages); + } + } + } + } + + // remove the heap from the subproc + if (!is_main) { + mi_heap_stats_merge_to_main(heap); + } + else { + _mi_stats_merge_into(&heap->subproc->stats,&heap->stats); + } + mi_atomic_decrement_relaxed(&heap->subproc->heap_count); + mi_subproc_stat_decrease(heap->subproc, heaps, 1); + mi_lock_maybe(&heap->subproc->heaps_lock, acquire_heaps_lock) { + if (heap->next!=NULL) { heap->next->prev = heap->prev; } + if (heap->prev!=NULL) { heap->prev->next = heap->next; } + else { heap->subproc->heaps = heap->next; } + } + + mi_lock_done(&heap->theaps_lock); + mi_lock_done(&heap->os_abandoned_pages_lock); + mi_lock_done(&heap->arena_pages_lock); + if (!mi_is_process_heap_main(heap)) { + // _mi_thread_local_free(heap->theap); + _mi_free_subproc_safe(heap); + } +} + +void mi_heap_delete(mi_heap_t* heap) { + if (heap==NULL) return; + mi_heap_t* heap_main = mi_heap_get_heap_main(heap); + if (heap == heap_main) { + _mi_warning_message("cannot delete the main heap\n"); + return; + } + if (mi_heap_qsbr_is_enabled(heap)) { + _mi_warning_message("cannot delete a QSBR heap; use destroy after readers and mutators have stopped\n"); + return; + } + mi_heap_free_theaps(heap); + _mi_heap_move_pages(heap, heap_main); + mi_heap_free(heap,true /* acquire subproc->heaps_lock */); +} + +void _mi_heap_force_destroy(mi_heap_t* heap, bool acquire_heaps_lock) { + if (heap==NULL) return; + mi_heap_free_theaps(heap); + _mi_heap_destroy_pages(heap); + // if (_mi_subproc_main()->heap_main == heap) { + // _mi_stats_merge_into(&heap->subproc->stats,&heap->stats); + // } + // else + { + mi_heap_free(heap, acquire_heaps_lock); // todo: release locks of the main heap? + } +} + +void mi_heap_destroy(mi_heap_t* heap) { + if (heap==NULL) return; + if (mi_is_heap_main(heap)) { + _mi_warning_message("cannot destroy the main heap\n"); + return; + } + _mi_heap_force_destroy(heap,true /* acquire subproc->heaps_lock */); +} + +mi_heap_t* mi_heap_of(const void* p) { + mi_page_t* const page = _mi_safe_ptr_page(p); + if (page==NULL) return NULL; + return mi_page_heap(page); +} + +bool mi_any_heap_contains(const void* p) { + mi_page_t* const page = _mi_safe_ptr_page(p); + return (page!=NULL); +} + +bool mi_heap_contains(const mi_heap_t* heap, const void* p) { + if (heap==NULL) { heap = mi_heap_main(); } + return (heap==mi_heap_of(p)); +} + +// deprecated +bool mi_check_owned(const void* p) { + return mi_any_heap_contains(p); +} + +// unsafe heap utilization function for DragonFly (see issue #1258) +// If the page of pointer `p` belongs to `heap` (or `heap==NULL`) and has less than `perc_threshold` used blocks in its used area return `true`. +// This function is unsafe in general as it assumes we are the only thread accessing the page of `p`. +bool mi_unsafe_heap_page_is_under_utilized(mi_heap_t* heap, void* p, size_t perc_threshold) mi_attr_noexcept { + if (p==NULL) return false; + const mi_page_t* const page = _mi_safe_ptr_page(p); // Get the page containing this pointer + if (page==NULL || mi_page_used(page)==page->capacity || page->capacity < page->reserved) return false; + // If the page is the head of the queue, it is currently being used for + // allocations; we skip it to avoid immediate thrashing. + if (page->prev == NULL) return false; + + // match heap? + const mi_heap_t* const page_heap = mi_page_heap(page); + if (page_heap==NULL) return false; + if (heap!=NULL && page_heap!=heap) return false; + + // check utilization + if (page->capacity==0) return false; + if (perc_threshold>=100) return true; + return (perc_threshold >= ((100UL*mi_page_used(page)) / page->capacity)); +} diff --git a/Objects/mimalloc/heap/init.c b/Objects/mimalloc/heap/init.c new file mode 100644 index 000000000000000..eb0b75ca879140a --- /dev/null +++ b/Objects/mimalloc/heap/init.c @@ -0,0 +1,716 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#include "mimalloc.h" +#include "mimalloc/prim.h" +#include "mimalloc/track.h" +#include "mimalloc/prim/os.h" +#include "mimalloc/prim/prim-tls.h" +#include "mimalloc/util/random.h" +#include "mimalloc/util/stats.h" +#include "mimalloc/heap/page-map.h" +#include "mimalloc/profile/pprof.h" +#include "mimalloc/profile/sample-profile.h" + +#include // atexit +#include "mimalloc/heap/init.h" + +// Empty page used to initialize the small free pages array +static const mi_page_t mi_page_empty = { + #if MI_PAGE_META_IS_ALIGNED + MI_ATOMIC_VAR_INIT(NULL), // self + #endif + MI_ATOMIC_VAR_INIT(0), // xthread_id + NULL, // free + {0}, // xused + #if MI_SIZE_SIZE < 8 + 0, // xlast_used + 0, // xlast_alloc + #endif + NULL, // local_free + 0, // block_size + 0, // page_offset + 0, // capacity + 0, // reserved capacity + 0, // slice_pcommitted + 0, // retire_expire + false, // is_zero + MI_ATOMIC_VAR_INIT(0), // xthread_free + NULL, // theap + NULL, // heap + NULL, NULL, // next, prev + MI_MEMID_STATIC, // memid + #if (MI_PADDING || MI_ENCODE_FREELIST) + #if MI_PAGE_KEY_COUNT==2 + { 0, 0 }, // keys + #else + { 0 }, // key + #endif + #endif + #if MI_QSBR + NULL, NULL, // qsbr_next, qsbr_prev + false, // qsbr_in_queue + mi_init_struct_zero, // qsbr + #endif +}; + +#define MI_PAGE_EMPTY() ((mi_page_t*)&mi_page_empty) + +#if MI_SMALL_WSIZE_MAX == 128 +#define MI_INIT_PAGES_DIRECT(p) MI_INIT128(p) +#elif MI_SMALL_WSIZE_MAX == 256 +#define MI_INIT_PAGES_DIRECT(p) MI_INIT128(p), MI_INIT128(p) +#else +#error define initializer for direct pages +#endif + +#if (MI_PADDING>0) && (MI_SIZE_SIZE >= 8) +#define MI_SMALL_PAGES_EMPTY { MI_INIT_PAGES_DIRECT(MI_PAGE_EMPTY), MI_PAGE_EMPTY(), MI_PAGE_EMPTY() } +#elif (MI_PADDING>0) +#define MI_SMALL_PAGES_EMPTY { MI_INIT_PAGES_DIRECT(MI_PAGE_EMPTY), MI_PAGE_EMPTY(), MI_PAGE_EMPTY(), MI_PAGE_EMPTY() } +#else +#define MI_SMALL_PAGES_EMPTY { MI_INIT_PAGES_DIRECT(MI_PAGE_EMPTY), MI_PAGE_EMPTY() } +#endif + + +// Empty page queues for every bin +#define QNULL(sz) { NULL, NULL, 0, (sz)*sizeof(uintptr_t), 0 } +#define MI_PAGE_QUEUES_EMPTY \ + { QNULL(1), \ + QNULL( 1), QNULL( 2), QNULL( 3), QNULL( 4), QNULL( 5), QNULL( 6), QNULL( 7), QNULL( 8), /* 8 */ \ + QNULL( 10), QNULL( 12), QNULL( 14), QNULL( 16), QNULL( 20), QNULL( 24), QNULL( 28), QNULL( 32), /* 16 */ \ + QNULL( 40), QNULL( 48), QNULL( 56), QNULL( 64), QNULL( 80), QNULL( 96), QNULL( 112), QNULL( 128), /* 24 */ \ + QNULL( 160), QNULL( 192), QNULL( 224), QNULL( 256), QNULL( 320), QNULL( 384), QNULL( 448), QNULL( 512), /* 32 */ \ + QNULL( 640), QNULL( 768), QNULL( 896), QNULL( 1024), QNULL( 1280), QNULL( 1536), QNULL( 1792), QNULL( 2048), /* 40 */ \ + QNULL( 2560), QNULL( 3072), QNULL( 3584), QNULL( 4096), QNULL( 5120), QNULL( 6144), QNULL( 7168), QNULL( 8192), /* 48 */ \ + QNULL( 10240), QNULL( 12288), QNULL( 14336), QNULL( 16384), QNULL( 20480), QNULL( 24576), QNULL( 28672), QNULL( 32768), /* 56 */ \ + QNULL( 40960), QNULL( 49152), QNULL( 57344), QNULL( 65536), QNULL( 81920), QNULL( 98304), QNULL(114688), QNULL(131072), /* 64 */ \ + QNULL(163840), QNULL(196608), QNULL(229376), QNULL(262144), QNULL(327680), QNULL(393216), QNULL(458752), QNULL(524288), /* 72 */ \ + QNULL(MI_LARGE_MAX_OBJ_WSIZE + 1 /* 655360, Huge queue */), \ + QNULL(MI_LARGE_MAX_OBJ_WSIZE + 2) /* Full queue */ } + +#define MI_STAT_COUNT_NULL() {0,0,0} + +// Empty statistics +#define MI_STAT_COUNT(stat) {0,0,0}, +#define MI_STAT_COUNTER(stat) {0}, + +#define MI_STATS_FIELDS_NULL \ + MI_STAT_FIELDS() /* regular stat fields */ \ + { MI_INIT4(MI_STAT_COUNT_NULL) }, /* stat reserved */ \ + { { 0 }, { 0 }, { 0 }, { 0 } }, /* stat counter reserved */ \ + { MI_INIT74(MI_STAT_COUNT_NULL) }, /* malloc_bins */ \ + { MI_INIT74(MI_STAT_COUNT_NULL) }, /* page bins */ \ + { MI_INIT5(MI_STAT_COUNT_NULL) } /* chunk bins */ + +#define MI_STATS_NULL \ + { sizeof(mi_stats_t), MI_STAT_VERSION, MI_STATS_FIELDS_NULL } + +// -------------------------------------------------------- +// Statically allocate an empty theap as the initial +// thread local value for the default theap, +// and statically allocate the backing theap for the main +// thread so it can function without doing any allocation +// itself (as accessing a thread local for the first time +// may lead to allocation itself on some platforms) +// -------------------------------------------------------- + +static mi_decl_cache_align mi_tld_t mi_tld_detached = { + MI_THREADID_DETACHED, // thread_id + 0, // thread_seq + 0, // default numa node + NULL, // subproc + NULL, // theaps list + MI_LOCK_INITIALIZER, // theaps lock + false, // recurse + false, // is_in_threadpool + MI_MEMID_STATIC // memid +}; + +#ifdef __cplusplus +extern // fix warning in C++ for const variables with internal linkage +#endif +mi_decl_hidden mi_decl_cache_align const mi_theap_t _mi_theap_empty = { + MI_SMALL_PAGES_EMPTY, // direct small pages + &mi_tld_detached, // tld + MI_ATOMIC_VAR_INIT(NULL), // heap + MI_ATOMIC_VAR_INIT(NULL), // subproc + MI_ATOMIC_VAR_INIT(1), // refcount + 0, // full page retain + false, // allow reclaim + true, // allow abandon + true, // is_detached + true, // profile_disabled: this static empty theap must never be sampled + ~MI_ZU(0), // sample countdown: "-1" (with a sample rate of 0, so we won't write to the empty theap with MI_SAMPLE==2) + 0, 0, // sample rate, requested + 0, 0, // profile rate, countdown + 0, 0, 0, 0, // guarded rate, countdown, min, max + 0, 0, // profiler_reserved1, profiler_reserved2 + 0, // heartbeat + { {0}, {0}, 0, true }, // random + 0, 0, // page count, OS page count + MI_BIN_FULL, 0, // page retired min/max + 0, // pages_full_size + 0, 0, // generic count + NULL, NULL, // tnext, tprev + NULL, NULL, // hnext, hprev + MI_PAGE_QUEUES_EMPTY, + MI_MEMID_STATIC, + MI_STATS_NULL, // stats + #if MI_QSBR + NULL, NULL, // qsbr_first, qsbr_last + #endif +}; + +#undef MI_STAT_COUNT +#undef MI_STAT_COUNTER + + +// pre-allocate the process heap, and meta-data theap +static mi_decl_cache_align mi_heap_t mi_process_heap_main = mi_init_struct_zero; +static mi_decl_cache_align mi_theap_t mi_process_theap_meta = mi_init_struct_zero; + +// pre-allocate the initial tld and theap for the main thread (this is not strictly needed but nice for stats) +static mi_decl_cache_align mi_tld_t mi_process_tld_main = mi_init_struct_zero; +static mi_decl_cache_align mi_theap_t mi_process_theap_main = mi_init_struct_zero; + +mi_decl_hidden mi_decl_cache_align mi_theap_t _mi_theap_empty_wrong = mi_init_struct_zero; // used for error paths +mi_decl_hidden bool _mi_process_is_initialized = false; // set to `true` in `mi_process_init`. + + +mi_page_t* _mi_page_empty_get(void) { + return (mi_page_t*)&mi_page_empty; +} + +mi_decl_cold mi_decl_noinline mi_theap_t* _mi_theap_empty_get(void) { + return (mi_theap_t*)&_mi_theap_empty; +} + +bool _mi_is_empty_theap(const mi_theap_t* theap) { + return (theap == &_mi_theap_empty); +} + +/* ----------------------------------------------------------- + Initialization + Note: on some platforms lock_init or just a thread local access + can cause allocation and induce recursion during initialization. +----------------------------------------------------------- */ + +static mi_tld_t* mi_tld_init(mi_tld_t* tld, size_t tseq, mi_subproc_t* subproc); + +// Initialize main heap +static void mi_heap_main_init_once(void) { + mi_memid_t memid_static = _mi_memid_create(MI_MEM_STATIC); + mi_memcpy(&_mi_theap_empty_wrong,&_mi_theap_empty,sizeof(_mi_theap_empty_wrong)); + + // initialize the main subprocess + mi_subproc_t* subproc_main = _mi_subproc_main_init(); + + // detached tld for mi_theap_empty (and theap_meta) + mi_tld_detached.memid = memid_static; + mi_tld_init(&mi_tld_detached, 0, subproc_main); + + // main process heap + mi_process_heap_main.memid = memid_static; + mi_atomic_store_ptr_release(mi_heap_t,&subproc_main->heap_main,&mi_process_heap_main); + _mi_heap_init(&mi_process_heap_main,mi_thread_local_key_fast,subproc_main,0,false); + + // detached theap for allocating meta-data (we can allocate on this without having an initialized thread) + mi_process_theap_meta.memid = memid_static; + _mi_theap_init(&mi_process_theap_meta,&mi_process_heap_main,&mi_tld_detached); + mi_process_theap_meta.allow_page_abandon = false; // for security, don't share with other threads + mi_process_theap_meta.page_full_retain = 2; + mi_process_theap_meta.sample_rate = 0; // no sampling for meta data + mi_process_theap_meta.sample_countdown = 0; + mi_theap_profile_disable(&mi_process_theap_meta); // permanently exclude from profiling, see `mi_theap_profile_disable` + subproc_main->theap_meta = &mi_process_theap_meta; + + // mi_heap_theap_set(&mi_process_heap_main,&mi_process_theap_main); // set in `mi_thread_init(_theap_default)` +} + +static void mi_heap_main_init(void) { + mi_atomic_do_once { + mi_heap_main_init_once(); + } +} + +mi_heap_t* _mi_subproc_heap_main(mi_subproc_t* subproc) { + mi_heap_t* heap = mi_atomic_load_ptr_acquire(mi_heap_t,&subproc->heap_main); + if mi_likely(heap!=NULL) { + return heap; + } + else if (_mi_subproc_is_main(subproc)) { + mi_heap_main_init(); + mi_assert_internal(mi_atomic_load_ptr_acquire(mi_heap_t,&subproc->heap_main) != NULL); + return mi_atomic_load_ptr_acquire(mi_heap_t,&subproc->heap_main); + } + else { + mi_assert_internal(false); + return &mi_process_heap_main; + } +} + +/* ----------------------------------------------------------- + Thread local data +----------------------------------------------------------- */ + +mi_theap_t* _mi_theap_main(void) { + // All subprocess main heaps share this slot; do not follow a possibly deleted default theap. + const mi_thread_local_t key = mi_process_heap_main.theap; + return (key==0 ? NULL : (mi_theap_t*)_mi_thread_local_get(key)); +} + +static mi_tld_t* mi_tld_init(mi_tld_t* tld, size_t tseq, mi_subproc_t* subproc) { + tld->subproc = subproc; + tld->theaps = NULL; + mi_lock_init(&tld->theaps_lock); + if (tld->thread_id == MI_THREADID_DETACHED) { + tld->numa_node = -1; + } + else { + tld->numa_node = _mi_os_numa_node(); + tld->thread_id = _mi_prim_thread_id(); + tld->is_in_threadpool = _mi_prim_thread_is_in_threadpool(); + tld->thread_seq = tseq; + mi_atomic_increment_relaxed(&tld->subproc->thread_count); + } + return tld; +} + +// Allocate fresh tld +static mi_tld_t* mi_tld_create(mi_subproc_t* subproc) { + mi_assert_internal(subproc->theap_meta != NULL); // should be initialized on the main thread before other threads allocate + const size_t tseq = mi_atomic_increment_relaxed(&subproc->thread_total_count); + + mi_memid_t memid; + mi_tld_t* tld; + if (_mi_subproc_is_main(subproc) && tseq==0 /* first tld */) { + tld = &mi_process_tld_main; + memid = _mi_memid_create_static(tld,sizeof(*tld)); + } + else { + tld = (mi_tld_t*)_mi_meta_zalloc(subproc, sizeof(mi_tld_t), &memid); + } + if (tld==NULL) { + _mi_error_message(ENOMEM, "unable to allocate memory for thread local data\n"); + return NULL; + } + tld->memid = memid; + return mi_tld_init(tld,tseq,subproc); +} + +mi_decl_noinline static void mi_tld_free(mi_tld_t* tld) { + if (tld==NULL) return; + mi_atomic_decrement_relaxed(&tld->subproc->thread_count); + tld->thread_id = (mi_threadid_t)(~0); // it is best to set an invalid tid for tld_main as sometimes the same thread-id + // is reused by the OS after a thread has terminated. (see issue #1287) + mi_lock_done(&tld->theaps_lock); + _mi_meta_free(tld->subproc, tld, tld->memid); // note: safe for static tld +} + + +/* ----------------------------------------------------------- + Thread Init +----------------------------------------------------------- */ + +#if MI_DEBUG || defined(MI_TLS_RECURSE_GUARD) +static mi_theap_t* mi_heap_check_for_existing_theap(mi_heap_t* heap) { + const mi_threadid_t tid = _mi_thread_id(); + mi_theap_t* thread_theap = NULL; + mi_lock(&heap->theaps_lock) { + for(mi_theap_t* theap = heap->theaps; theap != NULL; theap = theap->hnext ) { + if (theap->tld->thread_id == tid) { + thread_theap = theap; + break; + } + } + } + return thread_theap; +} +#endif + +// Initialize thread +mi_theap_t* _mi_thread_init_with_heap(mi_heap_t* heap_main) +{ + // ensure our process has started already + mi_process_init(); + + // if the theap_default is already set we have already initialized + mi_theap_t* theap = _mi_theap_default(); + if (mi_theap_is_initialized(theap)) return theap; + + // initialize the default theap + // note: we cannot access thread-locals yet as that can cause (recursive) allocation + // (on macOS <= 14 for example where the loader allocates thread-local data on demand). + if (heap_main==NULL) { + heap_main = mi_heap_main(); + mi_assert_internal(heap_main == &mi_process_heap_main); + } + mi_assert_internal(heap_main!=NULL); + + #if MI_DEBUG || defined(MI_TLS_RECURSE_GUARD) + theap = mi_heap_check_for_existing_theap(heap_main); // recursion check + #if !defined(MI_TLS_RECURSE_GUARD) + mi_assert_internal(theap==NULL); + #endif + #else + theap = NULL; + #endif + + if (theap==NULL) { + // allocated the tld + mi_tld_t* tld = mi_tld_create(heap_main->subproc); + if (tld==NULL) return NULL; // out-of-memory on tld allocation + // allocate and initialize the theap for the main heap + if (tld==&mi_process_tld_main) { + theap = &mi_process_theap_main; // initial theap is pre-allocated + theap->memid = _mi_memid_create_static(theap,sizeof(*theap)); + } + else { + theap = _mi_theap_alloc(heap_main,tld); // otherwise meta allocate + if (theap==NULL) { mi_tld_free(tld); return NULL; } // out-of-memory on theap allocation + } + _mi_theap_init(theap,heap_main,tld); + } + + // now initialize the thread + _mi_theap_default_set(theap); + // and only then set the heap_theap field as that accesses thread locals + _mi_heap_theap_set(heap_main, theap); // todo: can fail! + + mi_assert_internal(mi_theap_is_initialized(theap)); + mi_theap_t* const heap_theap = (heap_main==NULL ? NULL : (mi_theap_t*)_mi_thread_local_get(heap_main->theap)); + mi_assert_internal(heap_main==NULL || heap_theap == theap); MI_UNUSED_RELEASE(heap_theap); + + mi_subproc_stat_increase(mi_theap_subproc(theap), threads, 1); // or theap stats and wait for merge? + // _mi_verbose_message("thread init: 0x%zx\n", _mi_thread_id()); + return theap; +} + +mi_theap_t* _mi_thread_init(void) { + return _mi_thread_init_with_heap(NULL); +} + +void mi_decl_noinline mi_thread_init(void) mi_attr_noexcept { + _mi_thread_init(); +} + + + +/* ----------------------------------------------------------- + Theaps done +----------------------------------------------------------- */ + +// Free the thread local theaps +static void mi_thread_theaps_done(mi_tld_t* tld) +{ + // abandon the pages of all theaps in this thread + mi_lock(&tld->theaps_lock) { + mi_theap_t* theap = tld->theaps; + while (theap != NULL) { + mi_theap_t* next = theap->tnext; + // never destroy theaps; if a dll is linked statically with mimalloc, + // there may still be delete/free calls after the mi_fls_done is called. Issue #207 + mi_heap_t* const heap = mi_theap_heap(theap); + mi_lock(&heap->theaps_lock) { // keep page queues stable during a heap walk + _mi_theap_collect_abandon(theap); + } + mi_assert_internal(theap->page_count==0); + theap = next; + } + } + + // reset the thread local theaps + // note: do this after abandon as page->heap may be NULL and mi_heap_main should return the heap + // belonging to the right subprocess + _mi_theap_default_set((mi_theap_t*)&_mi_theap_empty); + _mi_theap_cached_set((mi_theap_t*)&_mi_theap_empty); + + // We might run concurrently with a `mi_heap_free_theaps` and we need to ensure we free theaps atomically. + // we first detach our theaps list from any heaps + _mi_tld_detach_theaps(tld); + + // no heaps point to our theaps anymore, free them + mi_lock(&tld->theaps_lock) { // paranoia + mi_theap_t* theap = tld->theaps; + tld->theaps = NULL; + while (theap != NULL) { + mi_theap_t* next = theap->tnext; + mi_assert_internal(theap->page_count==0); + mi_assert_internal(mi_theap_heap_peek(theap)==NULL); + theap->tld = NULL; + theap->tnext = NULL; + theap->tprev = NULL; + mi_assert_internal(mi_atomic_load_relaxed(&theap->refcount) == 1); // as the cached entry is set to empty + _mi_theap_decref(theap); + theap = next; + } + } + + mi_assert(_mi_theap_default()==(mi_theap_t*)&_mi_theap_empty); // careful to not re-initialize the default theap during theap_delete + mi_assert(!mi_theap_is_initialized(_mi_theap_default())); +} + + +// -------------------------------------------------------- +// Try to run `mi_thread_done()` automatically so any memory +// owned by the thread but not yet released can be abandoned +// and re-owned by another thread. +// +// 1. windows dynamic library: +// call from DllMain on DLL_THREAD_DETACH +// 2. windows static library: +// use special linker section to call a destructor when the thread is done +// 3. unix, pthreads: +// use a pthread key to call a destructor when a pthread is done +// +// In the last two cases we also need to call `mi_process_init` +// to set up the thread local keys. +// -------------------------------------------------------- + +// Set up hooks so `mi_thread_done` is called automatically +static void mi_process_setup_auto_thread_done(void) { + mi_atomic_do_once { + _mi_prim_thread_init_auto_done(); + mi_theap_t* theap = _mi_theap_default(); + mi_assert_internal(mi_theap_is_initialized(theap)); + if (mi_theap_is_initialized(theap)) { + _mi_theap_default_set(theap); + } + } +} + +void mi_thread_done(void) mi_attr_noexcept { + _mi_thread_done(NULL); +} + +void _mi_thread_done(mi_theap_t* _theap_main) +{ + // NULL can be passed on some platforms + if (_theap_main==NULL) { + _theap_main = _mi_theap_default(); + } + + // prevent re-entrancy through theap_done/theap_set_default_direct (issue #699) + if (!mi_theap_is_initialized(_theap_main)) { + return; + } + + // get the current tld + mi_tld_t* const tld = _theap_main->tld; + + // release dynamic thread_local's + _mi_thread_locals_thread_done(); + + // adjust stats + mi_subproc_stat_decrease(tld->subproc, threads, 1); // todo: or `_theap_main->heap`? + + // check thread-id as on Windows shutdown with FLS the main (exit) thread may call this on thread-local theaps... + if (tld->thread_id != _mi_prim_thread_id()) return; + + // delete the thread local theaps + mi_thread_theaps_done(tld); + + // free thread local data + mi_tld_free(tld); +} + +void mi_thread_set_in_threadpool(void) mi_attr_noexcept { + mi_theap_t* theap = mi_theap_get_default(); + theap->tld->is_in_threadpool = true; +} + + +// -------------------------------------------------------- +// Process init and done +// -------------------------------------------------------- + +static bool os_preloading = true; // true until this module is initialized + +// Returns true if this module has not been initialized; Don't use C runtime routines until it returns false. +bool mi_decl_noinline _mi_preloading(void) { + return os_preloading; +} + +// Returns true if mimalloc was redirected +mi_decl_nodiscard bool mi_is_redirected(void) mi_attr_noexcept { + return _mi_is_redirected(); +} + +// Called once by the process loader from `src/prim/prim.c` before `main` is called. +void _mi_auto_process_init(void) { + os_preloading = false; + + mi_process_init(); + + _mi_options_post_init(); // now we can print to stderr + if (_mi_is_redirected()) _mi_verbose_message("malloc is redirected.\n"); + + // show message from the redirector (if present) + const char* msg = NULL; + _mi_allocator_init(&msg); + if (msg != NULL && (mi_option_is_enabled(mi_option_verbose) || mi_option_is_enabled(mi_option_show_errors))) { + _mi_fputs(NULL,NULL,NULL,msg); + } + + // reseed random + mi_theap_t* theap = _mi_theap_default(); + if (theap != NULL) { + _mi_random_reinit_if_weak(&theap->random); + mi_subproc_t* subproc = mi_theap_subproc(theap); + if (subproc->theap_meta != NULL) { + mi_lock(&subproc->theap_meta_lock) { + _mi_random_reinit_if_weak(&subproc->theap_meta->random); + } + } + } + + // check if we should start the pprof profiler + _mi_pprof_profiler_init(); +} + + +// Initialize the process; called by thread_init, the process loader, or an initial allocation (perhaps by the loader or a system library) +static void mi_process_init_once(void) { + #if defined(__CYGWIN__) // we need to kickstart the cygwin runtime + __mi_thread_id_helper = NULL; + #endif + _mi_verbose_message("process init: 0x%zx\n", _mi_thread_id()); + + _mi_detect_cpu_features(); + _mi_options_init(); // read environment (if possible) + _mi_stats_init(); // start timer + _mi_os_init(); // primitive dependent + + mi_heap_main_init(); // before page_map_init so stats are working + _mi_page_map_init(); // todo: this could fail.. should we abort in that case? + mi_thread_init(); + + // the following can potentially allocate (on freeBSD for pthread keys) + _mi_tls_slots_init(); // pthread key create + _mi_thread_locals_init(); // pthread key create + _mi_process_is_initialized = true; // before `mi_process_setup_auto_thread_done` so `_mi_theap_default` returns the current theap with `MI_TLS_RECURSE_GUARD` + mi_process_setup_auto_thread_done(); // after the above mi_thread_init so it can add the current theap + _mi_fork_init(); // register fork handlers (can allocate) + + #if defined(_WIN32) && defined(MI_WIN_INIT_USE_FLS) + // On windows, when building as a static lib the FLS cleanup happens to early for the main thread. + // To avoid this, set the FLS value for the main thread to NULL so the fls cleanup + // will not call _mi_thread_done on the (still executing) main thread. See issue #508. + _mi_prim_thread_associate_default_theap(NULL); + #endif + + // mi_stats_reset(); // only call stat reset *after* thread init (or the theap tld == NULL) + mi_track_init(); + if (mi_option_is_enabled(mi_option_reserve_huge_os_pages)) { + size_t pages = mi_option_get_clamp(mi_option_reserve_huge_os_pages, 0, 128*1024); + int reserve_at = (int)mi_option_get_clamp(mi_option_reserve_huge_os_pages_at, -1, INT_MAX); + if (reserve_at != -1) { + mi_reserve_huge_os_pages_at(pages, reserve_at, pages*500); + } else { + mi_reserve_huge_os_pages_interleave(pages, 0, pages*500); + } + } + if (mi_option_is_enabled(mi_option_reserve_os_memory)) { + long ksize = mi_option_get(mi_option_reserve_os_memory); + if (ksize > 0) { + mi_reserve_os_memory((size_t)ksize*MI_KiB, true, true); + } + } +} + +// Initialize the process; called by thread_init or the process loader +void mi_process_init(void) mi_attr_noexcept { + mi_atomic_do_once { + mi_process_init_once(); + } +} + +// Called once on process exit (when the process/dll is exiting gracefully) +static void mi_process_unwind_once(void) +{ + // stop the profiler + _mi_pprof_profiler_done(); + + // decref any cached theap + _mi_theap_cached_set(_mi_theap_empty_get()); + + // release any thread specific resources and ensure _mi_thread_done is called on all but the main thread + _mi_prim_thread_done_auto_done(); + +#ifndef MI_SKIP_COLLECT_ON_EXIT +#if (MI_DEBUG || !defined(MI_SHARED_LIB)) + // free all memory if possible on process exit. This is not needed for a stand-alone process + // but should be done if mimalloc is statically linked into another shared library which + // is repeatedly loaded/unloaded, see issue #281. + mi_theap_collect(_mi_theap_default(), true /* force */); +#endif +#endif + + // done with tracking tools + mi_track_done(); + + // Forcefully release all retained memory; this can be dangerous in general if overriding regular malloc/free + // since after process_done there might still be other code running that calls `free` (like at_exit routines, + // or C-runtime termination code. + mi_subproc_t* subproc_main = _mi_subproc_main(); + if (mi_option_is_enabled(mi_option_destroy_on_exit)) { + _mi_subprocs_unsafe_destroy_all(); // destroys all mi_subprocs, arenas, thread locals, and the page_map! + } + else { + // free dynamic thread locals (if used at all) + _mi_thread_locals_thread_done(); + _mi_thread_locals_done(); + if (subproc_main->heap_main != NULL) { + if (mi_option_is_enabled(mi_option_show_stats) || mi_option_is_enabled(mi_option_verbose)) { + mi_theap_collect(subproc_main->theap_meta, false /* force */); // update stats of all pages in theap_meta + _mi_theap_merge_stats(subproc_main->theap_meta); + _mi_theap_merge_stats(_mi_theap_default()); // _mi_thread_locals_done can free + mi_heap_stats_merge_to_subproc(subproc_main->heap_main); + mi_subproc_stats_print_out(mi_subproc_main(), NULL, NULL); // note: can try to access (the now freed) thread_locals in mi_heap_theap_peek + } + } + } + + _mi_tls_slots_done(); +} + +// Called when the process is done +static void mi_process_done_once(void) { + // only shutdown if we were initialized + if (!_mi_process_is_initialized) return; + // ensure we are called once + static bool process_done = false; + if (process_done) return; + process_done = true; + + const size_t thread_count = mi_atomic_load_acquire(&_mi_subproc_main()->thread_count); + if (thread_count == 1 || !_mi_prim_process_is_killed()) { // can we safely take locks? + mi_process_unwind_once(); + _mi_verbose_message("process done: 0x%zx\n", mi_process_tld_main.thread_id); + } + + _mi_subproc_main_done(); + _mi_allocator_done(); + os_preloading = true; // don't call the C runtime anymore +} + + +// Call when the process is done (cdecl as it is used with `at_exit` on some platforms) +void mi_cdecl mi_process_done(void) mi_attr_noexcept { + // we don't use `mi_atomic_do_once` here to avoid deadlocks if threads are killed due to a forced exit (issue #1377) + static _Atomic(size_t) mi_process_is_done; + size_t expected = 0; + if (mi_atomic_cas_strong_acq_rel(&mi_process_is_done, &expected, 1)) { + mi_process_done_once(); + } +} + +// Called automatically when the process is done (cdecl as it is used with `at_exit` on some platforms) +void mi_cdecl _mi_auto_process_done(void) mi_attr_noexcept { + if (_mi_option_get_fast(mi_option_destroy_on_exit)>=2) return; // allow disabling auto process done + mi_process_done(); +} diff --git a/Objects/mimalloc/heap/page-map.c b/Objects/mimalloc/heap/page-map.c new file mode 100644 index 000000000000000..74954b797fa2538 --- /dev/null +++ b/Objects/mimalloc/heap/page-map.c @@ -0,0 +1,520 @@ +/*---------------------------------------------------------------------------- +Copyright (c) 2023-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +#include "mimalloc.h" +#include "mimalloc/alloc/page.h" +#include "mimalloc/arena/arena.h" +#include "mimalloc/heap/subproc.h" +#include "mimalloc/heap/page-map.h" +#include "../arena/bitmap.h" + +static void mi_page_map_cannot_commit(void) { + _mi_warning_message("unable to commit the allocation page-map on-demand\n" ); +} + +#if MI_PAGE_MAP_FLAT + +// The page-map contains a byte for each 64kb slice in the address space. +// For an address `a` where `ofs = _mi_page_map[a >> 16]`: +// 0 = unused +// 1 = the slice at `a & ~0xFFFF` is a mimalloc page. +// 1 < ofs <= 127 = the slice is part of a page, starting at `(((a>>16) - ofs - 1) << 16)`. +// +// 1 byte per slice => 1 TiB address space needs a 2^14 * 2^16 = 16 MiB page map. +// A full 256 TiB address space (48 bit) needs a 4 GiB page map. +// A full 4 GiB address space (32 bit) needs only a 64 KiB page map. + +// Use an initial empty page map so `free(NULL)` works even if mimalloc is not yet initialized (issue #1341) +static uint8_t mi_page_map_empty[1] = { 1 }; // mi_ptr_page(NULL) == NULL + +mi_decl_hidden mi_decl_cache_align _Atomic(uint8_t*) _mi_page_map = mi_page_map_empty; +mi_decl_hidden _Atomic(void*) _mi_page_map_max_address = NULL; +static mi_memid_t mi_page_map_memid; + +#define MI_PAGE_MAP_ENTRIES_PER_COMMIT_BIT MI_ARENA_SLICE_SIZE +static mi_bitmap_t* mi_page_map_commit; // one bit per committed 64 KiB entries + +mi_decl_nodiscard static bool mi_page_map_ensure_committed(size_t idx, size_t slice_count); + +bool _mi_page_map_init(void) { + size_t vbits = (size_t)mi_option_get_clamp(mi_option_max_vabits, 0, MI_MAX_VABITS); + if (vbits == 0) { + vbits = _mi_os_virtual_address_bits(); + #if MI_ARCH_X64 // canonical address is limited to the first 128 TiB + if (vbits >= 48) { vbits = 47; } + #endif + } + if (vbits < MI_ARENA_SLICE_SHIFT) { + vbits = MI_ARENA_SLICE_SHIFT; + } + if (vbits < MI_MIN_VABITS) { // cover at least this much for a faster _mi_checked_ptr + vbits = MI_MIN_VABITS; + } + if (vbits > MI_MAX_VABITS) { // limit page map size even if more virtual addresses are available + vbits = MI_MAX_VABITS; + } + + // Allocate the page map and commit bits + mi_atomic_store_ptr_release(void, &_mi_page_map_max_address, (void*)(vbits >= MI_SIZE_BITS ? (SIZE_MAX - MI_ARENA_SLICE_SIZE + 1) : (MI_PU(1) << vbits))); + const size_t page_map_size = (MI_ZU(1) << (vbits - MI_ARENA_SLICE_SHIFT)); + const bool commit = (page_map_size <= 1*MI_MiB || mi_option_is_enabled(mi_option_pagemap_commit)); // _mi_os_has_overcommit(); // commit on-access on Linux systems? + const size_t commit_bits = mi_divide_up(page_map_size, MI_PAGE_MAP_ENTRIES_PER_COMMIT_BIT); + const size_t bitmap_size = (commit ? 0 : mi_bitmap_size(commit_bits, NULL)); + const size_t reserve_size = bitmap_size + page_map_size; + mi_subproc_t* const subproc = _mi_subproc_main(); + uint8_t* const base = (uint8_t*)_mi_os_alloc_aligned(subproc, reserve_size, 1, commit, true /* allow large */, &mi_page_map_memid); + if (base==NULL) { + _mi_error_message(ENOMEM, "unable to reserve virtual memory for the page map (%zu KiB)\n", page_map_size / MI_KiB); + return false; + } + if (mi_page_map_memid.initially_committed && !mi_page_map_memid.initially_zero) { + _mi_warning_message("internal: the page map was committed but not zero initialized!\n"); + mi_memzero_aligned(base, reserve_size); + } + if (bitmap_size > 0) { + mi_page_map_commit = (mi_bitmap_t*)base; + if (!_mi_os_commit(subproc, mi_page_map_commit, bitmap_size, NULL)) { + mi_page_map_cannot_commit(); + return false; + } + mi_bitmap_init(mi_page_map_commit, commit_bits, true); + } + mi_atomic_store_ptr_release(uint8_t,&_mi_page_map, base + bitmap_size); + + // commit the first part so NULL pointers get resolved without an access violation + if (!commit) { + if (!mi_page_map_ensure_committed(0, 1)) { + mi_page_map_cannot_commit(); + return false; + } + } + mi_atomic_load_ptr_relaxed(uint8_t, &_mi_page_map)[0] = 1; // so mi_ptr_page(NULL) == NULL + mi_assert_internal(mi_ptr_page(NULL)==NULL); + return true; +} + +void _mi_page_map_unsafe_destroy(void) { + mi_assert_internal(mi_atomic_load_ptr_relaxed(uint8_t, &_mi_page_map) != NULL); + if (mi_atomic_load_ptr_relaxed(uint8_t, &_mi_page_map) == NULL) return; + _mi_os_free_ex(_mi_subproc_main(), mi_page_map_memid.mem.os.base, mi_page_map_memid.mem.os.size, true, mi_page_map_memid); + mi_atomic_store_ptr_release(uint8_t, &_mi_page_map, NULL); + mi_page_map_commit = NULL; + mi_atomic_store_ptr_release(void, &_mi_page_map_max_address, NULL); + mi_page_map_memid = _mi_memid_none(); +} + + +static bool mi_page_map_ensure_committed(size_t idx, size_t slice_count) { + // is the page map area that contains the page address committed? + // we always set the commit bits so we can track what ranges are in-use. + // we only actually commit if the map wasn't committed fully already. + uint8_t* const page_map = mi_atomic_load_ptr_relaxed(uint8_t,&_mi_page_map); + if (mi_page_map_commit != NULL) { + const size_t commit_idx = idx / MI_PAGE_MAP_ENTRIES_PER_COMMIT_BIT; + const size_t commit_idx_hi = (idx + slice_count - 1) / MI_PAGE_MAP_ENTRIES_PER_COMMIT_BIT; + for (size_t i = commit_idx; i <= commit_idx_hi; i++) { // per bit to avoid crossing over bitmap chunks + if (mi_bitmap_is_clear(mi_page_map_commit, i)) { + // this may race, in which case we do multiple commits (which is ok) + bool is_zero; + uint8_t* const start = page_map + (i * MI_PAGE_MAP_ENTRIES_PER_COMMIT_BIT); + const size_t size = MI_PAGE_MAP_ENTRIES_PER_COMMIT_BIT; + if (!_mi_os_commit(_mi_subproc_main(), start, size, &is_zero)) { + mi_page_map_cannot_commit(); + return false; + } + if (!is_zero && !mi_page_map_memid.initially_zero) { mi_memzero(start, size); } + mi_bitmap_set(mi_page_map_commit, i); + } + } + } + #if MI_DEBUG > 0 + page_map[idx] = 0; + page_map[idx+slice_count-1] = 0; + #endif + return true; +} + + +static size_t mi_page_map_get_idx(mi_page_t* page, uint8_t** page_start, size_t* slice_count) { + size_t page_size; + *page_start = mi_page_area(page, &page_size); + if (page_size > MI_LARGE_PAGE_SIZE) { page_size = MI_LARGE_PAGE_SIZE - MI_ARENA_SLICE_SIZE; } // furthest interior pointer + *slice_count = _mi_slice_count_of_size(page_size) + ((*page_start - mi_page_slice_start(page))/MI_ARENA_SLICE_SIZE); // add for large aligned blocks + return mi_page_map_index(page); +} + +bool _mi_page_map_register(mi_page_t* page) { + mi_assert_internal(page != NULL); + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_assert_internal(mi_atomic_load_ptr_relaxed(uint8_t,&_mi_page_map) != NULL); // should be initialized before multi-thread access! + uint8_t* page_map = mi_atomic_load_ptr_relaxed(uint8_t,&_mi_page_map); + if mi_unlikely(mi_atomic_load_ptr_relaxed(uint8_t,&_mi_page_map) == NULL) { + if (!_mi_page_map_init()) return false; + page_map = mi_atomic_load_ptr_acquire(uint8_t,&_mi_page_map); + } + mi_assert(page_map!=NULL); + uint8_t* page_start; + size_t slice_count; + const size_t idx = mi_page_map_get_idx(page, &page_start, &slice_count); + + if (!mi_page_map_ensure_committed(idx, slice_count)) { + return false; + } + + // set the offsets + for (size_t i = 0; i < slice_count; i++) { + mi_assert_internal(i < 128); + page_map[idx + i] = (uint8_t)(i+1); + } + return true; +} + +void _mi_page_map_unregister(mi_page_t* page) { + uint8_t* const page_map = mi_atomic_load_ptr_relaxed(uint8_t,&_mi_page_map); + mi_assert_internal(page_map != NULL); + if (page_map == NULL) return; + // get index and count + uint8_t* page_start; + size_t slice_count; + const size_t idx = mi_page_map_get_idx(page, &page_start, &slice_count); + // unset the offsets + mi_memzero(page_map + idx, slice_count); +} + +void _mi_page_map_unregister_range(void* start, size_t size) { + uint8_t* const page_map = mi_atomic_load_ptr_relaxed(uint8_t,&_mi_page_map); + mi_assert_internal(page_map!=NULL); + if (page_map == NULL) return; + const size_t slice_count = mi_divide_up(size, MI_ARENA_SLICE_SIZE); + const uintptr_t index = mi_page_map_index(start); + // todo: scan the commit bits and clear only those ranges? + if (!mi_page_map_ensure_committed(index, slice_count)) { // we commit the range in total; + return; + } + mi_memzero(&page_map[index], slice_count); +} + + +mi_page_t* _mi_safe_ptr_page(const void* p) { + if mi_unlikely(p >= mi_atomic_load_ptr_relaxed(void, &_mi_page_map_max_address)) return NULL; + const uintptr_t idx = mi_page_map_index(p); + if mi_unlikely(mi_page_map_commit != NULL && !mi_bitmap_is_set(mi_page_map_commit, idx/MI_PAGE_MAP_ENTRIES_PER_COMMIT_BIT)) return NULL; + const uintptr_t ofs = mi_page_map_at(idx); + if mi_unlikely(ofs == 0) return NULL; + return (mi_page_t*)((((uintptr_t)p >> MI_ARENA_SLICE_SHIFT) - ofs + 1) << MI_ARENA_SLICE_SHIFT); +} + +mi_decl_nodiscard mi_decl_export bool mi_is_in_heap_region(const void* p) mi_attr_noexcept { + return (_mi_safe_ptr_page(p) != NULL); +} + +#else + +// A 2-level page map +#define MI_PAGE_MAP_SUB_SIZE (MI_PAGE_MAP_SUB_COUNT * sizeof(mi_page_t*)) + +// Use an initial empty page map so `free(NULL)` works even if mimalloc is not yet initialized (issue #1341) +static mi_page_map_t mi_page_map_empty = { + MI_ATOMIC_VAR_INIT(1), + sizeof(mi_page_map_t), + MI_MEMID_STATIC, + MI_LOCK_INITIALIZER, + { MI_ATOMIC_VAR_INIT(NULL) } +}; + +mi_decl_hidden mi_decl_cache_align _Atomic(mi_page_map_t*) __mi_page_map = MI_ATOMIC_VAR_INIT(&mi_page_map_empty); + +static size_t mi_page_map_count_of_size(size_t size) { + return (size < sizeof(mi_page_map_t) ? 0 : 1 + (size - sizeof(mi_page_map_t))/sizeof(mi_submap_t)); +} + +// static void* mi_page_map_addr_of_index(size_t idx) { +// return (void*)((uintptr_t)idx * MI_PAGE_MAP_SUB_COUNT * MI_ARENA_SLICE_SIZE); +// } + +mi_decl_nodiscard static mi_decl_noinline bool mi_page_map_commit_entries(mi_page_map_t* pmap, size_t required_idx) { + const size_t reserved_count = mi_page_map_count_of_size(pmap->reserved_size); + if mi_unlikely(required_idx >= reserved_count) { + mi_page_map_cannot_commit(); + return false; + } + size_t commit_size = mi_align_up( sizeof(mi_page_map_t) + (required_idx * sizeof(mi_submap_t)), MI_ARENA_SLICE_SIZE ); + if (pmap->reserved_size < commit_size) { commit_size = pmap->reserved_size; } + const size_t commit_count = mi_page_map_count_of_size(commit_size); + mi_assert_internal(commit_count > required_idx); + mi_assert_internal(commit_count <= reserved_count); + // note: we rely on uncommitted memory to be zero initialized on the first commit (and further concurrent commits leave the memory as is). + bool is_zero; + if mi_unlikely(!_mi_os_commit(_mi_subproc_main(), pmap, commit_size, &is_zero)) { + mi_page_map_cannot_commit(); + return false; + } + mi_assert_internal(is_zero || pmap->memid.initially_zero); + mi_atomic_store_release(&pmap->committed_count, commit_count); + // mi_atomic_store_release(&pmap->committed_addr, mi_page_map_addr_of_index(commit_count)); + return true; +} + +mi_decl_nodiscard static bool mi_page_map_ensure_committed(mi_page_map_t* pmap, size_t idx, mi_submap_t* submap) { + mi_assert_internal(submap!=NULL && *submap==NULL); + if mi_unlikely(idx >= mi_atomic_load_relaxed(&pmap->committed_count)) { + if (idx >= mi_atomic_load_acquire(&pmap->committed_count)) { + if (!mi_page_map_commit_entries(pmap,idx)) return false; + mi_assert_internal(idx < mi_atomic_load_relaxed(&pmap->committed_count)); + } + } + *submap = mi_atomic_load_ptr_acquire(mi_page_t*, &pmap->submaps[idx]); + return true; +} + +// initialize the page map +static bool mi_page_map_init_once(void) { + size_t vbits = (size_t)mi_option_get_clamp(mi_option_max_vabits, 0, MI_MAX_VABITS); + if (vbits == 0) { + vbits = _mi_os_virtual_address_bits(); + #if MI_ARCH_X64 // canonical address is limited to the first 128 TiB + if (vbits >= 48) { vbits = 47; } + #endif + } + if (vbits < MI_PAGE_MAP_SUB_SHIFT + MI_ARENA_SLICE_SHIFT) { + vbits = MI_PAGE_MAP_SUB_SHIFT + MI_ARENA_SLICE_SHIFT; + } + if (vbits < MI_MIN_VABITS) { // cover at least this much for a faster _mi_checked_ptr + vbits = MI_MIN_VABITS; + } + if (vbits > MI_MAX_VABITS) { // limit page map size even if more virtual addresses are available + vbits = MI_MAX_VABITS; + } + + // Allocate the page map and commit bits + mi_assert(MI_MAX_VABITS >= vbits); + mi_assert(MI_MIN_VABITS <= vbits); + const size_t reserve_count = (MI_ZU(1) << (vbits - MI_PAGE_MAP_SUB_SHIFT - MI_ARENA_SLICE_SHIFT)); + const size_t os_page_size = _mi_os_page_size(); + const size_t reserve_size = mi_align_up( sizeof(mi_page_map_t) + ((reserve_count - 1) * sizeof(mi_submap_t)), os_page_size); + const size_t submap_size = MI_PAGE_MAP_SUB_SIZE; + const size_t extra_reserve_size = reserve_size + submap_size; + const bool commit = (vbits == MI_MIN_VABITS) || (reserve_size <= 64*MI_KiB) || // 42 virtual address bits + mi_option_is_enabled(mi_option_pagemap_commit) || _mi_os_has_overcommit(); + mi_subproc_t* const subproc = _mi_subproc_main(); + mi_memid_t memid; + mi_page_map_t* const pmap = (mi_page_map_t*)_mi_os_alloc_aligned(subproc, extra_reserve_size, 1, commit, true /* allow large */, &memid); + if mi_unlikely(pmap==NULL) { + _mi_error_message(ENOMEM, "unable to reserve virtual memory for the page map (%zu KiB)\n", extra_reserve_size / MI_KiB); + return false; + } + + // commit + size_t commit_count; + if (memid.initially_committed) { + if (!memid.initially_zero) { + _mi_warning_message("internal: the page map was committed but not zero initialized!\n"); + mi_memzero_aligned(pmap, extra_reserve_size); + memid.initially_zero = true; + } + commit_count = mi_page_map_count_of_size(reserve_size); + } + else { + // commit first entries up to MI_MIN_VABITS entries + const size_t min_commit_count = (MI_ZU(1) << (MI_MIN_VABITS - MI_PAGE_MAP_SUB_SHIFT - MI_ARENA_SLICE_SHIFT)); + const size_t min_commit_size = mi_align_up( sizeof(mi_page_map_t) + ((min_commit_count-1) * sizeof(mi_submap_t)), os_page_size); + mi_assert_internal(min_commit_size <= reserve_size); + bool is_zero; + if (!_mi_os_commit(subproc,pmap,min_commit_size,&is_zero)) { + mi_page_map_cannot_commit(); + _mi_os_free(subproc,pmap,extra_reserve_size,memid); + return false; + }; + mi_assert_internal(is_zero || memid.initially_zero); + commit_count = mi_page_map_count_of_size(min_commit_size); + mi_assert_internal(commit_count >= min_commit_count); + } + + // ensure there is a submap for the NULL address + mi_page_t** const sub0 = (mi_submap_t)((uint8_t*)pmap + reserve_size); // we reserved a submap part at the end already + if (!memid.initially_committed) { + if (!_mi_os_commit(subproc, sub0, submap_size, NULL)) { // commit full submap (issue #1087) + mi_page_map_cannot_commit(); + _mi_os_free(subproc,pmap,extra_reserve_size,memid); + return false; + } + } + if (!memid.initially_zero) { // initialize low addresses with NULL + mi_memzero_aligned(sub0, submap_size); + } + + // initialize the fields + pmap->memid = memid; + pmap->reserved_size = reserve_size; + mi_lock_init(&pmap->lock); + mi_atomic_store_release(&pmap->committed_count, commit_count); + // mi_atomic_store_release(&pmap->committed_addr, mi_page_map_addr_of_index(commit_count)); + mi_atomic_store_ptr_release(mi_page_t*, &pmap->submaps[0], sub0); + mi_atomic_store_ptr_release(mi_page_map_t, &__mi_page_map, pmap); + mi_assert_internal(mi_ptr_page(NULL)==NULL); + return true; +} + +bool _mi_page_map_init(void) { + bool ok = true; + mi_atomic_do_once { + ok = mi_page_map_init_once(); + } + return ok; +} + +void _mi_page_map_unsafe_destroy(void) { + mi_page_map_t* const pmap = mi_page_map(); + mi_assert_internal(pmap != NULL); + if (pmap == NULL || pmap == &mi_page_map_empty) return; + mi_subproc_t* const subproc = _mi_subproc_main(); + mi_lock_done(&pmap->lock); + for (size_t idx = 1; idx < pmap->committed_count; idx++) { // skip entry 0 (as we allocate that submap at the end of the page_map) + // free all sub-maps + mi_submap_t sub = mi_page_map_at(pmap,idx); + if (sub != NULL) { + mi_memid_t memid = _mi_memid_create_os(sub, MI_PAGE_MAP_SUB_SIZE, true, false, false); + _mi_os_free_ex(subproc, memid.mem.os.base, memid.mem.os.size, true, memid); + mi_atomic_store_ptr_release(mi_page_t*, &pmap->submaps[idx], NULL); + } + } + _mi_os_free_ex(subproc, pmap, pmap->reserved_size, true, pmap->memid); + mi_atomic_store_ptr_release(mi_page_map_t, &__mi_page_map, &mi_page_map_empty); +} + +mi_decl_nodiscard static mi_decl_noinline mi_submap_t mi_page_map_alloc_submap_at(mi_page_map_t* pmap, size_t idx) { + // sub map not yet allocated, alloc now + mi_submap_t sub = NULL; + mi_lock(&pmap->lock) + { + sub = mi_atomic_load_ptr_acquire(mi_page_t*, &pmap->submaps[idx]); // reload + if (sub==NULL) // not yet allocated by another thread? + { + mi_subproc_t* const subproc = _mi_subproc_main(); + mi_memid_t memid; + const size_t submap_size = MI_PAGE_MAP_SUB_SIZE; + sub = (mi_submap_t)_mi_os_zalloc(subproc, submap_size, &memid); + if (sub==NULL) { + _mi_warning_message("internal error: unable to extend the page map\n"); + } + else { + mi_submap_t expect = NULL; + if (!mi_atomic_cas_ptr_strong_acq_rel(mi_page_t*, &pmap->submaps[idx], &expect, sub)) { + // another thread already allocated it.. free and continue + _mi_os_free(subproc, sub, submap_size, memid); + sub = expect; + } + } + } + } + return sub; +} + +mi_decl_nodiscard static bool mi_page_map_ensure_submap_at(mi_page_map_t* pmap, size_t idx, mi_submap_t* submap) { + mi_assert_internal(submap!=NULL && *submap==NULL); + mi_submap_t sub = NULL; + if (!mi_page_map_ensure_committed(pmap, idx, &sub)) { + return false; + } + if mi_unlikely(sub==NULL) { + sub = mi_page_map_alloc_submap_at(pmap, idx); + if (sub==NULL) return false; // unable to allocate the submap.. + } + mi_assert_internal(sub!=NULL); + *submap = sub; + return true; +} + +static bool mi_page_map_set_range_prim(mi_page_map_t* pmap, mi_page_t* page, size_t idx, size_t sub_idx, size_t slice_count) { + // is the page map area that contains the page address committed? + while (slice_count > 0) { + mi_submap_t sub = NULL; + if (!mi_page_map_ensure_submap_at(pmap, idx, &sub)) { + return false; + }; + mi_assert_internal(sub!=NULL); + // set the offsets for the page + while (slice_count > 0 && sub_idx < MI_PAGE_MAP_SUB_COUNT) { + sub[sub_idx] = page; + slice_count--; + sub_idx++; + } + idx++; // potentially wrap around to the next idx + sub_idx = 0; + } + return true; +} + +static bool mi_page_map_set_range(mi_page_map_t* pmap, mi_page_t* page, size_t idx, size_t sub_idx, size_t slice_count) { + if mi_unlikely(!mi_page_map_set_range_prim(pmap, page,idx,sub_idx,slice_count)) { + // failed to commit, call again to reset the page pointer if needed + if (page!=NULL) { + mi_page_map_set_range_prim(pmap,NULL,idx,sub_idx,slice_count); + } + return false; + } + return true; +} + +static size_t mi_page_map_get_idx(mi_page_t* page, size_t* sub_idx, size_t* slice_count) { + size_t page_size; + uint8_t* page_start = mi_page_area(page, &page_size); + if (page_size > MI_LARGE_PAGE_SIZE) { page_size = MI_LARGE_PAGE_SIZE - MI_ARENA_SLICE_SIZE; } // furthest interior pointer + *slice_count = _mi_slice_count_of_size(page_size) + ((page_start - mi_page_slice_start(page))/MI_ARENA_SLICE_SIZE); // add for large aligned blocks + return mi_page_map_index(page_start, sub_idx); +} + +bool _mi_page_map_register(mi_page_t* page) { + mi_assert_internal(page != NULL); + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_page_map_t* pmap = mi_page_map(); + mi_assert_internal(pmap != NULL); // should be initialized before multi-thread access! + if mi_unlikely(pmap == NULL) { + if (!_mi_page_map_init()) return false; + pmap = mi_atomic_load_ptr_acquire(mi_page_map_t,&__mi_page_map); + } + mi_assert(pmap!=NULL); + size_t slice_count; + size_t sub_idx; + const size_t idx = mi_page_map_get_idx(page, &sub_idx, &slice_count); + return mi_page_map_set_range(pmap, page, idx, sub_idx, slice_count); +} + +void _mi_page_map_unregister(mi_page_t* page) { + mi_assert_internal(__mi_page_map != NULL); + mi_assert_internal(page != NULL); + mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN)); + mi_page_map_t* const pmap = mi_page_map(); + // note: should proceed even if the page was not registered yet (for failure paths in page allocation in `arena/arena-page.c`) + if mi_unlikely(pmap == NULL) return; + // get index and count + size_t slice_count; + size_t sub_idx; + const size_t idx = mi_page_map_get_idx(page, &sub_idx, &slice_count); + // unset the offsets + mi_page_map_set_range(pmap, NULL, idx, sub_idx, slice_count); +} + +void _mi_page_map_unregister_range(void* start, size_t size) { + mi_page_map_t* const pmap = mi_page_map(); + if mi_unlikely(pmap == NULL) return; + const size_t slice_count = mi_divide_up(size, MI_ARENA_SLICE_SIZE); + size_t sub_idx; + const uintptr_t idx = mi_page_map_index(start, &sub_idx); + mi_page_map_set_range(pmap, NULL, idx, sub_idx, slice_count); // todo: avoid committing if not already committed? +} + +// Return NULL for invalid pointers +mi_page_t* _mi_safe_ptr_page(const void* p) { + return mi_checked_ptr_page(p); +} + +mi_decl_nodiscard mi_decl_export bool mi_is_in_heap_region(const void* p) mi_attr_noexcept { + return (_mi_safe_ptr_page(p) != NULL); +} + +#endif diff --git a/Objects/mimalloc/heap/readme.md b/Objects/mimalloc/heap/readme.md new file mode 100644 index 000000000000000..66d4b2b77843294 --- /dev/null +++ b/Objects/mimalloc/heap/readme.md @@ -0,0 +1,9 @@ +# Heaps and Initialization + +- [`heap.c`](heap.c): First-class heap creation, destruction, ownership, and management. +- [`theap.c`](theap.c): Thread-local heap instances and page collection. +- [`init.c`](init.c): Process and thread initialization, shutdown, and initial allocator state. +- [`subproc.c`](subproc.c): Subprocess allocation domains, metadata allocation, and subprocess lifecycle management. +- [`page-map.c`](page-map.c): Address-to-page mapping to safely map any pointer +to a valid mimalloc page (or `NULL`). + diff --git a/Objects/mimalloc/heap/subproc.c b/Objects/mimalloc/heap/subproc.c new file mode 100644 index 000000000000000..40f84bf1b4d65d1 --- /dev/null +++ b/Objects/mimalloc/heap/subproc.c @@ -0,0 +1,396 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +#include "mimalloc.h" +#include "mimalloc/prim/prim-tls.h" +#include "mimalloc/util/stats.h" +#include "mimalloc/arena/arena-alloc.h" +#include "mimalloc/arena/arena.h" +#include "mimalloc/heap/page-map.h" +#include "mimalloc/profile/sample-profile.h" +#include "mimalloc/heap/subproc.h" +#include "mimalloc/util/threadlocal.h" + +// pre-allocate the main subprocess structure. +static mi_decl_cache_align mi_subproc_t mi_process_subproc_main = mi_init_struct_zero; +static mi_subproc_t* mi_subprocs = NULL; +static mi_lock_t mi_subprocs_lock = MI_LOCK_INITIALIZER; + + +/* ----------------------------------------------------------- + Meta-data allocation + We allocate thread local data and theaps through a dedicated + theap `subproc.theap_meta` which uses a detached tld with + a detached thread id. The initial theap_meta is statically + allocated and can thus be used to allocate on an as yet + uninitialized thread or process. + We need to take a lock though to allocate safely on the + detached `theap_meta`. +----------------------------------------------------------- */ + +void* _mi_meta_zalloc( mi_subproc_t* subproc, size_t size, mi_memid_t* memid ) { + mi_assert_internal(subproc->theap_meta != NULL); + void* p = NULL; + mi_lock(&subproc->theap_meta_lock) { + p = mi_theap_zalloc(subproc->theap_meta, size); + if (memid != NULL) { *memid = (p==NULL ? _mi_memid_none() : _mi_memid_create_malloc(p,size,true) ); } + } + return p; +} + +void* _mi_meta_zalloc_aligned( mi_subproc_t* subproc, size_t size, size_t aligned, mi_memid_t* memid ) { + mi_assert_internal(subproc->theap_meta != NULL); + void* p = NULL; + mi_lock(&subproc->theap_meta_lock) { + p = mi_theap_zalloc_aligned(subproc->theap_meta, size, aligned); + if (memid != NULL) { *memid = (p==NULL ? _mi_memid_none() : _mi_memid_create_malloc(p,size,true) ); } + } + return p; +} + +void* _mi_meta_rezalloc( mi_subproc_t* subproc, void* oldp, size_t newsize, mi_memid_t* memid ) { + mi_assert_internal(subproc->theap_meta != NULL); + // note: since we take a meta lock we cannot use `mi_theap_rezalloc` as that could call `mi_free` which + // can call `mi_stat_free` which would try to take the meta lock again. See issue #1358. + void* p = NULL; + mi_lock(&subproc->theap_meta_lock) { + p = mi_theap_zalloc(subproc->theap_meta, newsize); + } + if (p!=NULL) { + if (oldp!=NULL) { + const size_t oldsize = mi_usable_size(oldp); + const size_t copysize = (newsize < oldsize ? newsize : oldsize); + mi_memcpy(p,oldp,copysize); + if (memid!=NULL) { _mi_meta_free(subproc,oldp,*memid); } + else { mi_free(oldp); } + } + if (memid!=NULL) { *memid = _mi_memid_create_malloc(p,newsize,true); } + } + else { + if (memid!=NULL) { *memid = _mi_memid_none(); } + } + return p; +} + +void _mi_meta_free(mi_subproc_t* subproc, void* p, mi_memid_t memid) { + if (p==NULL || mi_memid_needs_no_free(memid)) return; + if (memid.memkind == MI_MEM_MALLOC) { + mi_free(p); + } + else { + mi_assert_internal(subproc!=NULL); + _mi_arenas_free(subproc, p, _mi_memid_size(memid), memid); + } +} + +bool _mi_meta_is_meta_page(const mi_subproc_t* subproc, const mi_page_t* page) { + if (page==NULL) return false; + mi_theap_t* theap = page->theap; + return (theap != NULL && theap == subproc->theap_meta); +} + + +/* ----------------------------------------------------------- + Sub process helpers +----------------------------------------------------------- */ + +mi_subproc_t* _mi_subproc_main(void) { + return &mi_process_subproc_main; +} + +bool _mi_subproc_is_main(mi_subproc_t* subproc) { + return (subproc == &mi_process_subproc_main); +} + +mi_subproc_t* _mi_subproc(void) { + mi_theap_t* theap = _mi_theap_default(); + if (theap == NULL || theap->tld == NULL || theap->tld->subproc == NULL) { // see issue #1289 and #1391 + return _mi_subproc_main(); + } + else { + return theap->tld->subproc; + } +} + +mi_heap_t* mi_heap_main(void) { + return _mi_subproc_heap_main(_mi_subproc()); // don't use mi_theap_main_init_get() so this call works during process_init +} + + +mi_subproc_t* _mi_subproc_from_id(mi_subproc_id_t subproc_id) { + return (mi_subproc_t*)(subproc_id._mi_subproc_id); +} + +mi_subproc_id_t _mi_subproc_to_id(mi_subproc_t* subproc) { + mi_subproc_id_t id = { subproc }; + return id; +} + +mi_subproc_id_t mi_subproc_main(void) { + return _mi_subproc_to_id(_mi_subproc_main()); +} + +mi_subproc_id_t mi_subproc_current(void) { + return _mi_subproc_to_id(_mi_subproc()); +} + + +/* ----------------------------------------------------------- + Sub process creation +----------------------------------------------------------- */ + + +static mi_subproc_t* mi_subproc_init(mi_subproc_t* subproc, mi_subproc_t* parent) { + static _Atomic(size_t) subproc_total_count; + subproc->parent = parent; + subproc->subproc_seq = mi_atomic_increment_relaxed(&subproc_total_count); + mi_stats_header_init(&subproc->stats); + mi_lock_init(&subproc->arena_reserve_lock); + mi_lock_init(&subproc->heaps_lock); + mi_lock_init(&subproc->theap_meta_lock); + mi_lock(&mi_subprocs_lock) { + // push on subproc list + subproc->next = mi_subprocs; + if (mi_subprocs!=NULL) { mi_subprocs->prev = subproc; } + mi_subprocs = subproc; + } + return subproc; +} + +mi_subproc_id_t mi_subproc_new(void) { + mi_thread_init(); + mi_subproc_t* const parent = _mi_subproc(); + mi_memid_t memid; + mi_subproc_t* const subproc = (mi_subproc_t*)_mi_meta_zalloc(parent, sizeof(mi_subproc_t), &memid); + if (subproc == NULL) { return _mi_subproc_to_id(NULL); } + subproc->memid = memid; + + mi_memid_t theap_memid; + mi_theap_t* const theap_meta = (mi_theap_t*)_mi_meta_zalloc(parent, sizeof(mi_theap_t), &theap_memid); + if (theap_meta==NULL) { + _mi_meta_free(parent, subproc, memid); + return _mi_subproc_to_id(NULL); + } + theap_meta->memid = memid; + + // init subproc + mi_subproc_init(subproc,parent); + + // init main heap + mi_heap_t* heap_main = _mi_heap_new_for_subproc(subproc,0,true,false); + if (heap_main==NULL) { + _mi_meta_free(parent, theap_meta, theap_meta->memid); + mi_subproc_destroy(_mi_subproc_to_id(subproc)); + return _mi_subproc_to_id(NULL); + } + mi_assert_internal(subproc->heap_main == heap_main); + + // init meta theap + mi_assert_internal(parent->theap_meta!=NULL); + mi_assert_internal(parent->theap_meta->tld!=NULL); + mi_assert_internal(parent->theap_meta->tld->thread_id == MI_THREADID_DETACHED); + _mi_theap_init(theap_meta,heap_main,parent->theap_meta->tld /* detached tld */); + mi_theap_profile_disable(theap_meta); // permanently exclude from profiling, see `mi_theap_profile_disable` + subproc->theap_meta = theap_meta; + + return _mi_subproc_to_id(subproc); +} + + +/* ----------------------------------------------------------- + Sub process destruction +----------------------------------------------------------- */ + +// destroy all subproc resources including arena's, heap's etc. +static void mi_subproc_unsafe_destroy(mi_subproc_t* subproc, bool acquire_subprocs_lock) +{ + if (subproc==NULL) return; + + // remove from the subproc list + mi_lock_maybe(&mi_subprocs_lock, acquire_subprocs_lock) { + if (subproc->next!=NULL) { subproc->next->prev = subproc->prev; } + if (subproc->prev!=NULL) { subproc->prev->next = subproc->next; } + else { mi_assert_internal(mi_subprocs==subproc); mi_subprocs = subproc->next; } + } + + // destroy all subproc heaps + mi_lock(&subproc->heaps_lock) { + mi_heap_t* heap = subproc->heaps; + while (heap != NULL) { + mi_heap_t* next = heap->next; + if (heap!=subproc->heap_main) { _mi_heap_force_destroy(heap, false /* don't re-acquire the heaps_lock */); } + heap = next; + } + mi_assert_internal(subproc->heap_main==NULL || subproc->heaps == subproc->heap_main); + if (subproc->heap_main!=NULL) { + _mi_thread_locals_thread_done(); // release thread locals that may have been allocated (safe as the main heap uses the fast key) + if (_mi_subproc_is_main(subproc)) { + _mi_thread_locals_done(); + } + _mi_heap_force_destroy(subproc->heap_main, false /* don't re-acquire the heaps_lock */); // no warning if destroying the main heap + } + } + + subproc->theap_meta = NULL; // theap meta stats are merged during heap_destroy of the main heap + + if (!_mi_subproc_is_main(subproc)) { + // merge stats back into the main subproc + _mi_stats_merge_into(&mi_process_subproc_main.stats, &subproc->stats); + } + + // remove associated arenas + _mi_arenas_unsafe_destroy_all(subproc); + + // show stats of the main process (at process end) before releasing the heaps lock + if (_mi_subproc_is_main(subproc)) { + if (mi_option_is_enabled(mi_option_show_stats) || mi_option_is_enabled(mi_option_verbose)) { + mi_subproc_stats_print_out(mi_subproc_main(), NULL, NULL); + } + } + + // todo: should we refcount subprocesses? + mi_lock_done(&subproc->arena_reserve_lock); + mi_lock_done(&subproc->heaps_lock); + mi_lock_done(&subproc->theap_meta_lock); + _mi_meta_free( subproc->parent, subproc, subproc->memid); + if (_mi_subproc_is_main(subproc)) { + // for the main subproc, also release the global page map + _mi_page_map_unsafe_destroy(); + } +} + +void mi_subproc_destroy(mi_subproc_id_t subproc_id) { + mi_subproc_t* subproc = _mi_subproc_from_id(subproc_id); + if (subproc==NULL || subproc==&mi_process_subproc_main) return; + mi_subproc_unsafe_destroy(subproc, true /* take lock */); +} + +void _mi_subprocs_unsafe_destroy_all(void) { + mi_lock(&mi_subprocs_lock) { + mi_subproc_t* subproc = mi_subprocs; + while (subproc!=NULL) { + mi_subproc_t* next = subproc->next; + if (subproc!=&mi_process_subproc_main) { + mi_subproc_unsafe_destroy(subproc, false /* take mi_subprocs lock */); + } + subproc = next; + } + } + mi_subproc_unsafe_destroy(&mi_process_subproc_main, true /* take mi_subprocs lock */); +} + + +/* ----------------------------------------------------------- + Sub process various +----------------------------------------------------------- */ + +void mi_subproc_add_current_thread(mi_subproc_id_t subproc_id) { + mi_subproc_t* subproc = _mi_subproc_from_id(subproc_id); + mi_assert_internal(subproc!=NULL); + if (subproc==NULL) return; + mi_assert_internal(subproc->heap_main!=NULL); + if (subproc->heap_main==NULL) return; + mi_theap_t* theap = _mi_theap_default(); + if (mi_theap_is_initialized(theap)) { + if (theap->tld!=NULL && theap->tld->subproc != subproc) { + _mi_warning_message("unable to add thread to the subprocess as it was already in another subprocess (at %p)\n", theap->tld->subproc); + } + return; + } + + // initialize this thread tld & theap + _mi_thread_init_with_heap(subproc->heap_main); +} + + +bool mi_subproc_visit_heaps(mi_subproc_id_t subproc_id, mi_heap_visit_fun* visitor, void* arg) { + mi_subproc_t* subproc = _mi_subproc_from_id(subproc_id); + if (subproc==NULL) return false; + bool ok = true; + mi_lock(&subproc->heaps_lock) { + for (mi_heap_t* heap = subproc->heaps; heap!=NULL && ok; heap = heap->next) { + ok = (*visitor)(heap, arg); + } + } + return ok; +} + + +mi_subproc_t* _mi_subproc_main_init(void) { + mi_lock_init(&mi_subprocs_lock); + mi_memid_t memid = _mi_memid_create_static(&mi_process_subproc_main,sizeof(mi_subproc_t)); + mi_process_subproc_main.memid = memid; + mi_subproc_init(&mi_process_subproc_main,NULL); + return &mi_process_subproc_main; +} + +void _mi_subproc_main_done(void) { + mi_lock_done(&mi_subprocs_lock); +} + + +/* ----------------------------------------------------------- + Fork support + + `fork()` is only safe if no other thread is executing inside mimalloc + at the time of the fork; all other threads must be quiesced at a safe + point outside mimalloc (as is done for example by CPython's stop-the-world). + The child then inherits consistent data structures with no locks held + (as no thread was inside mimalloc) so we only need to abandon the pages + of the threads that do not exist in the child. +----------------------------------------------------------- */ + +// Abandon the pages of theaps whose threads do not exist in the child. +// The theap and tld memory itself is not freed as the (dead) thread lists may be inconsistent. +// We need to take no locks as at this point only the child process runs in a single thread. +static void mi_fork_child_abandon_theaps(void) { + mi_theap_t* const theap_default = _mi_theap_default(); + mi_tld_t* const tld_current = (mi_theap_is_initialized(theap_default) ? theap_default->tld : NULL); + mi_theap_t* dead = NULL; + for (mi_subproc_t* subproc = mi_subprocs; subproc != NULL; subproc = subproc->next) { + for (mi_heap_t* heap = subproc->heaps; heap != NULL; heap = heap->next) { + mi_theap_t* theap = heap->theaps; + while (theap != NULL) { + mi_theap_t* const next = theap->hnext; + mi_tld_t* const tld = theap->tld; + if (tld != NULL && tld != tld_current && tld->thread_id != MI_THREADID_DETACHED) { + // remove from the heap list and push on the dead list + if (theap->hnext != NULL) { theap->hnext->hprev = theap->hprev; } + if (theap->hprev != NULL) { theap->hprev->hnext = theap->hnext; } + else { heap->theaps = theap->hnext; } + theap->hprev = NULL; + theap->hnext = dead; + dead = theap; + } + theap = next; + } + } + } + while (dead != NULL) { + mi_theap_t* const next = dead->hnext; + mi_heap_t* const heap = mi_theap_heap(dead); + dead->hnext = NULL; + dead->tld = NULL; + _mi_theap_abandon_owned_pages(dead); + _mi_stats_merge_into(&heap->stats, &dead->stats); + mi_atomic_store_ptr_release(mi_heap_t, &dead->heap, NULL); + dead = next; + } +} + +void _mi_fork_child(void) { + mi_fork_child_abandon_theaps(); +} + +// Called at process initialization. +void _mi_fork_init(void) { + #if defined(MI_USE_PTHREADS) && !defined(__wasi__) && !defined(__EMSCRIPTEN__) && !MI_NO_PTHREAD_ATFORK + if (pthread_atfork(NULL, NULL, &_mi_fork_child) != 0) { + _mi_warning_message("unable to register the pthread fork handler\n"); + } + #endif +} \ No newline at end of file diff --git a/Objects/mimalloc/heap/theap.c b/Objects/mimalloc/heap/theap.c new file mode 100644 index 000000000000000..d6086498cb47ec1 --- /dev/null +++ b/Objects/mimalloc/heap/theap.c @@ -0,0 +1,777 @@ +/*---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +#include "mimalloc.h" +#include "mimalloc/prim.h" // _mi_prim_thread_yield +#include "mimalloc/prim/prim-tls.h" // _mi_theap_default +#include "mimalloc/util/random.h" +#include "mimalloc/util/stats.h" +#include "mimalloc/alloc/page.h" +#include "mimalloc/arena/arena-alloc.h" +#include "mimalloc/profile/sample-guarded.h" +#include "mimalloc/heap/heap.h" +#include "mimalloc/heap/theap.h" + +#if defined(_MSC_VER) && (_MSC_VER < 1920) +#pragma warning(disable:4204) // non-constant aggregate initializer +#endif + +/* ----------------------------------------------------------- + Helpers +----------------------------------------------------------- */ + +// return `true` if ok, `false` to break +typedef bool (theap_page_visitor_fun)(mi_theap_t* theap, mi_page_queue_t* pq, mi_page_t* page, void* arg1, void* arg2); + +// Visit all pages in a theap; returns `false` if break was called. +static bool mi_theap_visit_pages(mi_theap_t* theap, theap_page_visitor_fun* fn, bool include_full, void* arg1, void* arg2) +{ + if (theap==NULL || theap->page_count==0) return true; + + // visit all pages + #if MI_DEBUG>1 + size_t total = theap->page_count; + size_t count = 0; + #endif + + const size_t max_bin = (include_full ? MI_BIN_FULL : MI_BIN_FULL - 1); + for (size_t i = 0; i <= max_bin; i++) { + mi_page_queue_t* pq = &theap->pages[i]; + mi_page_t* page = pq->first; + while(page != NULL) { + mi_page_t* next = page->next; // save next in case the page gets removed from the queue + mi_assert_internal(mi_page_theap(page) == theap); + #if MI_DEBUG>1 + count++; + #endif + if (!fn(theap, pq, page, arg1, arg2)) return false; + page = next; // and continue + } + } + mi_assert_internal(!include_full || count == total); + return true; +} + + +#if MI_DEBUG>=3 +static bool mi_theap_page_is_valid(mi_theap_t* theap, mi_page_queue_t* pq, mi_page_t* page, void* arg1, void* arg2) { + MI_UNUSED(arg1); + MI_UNUSED(arg2); + MI_UNUSED(pq); + mi_assert_internal(mi_page_theap(page) == theap); + mi_theap_t* const page_theap = _mi_heap_theap_peek(page->heap); + mi_assert_internal(page_theap == NULL || theap == page_theap || mi_theap_is_detached(theap)); + mi_assert_expensive(_mi_page_is_valid(page)); + return true; +} + +static bool mi_theap_is_valid(mi_theap_t* theap) { + mi_assert_internal(theap!=NULL); + mi_heap_t* const heap = mi_theap_heap_peek(theap); + mi_assert_internal(heap != NULL); + mi_theap_t* const heap_theap = _mi_heap_theap_peek(heap); // don't use mi_heap_theap as that may re-initialize the thread + // Detached metadata theaps are shared and need not match the calling thread's TLS. + mi_assert_internal(heap_theap==NULL || heap_theap == theap || mi_theap_is_detached(theap)); + mi_theap_visit_pages(theap, &mi_theap_page_is_valid, true, NULL, NULL); + for (size_t bin = 0; bin < MI_BIN_COUNT; bin++) { + mi_assert_internal(_mi_page_queue_is_valid(theap, &theap->pages[bin])); + } + return true; +} +#endif + + + + +/* ----------------------------------------------------------- + "Collect" pages by migrating `local_free` and `thread_free` + lists and freeing empty pages. This is done when a thread + stops (and in that case abandons pages if there are still + blocks alive) +----------------------------------------------------------- */ + +typedef enum mi_collect_e { + MI_NORMAL, + MI_FORCE, + MI_ABANDON +} mi_collect_t; + + +static bool mi_theap_page_collect(mi_theap_t* theap, mi_page_queue_t* pq, mi_page_t* page, void* arg_collect, void* arg2 ) { + MI_UNUSED(arg2); + MI_UNUSED(theap); + mi_assert_expensive(mi_theap_page_is_valid(theap, pq, page, NULL, NULL)); + mi_collect_t collect = *((mi_collect_t*)arg_collect); + _mi_page_free_collect(page, collect >= MI_FORCE); // update used count + _mi_page_update_stats(page); + if (mi_page_all_free(page)) { + // no more used blocks, possibly free the page. + if (collect >= MI_FORCE || page->retire_expire == 0) { // either forced/abandon, or not already retired + // note: this will potentially free retired pages as well. + if (_mi_page_free(page, pq)) return true; + } + } + if (collect == MI_ABANDON) { + _mi_page_abandon(page, pq); + } + return true; // don't break +} + +void _mi_theap_merge_stats(mi_theap_t* theap) { + mi_assert_internal(mi_theap_is_initialized(theap)); + mi_heap_t* const heap = mi_theap_heap(theap); + _mi_stats_merge_into(&heap->stats, &theap->stats); +} + +static void mi_theap_collect_ex(mi_theap_t* theap, mi_collect_t collect) +{ + if (theap==NULL || !mi_theap_is_initialized(theap)) return; + mi_assert_expensive(mi_theap_is_valid(theap)); + + const bool force = (collect >= MI_FORCE); + if (collect != MI_ABANDON) { // skip on thread shutdown to avoid running user-code after process exit (issue #1377) + _mi_deferred_free(theap, force); + } + + // python/cpython#112532: we may be called from a thread that is not the owner of the theap + // const bool is_main_thread = (_mi_is_main_thread() && theap->thread_id == _mi_thread_id()); + + // collect retired pages (and full pages if theap->allow_page_abandon is false) + _mi_theap_collect_retired(theap, force); + + // collect all pages owned by this thread + mi_theap_visit_pages(theap, &mi_theap_page_collect, (collect!=MI_NORMAL), &collect, NULL); // dont normally visit full pages, see issue #1220 + + // collect arenas (this is program wide so don't force purges on abandonment of threads) + //mi_atomic_storei64_release(&theap->tld->subproc->purge_expire, 1); + if (collect != MI_ABANDON) { + _mi_arenas_collect(collect == MI_FORCE /* force purge? */, collect >= MI_FORCE /* visit all? */, theap->tld); + } + + // merge statistics + if (mi_option_is_enabled(mi_option_stats_merge_on_collect)) { + _mi_theap_merge_stats(theap); + } +} + +void _mi_theap_collect_abandon(mi_theap_t* theap) { + mi_theap_collect_ex(theap, MI_ABANDON); +} + +// Abandon all owned pages of a valid but detached theap (`tld==NULL`) into the heap-wide registries. +void _mi_theap_abandon_owned_pages(mi_theap_t* theap) { + mi_assert_internal(theap!=NULL); + mi_assert_internal(theap->tld == NULL); // should be detached as we may access from another thread + if (theap==NULL) return; + for (size_t bin = 0; bin <= MI_BIN_FULL; bin++) { + mi_page_queue_t* const pq = &theap->pages[bin]; + mi_page_t* page = pq->first; + while (page != NULL) { + mi_page_t* const next = page->next; // save as the page is removed from the queue + _mi_page_free_collect(page, true); // collect cross-thread frees so `used` is precise + if (!mi_page_all_free(page) || !_mi_page_free(page, pq)) { + _mi_page_abandon(page, pq); + } + page = next; + } + } +} + +void mi_theap_collect(mi_theap_t* theap, bool force) mi_attr_noexcept { + mi_theap_collect_ex(theap, (force ? MI_FORCE : MI_NORMAL)); +} + +void mi_collect(bool force) mi_attr_noexcept { + // cannot really collect process wide, just a theap.. + mi_theap_collect(_mi_theap_default(), force); +} + +void mi_heap_collect(mi_heap_t* heap, bool force) { + // cannot really collect a heap, just a theap.. + mi_theap_collect(mi_heap_theap(heap), force); +} + +/* ----------------------------------------------------------- + Heap new +----------------------------------------------------------- */ + +mi_theap_t* mi_theap_get_default(void) { + mi_theap_t* theap = _mi_theap_default(); + if mi_unlikely(!mi_theap_is_initialized(theap)) { + mi_thread_init(); + theap = _mi_theap_default(); + mi_assert_internal(mi_theap_is_initialized(theap)); + } + return theap; +} + +mi_theap_t* mi_theap_set_default(mi_theap_t* theap) { + mi_theap_t* const previous = mi_theap_get_default(); + if (mi_theap_is_initialized(theap)) { + _mi_theap_default_set(theap); + } + return previous; +} + + +static void mi_theap_options_init(mi_theap_t* theap, mi_heap_t* heap) { + theap->allow_page_reclaim = (mi_heap_reclaim_on_free(heap) >= 0); + theap->allow_page_abandon = (mi_option_get(mi_option_page_full_retain) >= 0 && !mi_heap_qsbr_is_enabled(heap)); + theap->page_full_retain = mi_option_get_clamp(mi_option_page_full_retain, -1, 32); + theap->is_detached = (theap->tld->thread_id == MI_THREADID_DETACHED); +} + +// todo: make order of parameters consistent (but would that break compat with CPython?) +void _mi_theap_init(mi_theap_t* theap, mi_heap_t* heap, mi_tld_t* tld) +{ + mi_assert_internal(theap!=NULL); + mi_assert_internal(heap!=NULL); + mi_assert_internal(tld!=NULL); + mi_memid_t memid = theap->memid; + mi_memcpy_aligned(theap, &_mi_theap_empty, sizeof(mi_theap_t)); + theap->memid = memid; + theap->tld = tld; // avoid reading the thread-local tld during initialization + mi_atomic_store_release(&theap->refcount,1); + mi_atomic_store_ptr_release(mi_subproc_t,&theap->subproc,heap->subproc); + mi_assert_internal(theap->stats.size == sizeof(mi_stats_t)); + mi_theap_options_init(theap, heap); + + if (theap->tld->is_in_threadpool) { + // if we run as part of a thread pool it is better to not arbitrarily reclaim abandoned pages into our theap. + // this is checked in `alloc/free.c:mi_free_try_collect_mt` + // .. but abandoning is good in this case: quarter the full page retain (possibly to 0) + // (so blocked threads do not hold on to too much memory) + if (theap->page_full_retain > 0) { + theap->page_full_retain = theap->page_full_retain / 4; + } + } + + // push on the thread local theaps list + mi_theap_t* head = NULL; + mi_random_ctx_t head_random; + mi_lock(&theap->tld->theaps_lock) { + head = theap->tld->theaps; + theap->tprev = NULL; + theap->tnext = head; + theap->tld->theaps = theap; + if (head!=NULL) { + head->tprev = theap; + head_random = head->random; + } + } + + // initialize random if heap==NULL + if (head==NULL) { // first theap of the first thread? + // will be initialized on the first random_next/split call. (see #1377, W20) + + // #if defined(_WIN32) && !defined(MI_SHARED_LIB) + // if (tld->thread_seq==0) { + // _mi_random_init_weak(&theap->random); // prevent allocation failure during bcrypt dll initialization with static linking (issue #1185) + // } + // else + // #endif + // { + // _mi_random_init(&theap->random); + // } + + } + else { + _mi_random_split(&head_random, &theap->random); // &theap->random is used as nonce so it is ok if threads capture the same head->random + } + // theap->cookie = _mi_theap_random_next(theap) | 1; + _mi_theap_guarded_init(theap); // needs theap->random + if (!theap->is_detached) { + mi_subproc_stat_increase(mi_theap_subproc(theap),theaps,1); // on subproc to match theap_free_mem + } + + // only now set the heap member as it is used to determine if a theap is initialized + mi_atomic_store_ptr_release(mi_heap_t,&theap->heap,heap); + + // push on the heap's theap list + mi_lock(&heap->theaps_lock) { + head = heap->theaps; + theap->hprev = NULL; + theap->hnext = head; + if (head!=NULL) { head->hprev = theap; } + heap->theaps = theap; + } +} + +mi_theap_t* _mi_theap_alloc(mi_heap_t* heap, mi_tld_t* tld) { + mi_assert_internal(tld!=NULL); + mi_assert_internal(heap!=NULL); + mi_assert_internal(tld->thread_id == MI_THREADID_DETACHED || _mi_thread_id() == tld->thread_id); + // mi_assert_internal(_mi_heap_theap_peek(heap)==NULL); // don't access thread locals as this is called on thread init + + // allocate and initialize a theap + mi_memid_t memid; + mi_theap_t* theap; + + if (heap->exclusive_arena == NULL) { + theap = (mi_theap_t*)_mi_meta_zalloc(heap->subproc, sizeof(mi_theap_t), &memid); + } + else { + // theaps associated with a specific arena are allocated in that arena + // note: takes up at least one slice which is quite wasteful... + const size_t size = mi_align_up(sizeof(mi_theap_t),MI_ARENA_MIN_OBJ_SIZE); + theap = (mi_theap_t*)_mi_arenas_alloc(heap, size, true, true, heap->exclusive_arena, tld->thread_seq, tld->numa_node, &memid); + } + if (theap==NULL) { + _mi_error_message(ENOMEM, "unable to allocate theap meta-data\n"); + return NULL; + } + + theap->memid = memid; + return theap; +} + +mi_theap_t* _mi_theap_create(mi_heap_t* heap, mi_tld_t* tld) { + mi_theap_t* theap = _mi_theap_alloc(heap,tld); + if (theap == NULL) return NULL; + _mi_theap_init(theap, heap, tld); + return theap; +} + +size_t _mi_theap_random_next(mi_theap_t* theap) { + return _mi_random_next(&theap->random); +} + +static void mi_theap_free_mem(mi_theap_t* theap) { + if (theap!=NULL) { + #if MI_QSBR + mi_assert_internal(theap->qsbr_first == NULL && theap->qsbr_last == NULL); + #endif + mi_subproc_t* const subproc = mi_atomic_load_ptr_relaxed(mi_subproc_t,&theap->subproc); + if (!theap->is_detached) { + mi_subproc_stat_decrease(subproc,theaps,1); + } + _mi_meta_free(subproc, theap, theap->memid); + } +} + +// we need to reference count theaps due to the _mi_theap_cached thread locals +void _mi_theap_incref(mi_theap_t* theap) { + if (theap!=NULL && !mi_memid_needs_no_free(theap->memid)) { + mi_atomic_increment_acq_rel(&theap->refcount); + } +} + +void _mi_theap_decref(mi_theap_t* theap) { + if (theap!=NULL && !mi_memid_needs_no_free(theap->memid)) { + if (mi_atomic_decrement_acq_rel(&theap->refcount) == 1) { + mi_theap_free_mem(theap); + } + } +} + +// Thread termination and heap delete/destroy might run concurrently +// and we need to ensure we free the memory correctly. A heap or tld +// will first "detach" its theaps so it has a list with theaps that are +// no longer shared, and only then free's the theaps in that list. +// To detach we need to hold both the `heap->theaps_lock` and the `tld->theaps_lock`. +// Due to lock-inversion we need to use `mi_lock_try_acquire` and if that fails +// we back-off, release the outer lock, and try again until we succeed. + +// Both the heap and tld list locks must be held. +static void mi_theap_detach_from_tld(mi_theap_t* theap) { + mi_tld_t* const tld = theap->tld; + if (theap->tnext != NULL) { theap->tnext->tprev = theap->tprev; } + if (theap->tprev != NULL) { theap->tprev->tnext = theap->tnext; } + else { mi_assert_internal(tld->theaps == theap); tld->theaps = theap->tnext; } + theap->tnext = theap->tprev = NULL; + theap->tld = NULL; +} + +static void mi_theap_detach_from_heap(mi_theap_t* theap) { + mi_heap_t* const heap = mi_theap_heap(theap); + if (theap->hnext != NULL) { theap->hnext->hprev = theap->hprev; } + if (theap->hprev != NULL) { theap->hprev->hnext = theap->hnext; } + else { mi_assert_internal(heap->theaps == theap); heap->theaps = theap->hnext; } + theap->hnext = theap->hprev = NULL; + mi_atomic_store_ptr_release(mi_heap_t, &theap->heap, NULL); +} + +// Remove the theaps in this heap from any thread local tld lists. +void _mi_heap_detach_theaps( mi_heap_t* heap ) { + bool all_detached; + do { + all_detached = true; + mi_lock(&heap->theaps_lock) { + mi_theap_t* theap = heap->theaps; + while (theap != NULL) { + mi_theap_t* next = theap->hnext; + mi_tld_t* tld = theap->tld; + if (tld != NULL) { + if (mi_lock_try_acquire(&tld->theaps_lock)) { + mi_theap_detach_from_tld(theap); + mi_lock_release(&tld->theaps_lock); + } + else { + all_detached = false; + } + } + theap = next; + } + } + if (!all_detached) { + mi_subproc_stat_counter_increase(heap->subproc,heaps_delete_wait,1); + _mi_prim_thread_yield(); + } + } while (!all_detached); +} + +// Remove the theaps in this thread from the heaps that own them. +void _mi_tld_detach_theaps( mi_tld_t* tld ) { + bool all_detached; + do { + all_detached = true; + mi_lock(&tld->theaps_lock) { + mi_theap_t* theap = tld->theaps; + while (theap != NULL) { + mi_theap_t* next = theap->tnext; + mi_assert_internal(theap->page_count==0); + mi_heap_t* heap = mi_theap_heap_peek(theap); // now the heap might be NULL from an earlier iteration + if (heap != NULL) { + if (mi_lock_try_acquire(&heap->theaps_lock)) { + // merge stats into the owning heap stats + _mi_stats_merge_into(&heap->stats, &theap->stats); + mi_theap_detach_from_heap(theap); + mi_lock_release(&heap->theaps_lock); + } + else { + all_detached = false; + } + } + theap = next; + } + } + if (!all_detached) { + mi_subproc_stat_counter_increase(tld->subproc,heaps_delete_wait,1); + _mi_prim_thread_yield(); + } + } while (!all_detached); +} + + + +/* ----------------------------------------------------------- + Safe theap delete +----------------------------------------------------------- */ + +void mi_theap_delete(mi_theap_t* theap) mi_attr_noexcept { + if (theap==NULL) return; + // The main theap stays alive even if a concurrent heap delete invalidates our default. + mi_theap_t* const main = _mi_theap_main(); + if (main==NULL) { + _mi_warning_message("cannot delete a theap on an uninitialized thread\n"); + return; + } + mi_tld_t* const tld = main->tld; + if (_mi_theap_default() == theap) { + _mi_theap_default_set(main); + } + + bool found = false; + bool release = false; + mi_lock(&tld->theaps_lock) { + // Do not dereference the argument until list membership protects it from heap deletion. + mi_theap_t* owned = tld->theaps; + while (owned!=NULL && owned!=theap) { owned = owned->tnext; } + if (owned!=NULL) { + found = true; + mi_heap_t* const heap = mi_theap_heap(owned); + mi_assert_internal(heap!=NULL); + mi_lock(&heap->theaps_lock) { + if (owned == main) { + _mi_theap_collect_abandon(owned); // retain the backing theap and tld for the living thread + _mi_theap_merge_stats(owned); + } + else { + const bool cleared = _mi_thread_local_set(heap->theap, NULL); + mi_assert_internal(cleared); MI_UNUSED_RELEASE(cleared); + mi_theap_detach_from_tld(owned); + _mi_theap_abandon_owned_pages(owned); + _mi_theap_merge_stats(owned); + mi_theap_detach_from_heap(owned); + release = true; + } + } + } + } + if (_mi_theap_cached() == theap) { + _mi_theap_cached_set(_mi_theap_empty_get()); + } + if (release) { _mi_theap_decref(theap); } + if (!found) { _mi_verbose_message("theap %p is no longer owned by this thread\n", theap); } +} + + +void mi_theap_quiescent_abandon(mi_theap_t* theap) mi_attr_noexcept { + if (theap==NULL || !mi_theap_is_initialized(theap)) return; + mi_tld_t* const tld = theap->tld; + mi_heap_t* const heap = mi_theap_heap(theap); + mi_lock_maybe(&tld->theaps_lock, tld!=NULL) { + mi_lock(&heap->theaps_lock) { + // Temporarily detach for foreign-thread page cleanup, without changing any TLS references. + theap->tld = NULL; + _mi_theap_abandon_owned_pages(theap); + theap->tld = tld; + _mi_theap_merge_stats(theap); + } + } +} + +/* ----------------------------------------------------------- + Load/unload theaps +----------------------------------------------------------- */ +/* +void mi_theap_unload(mi_theap_t* theap) { + mi_assert(mi_theap_is_initialized(theap)); + mi_assert_expensive(mi_theap_is_valid(theap)); + if (theap==NULL || !mi_theap_is_initialized(theap)) return; + if (mi_theap_heap(theap)->exclusive_arena == NULL) { + _mi_warning_message("cannot unload theaps that are not associated with an exclusive arena\n"); + return; + } + + // abandon all pages so all thread'id in the pages are cleared + _mi_theap_collect_abandon(theap); + mi_assert_internal(theap->page_count==0); + + // remove from theap list + mi_theap_free(theap, false); // but don't actually free the memory + + // disassociate from the current thread-local and static state + theap->tld = NULL; + return; +} + +bool mi_theap_reload(mi_theap_t* theap, mi_arena_id_t arena_id) { + mi_assert(mi_theap_is_initialized(theap)); + if (theap==NULL || !mi_theap_is_initialized(theap)) return false; + if (mi_theap_heap(theap)->exclusive_arena == NULL) { + _mi_warning_message("cannot reload theaps that were not associated with an exclusive arena\n"); + return false; + } + if (theap->tld != NULL) { + _mi_warning_message("cannot reload theaps that were not unloaded first\n"); + return false; + } + mi_arena_t* arena = _mi_arena_from_id(arena_id); + if (mi_theap_heap(theap)->exclusive_arena != arena) { + _mi_warning_message("trying to reload a theap at a different arena address: %p vs %p\n", mi_theap_heap(theap)->exclusive_arena, arena); + return false; + } + + mi_assert_internal(theap->page_count==0); + + // re-associate with the current thread-local and static state + theap->tld = mi_theap_get_default()->tld; + + // reinit direct pages (as we may be in a different process) + mi_assert_internal(theap->page_count == 0); + for (size_t i = 0; i < MI_PAGES_DIRECT; i++) { + theap->pages_free_direct[i] = _mi_page_empty_get(); + } + + // push on the thread local theaps list + theap->tnext = theap->tld->theaps; + theap->tld->theaps = theap; + return true; +} +*/ + + +/* ----------------------------------------------------------- + Visit all theap blocks and areas + Todo: enable visiting abandoned pages, and + enable visiting all blocks of all theaps across threads +----------------------------------------------------------- */ + +void _mi_heap_area_init(mi_heap_area_t* area, mi_page_t* page) { + const size_t bsize = mi_page_block_size(page); + const size_t ubsize = mi_page_usable_block_size(page); + area->reserved = page->reserved * bsize; + area->committed = page->capacity * bsize; + area->blocks = mi_page_start(page); + area->used = mi_page_used(page); // number of blocks in use (#553) + area->block_size = ubsize; + area->full_block_size = bsize; + area->reserved1 = page; +} + +static void mi_get_fast_divisor(size_t divisor, uint64_t* magic, size_t* shift) { + mi_assert_internal(divisor > 0 && divisor <= UINT32_MAX); + *shift = MI_SIZE_BITS - mi_clz(divisor - 1); + *magic = ((((uint64_t)1 << 32) * (((uint64_t)1 << *shift) - divisor)) / divisor + 1); +} + +static size_t mi_fast_divide(size_t n, uint64_t magic, size_t shift) { + mi_assert_internal(n <= UINT32_MAX); + const uint64_t hi = ((uint64_t)n * magic) >> 32; + return (size_t)((hi + n) >> shift); +} + +bool _mi_theap_area_visit_blocks(const mi_heap_area_t* area, mi_page_t* page, mi_block_visit_fun* visitor, void* arg) { + mi_assert(area != NULL); + if (area==NULL) return true; + mi_assert(page != NULL); + if (page == NULL) return true; + + _mi_page_free_collect(page,true); // collect both thread_delayed and local_free + mi_assert_internal(page->local_free == NULL); + if (mi_page_used(page) == 0) return true; + + size_t psize; + uint8_t* const pstart = mi_page_area(page, &psize); + mi_heap_t* const heap = mi_page_heap(page); + const size_t bsize = mi_page_block_size(page); + const size_t ubsize = mi_page_usable_block_size(page); // without padding + + // optimize page with one block + if (page->capacity == 1) { + mi_assert_internal(mi_page_used(page) == 1 && page->free == NULL); + return visitor(heap, area, pstart, ubsize, arg); + } + mi_assert(bsize <= UINT32_MAX); + + // optimize full pages + if (mi_page_used(page) == page->capacity) { + uint8_t* block = pstart; + for (size_t i = 0; i < page->capacity; i++) { + if (!visitor(heap, area, block, ubsize, arg)) return false; + block += bsize; + } + return true; + } + + // create a bitmap of free blocks. + #define MI_MAX_BLOCKS (MI_SMALL_PAGE_SIZE / sizeof(void*)) + uintptr_t free_map[MI_MAX_BLOCKS / MI_INTPTR_BITS]; + const uintptr_t bmapsize = mi_divide_up(page->capacity, MI_INTPTR_BITS); + memset(free_map, 0, bmapsize * sizeof(intptr_t)); + if (page->capacity % MI_INTPTR_BITS != 0) { + // mark left-over bits at the end as free + size_t shift = (page->capacity % MI_INTPTR_BITS); + uintptr_t mask = (UINTPTR_MAX << shift); + free_map[bmapsize - 1] = mask; + } + + // fast repeated division by the block size + uint64_t magic; + size_t shift; + mi_get_fast_divisor(bsize, &magic, &shift); + + #if MI_DEBUG>1 + size_t free_count = 0; + #endif + for (mi_block_t* block = page->free; block != NULL; block = mi_block_next(page, block)) { + #if MI_DEBUG>1 + free_count++; + #endif + mi_assert_internal((uint8_t*)block >= pstart && (uint8_t*)block < (pstart + psize)); + size_t offset = (uint8_t*)block - pstart; + mi_assert_internal(offset % bsize == 0); + mi_assert_internal(offset <= UINT32_MAX); + size_t blockidx = mi_fast_divide(offset, magic, shift); + mi_assert_internal(blockidx == offset / bsize); + mi_assert_internal(blockidx < MI_MAX_BLOCKS); + size_t bitidx = (blockidx / MI_INTPTR_BITS); + size_t bit = blockidx - (bitidx * MI_INTPTR_BITS); + free_map[bitidx] |= ((uintptr_t)1 << bit); + } + mi_assert_internal(page->capacity == (free_count + mi_page_used(page))); + + // walk through all blocks skipping the free ones + #if MI_DEBUG>1 + size_t used_count = 0; + #endif + uint8_t* block = pstart; + for (size_t i = 0; i < bmapsize; i++) { + if (free_map[i] == 0) { + // every block is in use + for (size_t j = 0; j < MI_INTPTR_BITS; j++) { + #if MI_DEBUG>1 + used_count++; + #endif + if (!visitor(heap, area, block, ubsize, arg)) return false; + block += bsize; + } + } + else { + // visit the used blocks in the mask + uintptr_t m = ~free_map[i]; + while (m != 0) { + #if MI_DEBUG>1 + used_count++; + #endif + size_t bitidx = mi_ctz(m); + if (!visitor(heap, area, block + (bitidx * bsize), ubsize, arg)) return false; + m &= m - 1; // clear least significant bit + } + block += bsize * MI_INTPTR_BITS; + } + } + mi_assert_internal(mi_page_used(page) == used_count); + return true; +} + +// bool _mi_page_visit_blocks( mi_page_t* page, mi_block_visit_fun* visitor, void* arg ) { +// mi_heap_area_t area; +// _mi_heap_area_init(&area, page); +// return _mi_theap_area_visit_blocks(&area, page, visitor, arg); +// } + + +// Separate struct to keep `mi_page_t` out of the public interface +typedef struct mi_theap_area_ex_s { + mi_heap_area_t area; + mi_page_t* page; +} mi_theap_area_ex_t; + +typedef bool (mi_theap_area_visit_fun)(const mi_theap_t* theap, const mi_theap_area_ex_t* area, void* arg); + +static bool mi_theap_visit_areas_page(mi_theap_t* theap, mi_page_queue_t* pq, mi_page_t* page, void* vfun, void* arg) { + MI_UNUSED(theap); + MI_UNUSED(pq); + mi_theap_area_visit_fun* fun = (mi_theap_area_visit_fun*)vfun; + mi_theap_area_ex_t xarea; + xarea.page = page; + _mi_page_free_collect(page, true); // collect cross-thread free'd blocks so `area.used` is precise + _mi_heap_area_init(&xarea.area, page); + return fun(theap, &xarea, arg); +} + +// Visit all theap pages as areas +static bool mi_theap_visit_areas(const mi_theap_t* theap, mi_theap_area_visit_fun* visitor, void* arg) { + if (visitor == NULL) return false; + return mi_theap_visit_pages((mi_theap_t*)theap, &mi_theap_visit_areas_page, true, (void*)(visitor), arg); // note: function pointer to void* :-{ +} + +// Just to pass arguments +typedef struct mi_visit_blocks_args_s { + bool visit_blocks; + mi_block_visit_fun* visitor; + void* arg; +} mi_visit_blocks_args_t; + +static bool mi_theap_area_visitor(const mi_theap_t* theap, const mi_theap_area_ex_t* xarea, void* arg) { + mi_visit_blocks_args_t* args = (mi_visit_blocks_args_t*)arg; + if (!args->visitor(mi_theap_heap(theap), &xarea->area, NULL, xarea->area.block_size, args->arg)) return false; + if (args->visit_blocks) { + return _mi_theap_area_visit_blocks(&xarea->area, xarea->page, args->visitor, args->arg); + } + else { + return true; + } +} + +// Visit all blocks in a theap +bool mi_theap_visit_blocks(const mi_theap_t* theap, bool visit_blocks, mi_block_visit_fun* visitor, void* arg) { + mi_visit_blocks_args_t args = { visit_blocks, visitor, arg }; + return mi_theap_visit_areas(theap, &mi_theap_area_visitor, &args); +} diff --git a/Objects/mimalloc/init.c b/Objects/mimalloc/init.c deleted file mode 100644 index 7711c827a58b1ca..000000000000000 --- a/Objects/mimalloc/init.c +++ /dev/null @@ -1,687 +0,0 @@ -/* ---------------------------------------------------------------------------- -Copyright (c) 2018-2022, Microsoft Research, Daan Leijen -This is free software; you can redistribute it and/or modify it under the -terms of the MIT license. A copy of the license can be found in the file -"LICENSE" at the root of this distribution. ------------------------------------------------------------------------------*/ -#include "mimalloc.h" -#include "mimalloc/internal.h" -#include "mimalloc/prim.h" - -#include // memcpy, memset -#include // atexit - - -// Empty page used to initialize the small free pages array -const mi_page_t _mi_page_empty; - -#define MI_PAGE_EMPTY() ((mi_page_t*)&_mi_page_empty) - -#if (MI_SMALL_WSIZE_MAX==128) -#if (MI_PADDING>0) && (MI_INTPTR_SIZE >= 8) -#define MI_SMALL_PAGES_EMPTY { MI_INIT128(MI_PAGE_EMPTY), MI_PAGE_EMPTY(), MI_PAGE_EMPTY() } -#elif (MI_PADDING>0) -#define MI_SMALL_PAGES_EMPTY { MI_INIT128(MI_PAGE_EMPTY), MI_PAGE_EMPTY(), MI_PAGE_EMPTY(), MI_PAGE_EMPTY() } -#else -#define MI_SMALL_PAGES_EMPTY { MI_INIT128(MI_PAGE_EMPTY), MI_PAGE_EMPTY() } -#endif -#else -#error "define right initialization sizes corresponding to MI_SMALL_WSIZE_MAX" -#endif - -// Empty page queues for every bin -#define QNULL(sz) { NULL, NULL, (sz)*sizeof(uintptr_t) } -#define MI_PAGE_QUEUES_EMPTY \ - { QNULL(1), \ - QNULL( 1), QNULL( 2), QNULL( 3), QNULL( 4), QNULL( 5), QNULL( 6), QNULL( 7), QNULL( 8), /* 8 */ \ - QNULL( 10), QNULL( 12), QNULL( 14), QNULL( 16), QNULL( 20), QNULL( 24), QNULL( 28), QNULL( 32), /* 16 */ \ - QNULL( 40), QNULL( 48), QNULL( 56), QNULL( 64), QNULL( 80), QNULL( 96), QNULL( 112), QNULL( 128), /* 24 */ \ - QNULL( 160), QNULL( 192), QNULL( 224), QNULL( 256), QNULL( 320), QNULL( 384), QNULL( 448), QNULL( 512), /* 32 */ \ - QNULL( 640), QNULL( 768), QNULL( 896), QNULL( 1024), QNULL( 1280), QNULL( 1536), QNULL( 1792), QNULL( 2048), /* 40 */ \ - QNULL( 2560), QNULL( 3072), QNULL( 3584), QNULL( 4096), QNULL( 5120), QNULL( 6144), QNULL( 7168), QNULL( 8192), /* 48 */ \ - QNULL( 10240), QNULL( 12288), QNULL( 14336), QNULL( 16384), QNULL( 20480), QNULL( 24576), QNULL( 28672), QNULL( 32768), /* 56 */ \ - QNULL( 40960), QNULL( 49152), QNULL( 57344), QNULL( 65536), QNULL( 81920), QNULL( 98304), QNULL(114688), QNULL(131072), /* 64 */ \ - QNULL(163840), QNULL(196608), QNULL(229376), QNULL(262144), QNULL(327680), QNULL(393216), QNULL(458752), QNULL(524288), /* 72 */ \ - QNULL(MI_MEDIUM_OBJ_WSIZE_MAX + 1 /* 655360, Huge queue */), \ - QNULL(MI_MEDIUM_OBJ_WSIZE_MAX + 2) /* Full queue */ } - -#define MI_STAT_COUNT_NULL() {0,0,0,0} - -// Empty statistics -#if MI_STAT>1 -#define MI_STAT_COUNT_END_NULL() , { MI_STAT_COUNT_NULL(), MI_INIT32(MI_STAT_COUNT_NULL) } -#else -#define MI_STAT_COUNT_END_NULL() -#endif - -#define MI_STATS_NULL \ - MI_STAT_COUNT_NULL(), MI_STAT_COUNT_NULL(), \ - MI_STAT_COUNT_NULL(), MI_STAT_COUNT_NULL(), \ - MI_STAT_COUNT_NULL(), MI_STAT_COUNT_NULL(), \ - MI_STAT_COUNT_NULL(), MI_STAT_COUNT_NULL(), \ - MI_STAT_COUNT_NULL(), MI_STAT_COUNT_NULL(), \ - MI_STAT_COUNT_NULL(), MI_STAT_COUNT_NULL(), \ - MI_STAT_COUNT_NULL(), MI_STAT_COUNT_NULL(), \ - MI_STAT_COUNT_NULL(), \ - { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, \ - { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } \ - MI_STAT_COUNT_END_NULL() - - -// Empty slice span queues for every bin -#define SQNULL(sz) { NULL, NULL, sz } -#define MI_SEGMENT_SPAN_QUEUES_EMPTY \ - { SQNULL(1), \ - SQNULL( 1), SQNULL( 2), SQNULL( 3), SQNULL( 4), SQNULL( 5), SQNULL( 6), SQNULL( 7), SQNULL( 10), /* 8 */ \ - SQNULL( 12), SQNULL( 14), SQNULL( 16), SQNULL( 20), SQNULL( 24), SQNULL( 28), SQNULL( 32), SQNULL( 40), /* 16 */ \ - SQNULL( 48), SQNULL( 56), SQNULL( 64), SQNULL( 80), SQNULL( 96), SQNULL( 112), SQNULL( 128), SQNULL( 160), /* 24 */ \ - SQNULL( 192), SQNULL( 224), SQNULL( 256), SQNULL( 320), SQNULL( 384), SQNULL( 448), SQNULL( 512), SQNULL( 640), /* 32 */ \ - SQNULL( 768), SQNULL( 896), SQNULL( 1024) /* 35 */ } - - -// -------------------------------------------------------- -// Statically allocate an empty heap as the initial -// thread local value for the default heap, -// and statically allocate the backing heap for the main -// thread so it can function without doing any allocation -// itself (as accessing a thread local for the first time -// may lead to allocation itself on some platforms) -// -------------------------------------------------------- - -mi_decl_cache_align const mi_heap_t _mi_heap_empty = { - NULL, - MI_SMALL_PAGES_EMPTY, - MI_PAGE_QUEUES_EMPTY, - MI_ATOMIC_VAR_INIT(NULL), - 0, // tid - 0, // cookie - 0, // arena id - { 0, 0 }, // keys - { {0}, {0}, 0, true }, // random - 0, // page count - MI_BIN_FULL, 0, // page retired min/max - NULL, // next - false, - 0, - 0 -}; - -#define tld_empty_stats ((mi_stats_t*)((uint8_t*)&tld_empty + offsetof(mi_tld_t,stats))) -#define tld_empty_os ((mi_os_tld_t*)((uint8_t*)&tld_empty + offsetof(mi_tld_t,os))) - -mi_decl_cache_align static const mi_tld_t tld_empty = { - 0, - false, - NULL, NULL, - { MI_SEGMENT_SPAN_QUEUES_EMPTY, 0, 0, 0, 0, tld_empty_stats, tld_empty_os, &_mi_abandoned_default }, // segments - { 0, tld_empty_stats }, // os - { MI_STATS_NULL } // stats -}; - -mi_threadid_t _mi_thread_id(void) mi_attr_noexcept { - return _mi_prim_thread_id(); -} - -// the thread-local default heap for allocation -mi_decl_thread mi_heap_t* _mi_heap_default = (mi_heap_t*)&_mi_heap_empty; - -extern mi_heap_t _mi_heap_main; - -static mi_tld_t tld_main = { - 0, false, - &_mi_heap_main, & _mi_heap_main, - { MI_SEGMENT_SPAN_QUEUES_EMPTY, 0, 0, 0, 0, &tld_main.stats, &tld_main.os, &_mi_abandoned_default }, // segments - { 0, &tld_main.stats }, // os - { MI_STATS_NULL } // stats -}; - -mi_heap_t _mi_heap_main = { - &tld_main, - MI_SMALL_PAGES_EMPTY, - MI_PAGE_QUEUES_EMPTY, - MI_ATOMIC_VAR_INIT(NULL), - 0, // thread id - 0, // initial cookie - 0, // arena id - { 0, 0 }, // the key of the main heap can be fixed (unlike page keys that need to be secure!) - { {0x846ca68b}, {0}, 0, true }, // random - 0, // page count - MI_BIN_FULL, 0, // page retired min/max - NULL, // next heap - false // can reclaim -}; - -bool _mi_process_is_initialized = false; // set to `true` in `mi_process_init`. - -mi_stats_t _mi_stats_main = { MI_STATS_NULL }; - - -static void mi_heap_main_init(void) { - if (_mi_heap_main.cookie == 0) { - _mi_heap_main.thread_id = _mi_thread_id(); - _mi_heap_main.cookie = 1; - #if defined(_WIN32) && !defined(MI_SHARED_LIB) - _mi_random_init_weak(&_mi_heap_main.random); // prevent allocation failure during bcrypt dll initialization with static linking - #else - _mi_random_init(&_mi_heap_main.random); - #endif - _mi_heap_main.cookie = _mi_heap_random_next(&_mi_heap_main); - _mi_heap_main.keys[0] = _mi_heap_random_next(&_mi_heap_main); - _mi_heap_main.keys[1] = _mi_heap_random_next(&_mi_heap_main); - } -} - -mi_heap_t* _mi_heap_main_get(void) { - mi_heap_main_init(); - return &_mi_heap_main; -} - - -/* ----------------------------------------------------------- - Initialization and freeing of the thread local heaps ------------------------------------------------------------ */ - -// note: in x64 in release build `sizeof(mi_thread_data_t)` is under 4KiB (= OS page size). -typedef struct mi_thread_data_s { - mi_heap_t heap; // must come first due to cast in `_mi_heap_done` - mi_tld_t tld; - mi_memid_t memid; // must come last due to zero'ing -} mi_thread_data_t; - - -// Thread meta-data is allocated directly from the OS. For -// some programs that do not use thread pools and allocate and -// destroy many OS threads, this may causes too much overhead -// per thread so we maintain a small cache of recently freed metadata. - -#define TD_CACHE_SIZE (16) -static _Atomic(mi_thread_data_t*) td_cache[TD_CACHE_SIZE]; - -static mi_thread_data_t* mi_thread_data_zalloc(void) { - // try to find thread metadata in the cache - bool is_zero = false; - mi_thread_data_t* td = NULL; - for (int i = 0; i < TD_CACHE_SIZE; i++) { - td = mi_atomic_load_ptr_relaxed(mi_thread_data_t, &td_cache[i]); - if (td != NULL) { - // found cached allocation, try use it - td = mi_atomic_exchange_ptr_acq_rel(mi_thread_data_t, &td_cache[i], NULL); - if (td != NULL) { - break; - } - } - } - - // if that fails, allocate as meta data - if (td == NULL) { - mi_memid_t memid; - td = (mi_thread_data_t*)_mi_os_alloc(sizeof(mi_thread_data_t), &memid, &_mi_stats_main); - if (td == NULL) { - // if this fails, try once more. (issue #257) - td = (mi_thread_data_t*)_mi_os_alloc(sizeof(mi_thread_data_t), &memid, &_mi_stats_main); - if (td == NULL) { - // really out of memory - _mi_error_message(ENOMEM, "unable to allocate thread local heap metadata (%zu bytes)\n", sizeof(mi_thread_data_t)); - } - } - if (td != NULL) { - td->memid = memid; - is_zero = memid.initially_zero; - } - } - - if (td != NULL && !is_zero) { - _mi_memzero_aligned(td, offsetof(mi_thread_data_t,memid)); - } - return td; -} - -static void mi_thread_data_free( mi_thread_data_t* tdfree ) { - // try to add the thread metadata to the cache - for (int i = 0; i < TD_CACHE_SIZE; i++) { - mi_thread_data_t* td = mi_atomic_load_ptr_relaxed(mi_thread_data_t, &td_cache[i]); - if (td == NULL) { - mi_thread_data_t* expected = NULL; - if (mi_atomic_cas_ptr_weak_acq_rel(mi_thread_data_t, &td_cache[i], &expected, tdfree)) { - return; - } - } - } - // if that fails, just free it directly - _mi_os_free(tdfree, sizeof(mi_thread_data_t), tdfree->memid, &_mi_stats_main); -} - -void _mi_thread_data_collect(void) { - // free all thread metadata from the cache - for (int i = 0; i < TD_CACHE_SIZE; i++) { - mi_thread_data_t* td = mi_atomic_load_ptr_relaxed(mi_thread_data_t, &td_cache[i]); - if (td != NULL) { - td = mi_atomic_exchange_ptr_acq_rel(mi_thread_data_t, &td_cache[i], NULL); - if (td != NULL) { - _mi_os_free(td, sizeof(mi_thread_data_t), td->memid, &_mi_stats_main); - } - } - } -} - -// Initialize the thread local default heap, called from `mi_thread_init` -static bool _mi_heap_init(void) { - if (mi_heap_is_initialized(mi_prim_get_default_heap())) return true; - if (_mi_is_main_thread()) { - // mi_assert_internal(_mi_heap_main.thread_id != 0); // can happen on freeBSD where alloc is called before any initialization - // the main heap is statically allocated - mi_heap_main_init(); - _mi_heap_set_default_direct(&_mi_heap_main); - //mi_assert_internal(_mi_heap_default->tld->heap_backing == mi_prim_get_default_heap()); - } - else { - // use `_mi_os_alloc` to allocate directly from the OS - mi_thread_data_t* td = mi_thread_data_zalloc(); - if (td == NULL) return false; - - _mi_tld_init(&td->tld, &td->heap); - _mi_heap_init_ex(&td->heap, &td->tld, _mi_arena_id_none(), false, 0); - _mi_heap_set_default_direct(&td->heap); - } - return false; -} - -void _mi_tld_init(mi_tld_t* tld, mi_heap_t* bheap) { - _mi_memcpy_aligned(tld, &tld_empty, sizeof(*tld)); - tld->segments.stats = &tld->stats; - tld->segments.os = &tld->os; - tld->segments.abandoned = &_mi_abandoned_default; - tld->os.stats = &tld->stats; - tld->heap_backing = bheap; -} - -// Free the thread local default heap (called from `mi_thread_done`) -static bool _mi_heap_done(mi_heap_t* heap) { - if (!mi_heap_is_initialized(heap)) return true; - - // reset default heap - _mi_heap_set_default_direct(_mi_is_main_thread() ? &_mi_heap_main : (mi_heap_t*)&_mi_heap_empty); - - // switch to backing heap - heap = heap->tld->heap_backing; - if (!mi_heap_is_initialized(heap)) return false; - - // delete all non-backing heaps in this thread - mi_heap_t* curr = heap->tld->heaps; - while (curr != NULL) { - mi_heap_t* next = curr->next; // save `next` as `curr` will be freed - if (curr != heap) { - mi_assert_internal(!mi_heap_is_backing(curr)); - mi_heap_delete(curr); - } - curr = next; - } - mi_assert_internal(heap->tld->heaps == heap && heap->next == NULL); - mi_assert_internal(mi_heap_is_backing(heap)); - - // collect if not the main thread - if (heap != &_mi_heap_main) { - _mi_heap_collect_abandon(heap); - } - - // merge stats - _mi_stats_done(&heap->tld->stats); - - // free if not the main thread - if (heap != &_mi_heap_main) { - // the following assertion does not always hold for huge segments as those are always treated - // as abondened: one may allocate it in one thread, but deallocate in another in which case - // the count can be too large or negative. todo: perhaps not count huge segments? see issue #363 - // mi_assert_internal(heap->tld->segments.count == 0 || heap->thread_id != _mi_thread_id()); - mi_thread_data_free((mi_thread_data_t*)heap); - } - else { - #if 0 - // never free the main thread even in debug mode; if a dll is linked statically with mimalloc, - // there may still be delete/free calls after the mi_fls_done is called. Issue #207 - _mi_heap_destroy_pages(heap); - mi_assert_internal(heap->tld->heap_backing == &_mi_heap_main); - #endif - } - return false; -} - - - -// -------------------------------------------------------- -// Try to run `mi_thread_done()` automatically so any memory -// owned by the thread but not yet released can be abandoned -// and re-owned by another thread. -// -// 1. windows dynamic library: -// call from DllMain on DLL_THREAD_DETACH -// 2. windows static library: -// use `FlsAlloc` to call a destructor when the thread is done -// 3. unix, pthreads: -// use a pthread key to call a destructor when a pthread is done -// -// In the last two cases we also need to call `mi_process_init` -// to set up the thread local keys. -// -------------------------------------------------------- - -// Set up handlers so `mi_thread_done` is called automatically -static void mi_process_setup_auto_thread_done(void) { - static bool tls_initialized = false; // fine if it races - if (tls_initialized) return; - tls_initialized = true; - _mi_prim_thread_init_auto_done(); - _mi_heap_set_default_direct(&_mi_heap_main); -} - - -bool _mi_is_main_thread(void) { - return (_mi_heap_main.thread_id==0 || _mi_heap_main.thread_id == _mi_thread_id()); -} - -static _Atomic(size_t) thread_count = MI_ATOMIC_VAR_INIT(1); - -size_t _mi_current_thread_count(void) { - return mi_atomic_load_relaxed(&thread_count); -} - -// This is called from the `mi_malloc_generic` -void mi_thread_init(void) mi_attr_noexcept -{ - // ensure our process has started already - mi_process_init(); - - // initialize the thread local default heap - // (this will call `_mi_heap_set_default_direct` and thus set the - // fiber/pthread key to a non-zero value, ensuring `_mi_thread_done` is called) - if (_mi_heap_init()) return; // returns true if already initialized - - _mi_stat_increase(&_mi_stats_main.threads, 1); - mi_atomic_increment_relaxed(&thread_count); - //_mi_verbose_message("thread init: 0x%zx\n", _mi_thread_id()); -} - -void mi_thread_done(void) mi_attr_noexcept { - _mi_thread_done(NULL); -} - -void _mi_thread_done(mi_heap_t* heap) -{ - // calling with NULL implies using the default heap - if (heap == NULL) { - heap = mi_prim_get_default_heap(); - if (heap == NULL) return; - } - - // prevent re-entrancy through heap_done/heap_set_default_direct (issue #699) - if (!mi_heap_is_initialized(heap)) { - return; - } - - // adjust stats - mi_atomic_decrement_relaxed(&thread_count); - _mi_stat_decrease(&_mi_stats_main.threads, 1); - - // check thread-id as on Windows shutdown with FLS the main (exit) thread may call this on thread-local heaps... - if (heap->thread_id != _mi_thread_id()) return; - - // abandon the thread local heap - if (_mi_heap_done(heap)) return; // returns true if already ran -} - -void _mi_heap_set_default_direct(mi_heap_t* heap) { - mi_assert_internal(heap != NULL); - #if defined(MI_TLS_SLOT) - mi_prim_tls_slot_set(MI_TLS_SLOT,heap); - #elif defined(MI_TLS_PTHREAD_SLOT_OFS) - *mi_tls_pthread_heap_slot() = heap; - #elif defined(MI_TLS_PTHREAD) - // we use _mi_heap_default_key - #else - _mi_heap_default = heap; - #endif - - // ensure the default heap is passed to `_mi_thread_done` - // setting to a non-NULL value also ensures `mi_thread_done` is called. - _mi_prim_thread_associate_default_heap(heap); -} - - -// -------------------------------------------------------- -// Run functions on process init/done, and thread init/done -// -------------------------------------------------------- -static void mi_cdecl mi_process_done(void); - -static bool os_preloading = true; // true until this module is initialized -static bool mi_redirected = false; // true if malloc redirects to mi_malloc - -// Returns true if this module has not been initialized; Don't use C runtime routines until it returns false. -bool mi_decl_noinline _mi_preloading(void) { - return os_preloading; -} - -mi_decl_nodiscard bool mi_is_redirected(void) mi_attr_noexcept { - return mi_redirected; -} - -// Communicate with the redirection module on Windows -#if defined(_WIN32) && defined(MI_SHARED_LIB) && !defined(MI_WIN_NOREDIRECT) -#ifdef __cplusplus -extern "C" { -#endif -mi_decl_export void _mi_redirect_entry(DWORD reason) { - // called on redirection; careful as this may be called before DllMain - if (reason == DLL_PROCESS_ATTACH) { - mi_redirected = true; - } - else if (reason == DLL_PROCESS_DETACH) { - mi_redirected = false; - } - else if (reason == DLL_THREAD_DETACH) { - mi_thread_done(); - } -} -__declspec(dllimport) bool mi_cdecl mi_allocator_init(const char** message); -__declspec(dllimport) void mi_cdecl mi_allocator_done(void); -#ifdef __cplusplus -} -#endif -#else -static bool mi_allocator_init(const char** message) { - if (message != NULL) *message = NULL; - return true; -} -static void mi_allocator_done(void) { - // nothing to do -} -#endif - -// Called once by the process loader -static void mi_process_load(void) { - mi_heap_main_init(); - #if defined(__APPLE__) || defined(MI_TLS_RECURSE_GUARD) - volatile mi_heap_t* dummy = _mi_heap_default; // access TLS to allocate it before setting tls_initialized to true; - if (dummy == NULL) return; // use dummy or otherwise the access may get optimized away (issue #697) - #endif - os_preloading = false; - mi_assert_internal(_mi_is_main_thread()); - #if !(defined(_WIN32) && defined(MI_SHARED_LIB)) // use Dll process detach (see below) instead of atexit (issue #521) - atexit(&mi_process_done); - #endif - _mi_options_init(); - mi_process_setup_auto_thread_done(); - mi_process_init(); - if (mi_redirected) _mi_verbose_message("malloc is redirected.\n"); - - // show message from the redirector (if present) - const char* msg = NULL; - mi_allocator_init(&msg); - if (msg != NULL && (mi_option_is_enabled(mi_option_verbose) || mi_option_is_enabled(mi_option_show_errors))) { - _mi_fputs(NULL,NULL,NULL,msg); - } - - // reseed random - _mi_random_reinit_if_weak(&_mi_heap_main.random); -} - -#if defined(_WIN32) && (defined(_M_IX86) || defined(_M_X64)) -#include -mi_decl_cache_align bool _mi_cpu_has_fsrm = false; - -static void mi_detect_cpu_features(void) { - // FSRM for fast rep movsb support (AMD Zen3+ (~2020) or Intel Ice Lake+ (~2017)) - int32_t cpu_info[4]; - __cpuid(cpu_info, 7); - _mi_cpu_has_fsrm = ((cpu_info[3] & (1 << 4)) != 0); // bit 4 of EDX : see -} -#else -static void mi_detect_cpu_features(void) { - // nothing -} -#endif - -// Initialize the process; called by thread_init or the process loader -void mi_process_init(void) mi_attr_noexcept { - // ensure we are called once - static mi_atomic_once_t process_init; - #if _MSC_VER < 1920 - mi_heap_main_init(); // vs2017 can dynamically re-initialize _mi_heap_main - #endif - if (!mi_atomic_once(&process_init)) return; - _mi_process_is_initialized = true; - _mi_verbose_message("process init: 0x%zx\n", _mi_thread_id()); - mi_process_setup_auto_thread_done(); - - mi_detect_cpu_features(); - _mi_os_init(); - mi_heap_main_init(); - #if MI_DEBUG - _mi_verbose_message("debug level : %d\n", MI_DEBUG); - #endif - _mi_verbose_message("secure level: %d\n", MI_SECURE); - _mi_verbose_message("mem tracking: %s\n", MI_TRACK_TOOL); - #if MI_TSAN - _mi_verbose_message("thread sanitizer enabled\n"); - #endif - mi_thread_init(); - - #if defined(_WIN32) - // On windows, when building as a static lib the FLS cleanup happens to early for the main thread. - // To avoid this, set the FLS value for the main thread to NULL so the fls cleanup - // will not call _mi_thread_done on the (still executing) main thread. See issue #508. - _mi_prim_thread_associate_default_heap(NULL); - #endif - - mi_stats_reset(); // only call stat reset *after* thread init (or the heap tld == NULL) - mi_track_init(); - - if (mi_option_is_enabled(mi_option_reserve_huge_os_pages)) { - size_t pages = mi_option_get_clamp(mi_option_reserve_huge_os_pages, 0, 128*1024); - long reserve_at = mi_option_get(mi_option_reserve_huge_os_pages_at); - if (reserve_at != -1) { - mi_reserve_huge_os_pages_at(pages, reserve_at, pages*500); - } else { - mi_reserve_huge_os_pages_interleave(pages, 0, pages*500); - } - } - if (mi_option_is_enabled(mi_option_reserve_os_memory)) { - long ksize = mi_option_get(mi_option_reserve_os_memory); - if (ksize > 0) { - mi_reserve_os_memory((size_t)ksize*MI_KiB, true /* commit? */, true /* allow large pages? */); - } - } -} - -// Called when the process is done (through `at_exit`) -static void mi_cdecl mi_process_done(void) { - // only shutdown if we were initialized - if (!_mi_process_is_initialized) return; - // ensure we are called once - static bool process_done = false; - if (process_done) return; - process_done = true; - - // release any thread specific resources and ensure _mi_thread_done is called on all but the main thread - _mi_prim_thread_done_auto_done(); - - #ifndef MI_SKIP_COLLECT_ON_EXIT - #if (MI_DEBUG || !defined(MI_SHARED_LIB)) - // free all memory if possible on process exit. This is not needed for a stand-alone process - // but should be done if mimalloc is statically linked into another shared library which - // is repeatedly loaded/unloaded, see issue #281. - mi_collect(true /* force */ ); - #endif - #endif - - // Forcefully release all retained memory; this can be dangerous in general if overriding regular malloc/free - // since after process_done there might still be other code running that calls `free` (like at_exit routines, - // or C-runtime termination code. - if (mi_option_is_enabled(mi_option_destroy_on_exit)) { - mi_collect(true /* force */); - _mi_heap_unsafe_destroy_all(); // forcefully release all memory held by all heaps (of this thread only!) - _mi_arena_unsafe_destroy_all(& _mi_heap_main_get()->tld->stats); - } - - if (mi_option_is_enabled(mi_option_show_stats) || mi_option_is_enabled(mi_option_verbose)) { - mi_stats_print(NULL); - } - mi_allocator_done(); - _mi_verbose_message("process done: 0x%zx\n", _mi_heap_main.thread_id); - os_preloading = true; // don't call the C runtime anymore -} - - - -#if defined(_WIN32) && defined(MI_SHARED_LIB) - // Windows DLL: easy to hook into process_init and thread_done - __declspec(dllexport) BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID reserved) { - MI_UNUSED(reserved); - MI_UNUSED(inst); - if (reason==DLL_PROCESS_ATTACH) { - mi_process_load(); - } - else if (reason==DLL_PROCESS_DETACH) { - mi_process_done(); - } - else if (reason==DLL_THREAD_DETACH) { - if (!mi_is_redirected()) { - mi_thread_done(); - } - } - return TRUE; - } - -#elif defined(_MSC_VER) - // MSVC: use data section magic for static libraries - // See - static int _mi_process_init(void) { - mi_process_load(); - return 0; - } - typedef int(*_mi_crt_callback_t)(void); - #if defined(_M_X64) || defined(_M_ARM64) - __pragma(comment(linker, "/include:" "_mi_msvc_initu")) - #pragma section(".CRT$XIU", long, read) - #else - __pragma(comment(linker, "/include:" "__mi_msvc_initu")) - #endif - #pragma data_seg(".CRT$XIU") - mi_decl_externc _mi_crt_callback_t _mi_msvc_initu[] = { &_mi_process_init }; - #pragma data_seg() - -#elif defined(__cplusplus) - // C++: use static initialization to detect process start - static bool _mi_process_init(void) { - mi_process_load(); - return (_mi_heap_main.thread_id != 0); - } - static bool mi_initialized = _mi_process_init(); - -#elif defined(__GNUC__) || defined(__clang__) - // GCC,Clang: use the constructor attribute - static void __attribute__((constructor)) _mi_process_init(void) { - mi_process_load(); - } - -#else -#pragma message("define a way to call mi_process_load on your platform") -#endif diff --git a/Objects/mimalloc/options.c b/Objects/mimalloc/options.c deleted file mode 100644 index 2a8f481d569e1ed..000000000000000 --- a/Objects/mimalloc/options.c +++ /dev/null @@ -1,571 +0,0 @@ -/* ---------------------------------------------------------------------------- -Copyright (c) 2018-2021, Microsoft Research, Daan Leijen -This is free software; you can redistribute it and/or modify it under the -terms of the MIT license. A copy of the license can be found in the file -"LICENSE" at the root of this distribution. ------------------------------------------------------------------------------*/ -#include "mimalloc.h" -#include "mimalloc/internal.h" -#include "mimalloc/atomic.h" -#include "mimalloc/prim.h" // mi_prim_out_stderr - -#include // FILE -#include // abort -#include - - -static long mi_max_error_count = 16; // stop outputting errors after this (use < 0 for no limit) -static long mi_max_warning_count = 16; // stop outputting warnings after this (use < 0 for no limit) - -static void mi_add_stderr_output(void); - -int mi_version(void) mi_attr_noexcept { - return MI_MALLOC_VERSION; -} - - -// -------------------------------------------------------- -// Options -// These can be accessed by multiple threads and may be -// concurrently initialized, but an initializing data race -// is ok since they resolve to the same value. -// -------------------------------------------------------- -typedef enum mi_init_e { - UNINIT, // not yet initialized - DEFAULTED, // not found in the environment, use default value - INITIALIZED // found in environment or set explicitly -} mi_init_t; - -typedef struct mi_option_desc_s { - long value; // the value - mi_init_t init; // is it initialized yet? (from the environment) - mi_option_t option; // for debugging: the option index should match the option - const char* name; // option name without `mimalloc_` prefix - const char* legacy_name; // potential legacy option name -} mi_option_desc_t; - -#define MI_OPTION(opt) mi_option_##opt, #opt, NULL -#define MI_OPTION_LEGACY(opt,legacy) mi_option_##opt, #opt, #legacy - -static mi_option_desc_t options[_mi_option_last] = -{ - // stable options - #if MI_DEBUG || defined(MI_SHOW_ERRORS) - { 1, UNINIT, MI_OPTION(show_errors) }, - #else - { 0, UNINIT, MI_OPTION(show_errors) }, - #endif - { 0, UNINIT, MI_OPTION(show_stats) }, - { 0, UNINIT, MI_OPTION(verbose) }, - - // the following options are experimental and not all combinations make sense. - { 1, UNINIT, MI_OPTION(eager_commit) }, // commit per segment directly (4MiB) (but see also `eager_commit_delay`) - { 2, UNINIT, MI_OPTION_LEGACY(arena_eager_commit,eager_region_commit) }, // eager commit arena's? 2 is used to enable this only on an OS that has overcommit (i.e. linux) - { 1, UNINIT, MI_OPTION_LEGACY(purge_decommits,reset_decommits) }, // purge decommits memory (instead of reset) (note: on linux this uses MADV_DONTNEED for decommit) - { 0, UNINIT, MI_OPTION_LEGACY(allow_large_os_pages,large_os_pages) }, // use large OS pages, use only with eager commit to prevent fragmentation of VMA's - { 0, UNINIT, MI_OPTION(reserve_huge_os_pages) }, // per 1GiB huge pages - {-1, UNINIT, MI_OPTION(reserve_huge_os_pages_at) }, // reserve huge pages at node N - { 0, UNINIT, MI_OPTION(reserve_os_memory) }, - { 0, UNINIT, MI_OPTION(deprecated_segment_cache) }, // cache N segments per thread - { 0, UNINIT, MI_OPTION(deprecated_page_reset) }, // reset page memory on free - { 0, UNINIT, MI_OPTION_LEGACY(abandoned_page_purge,abandoned_page_reset) }, // reset free page memory when a thread terminates - { 0, UNINIT, MI_OPTION(deprecated_segment_reset) }, // reset segment memory on free (needs eager commit) -#if defined(__NetBSD__) - { 0, UNINIT, MI_OPTION(eager_commit_delay) }, // the first N segments per thread are not eagerly committed -#else - { 1, UNINIT, MI_OPTION(eager_commit_delay) }, // the first N segments per thread are not eagerly committed (but per page in the segment on demand) -#endif - { 10, UNINIT, MI_OPTION_LEGACY(purge_delay,reset_delay) }, // purge delay in milli-seconds - { 0, UNINIT, MI_OPTION(use_numa_nodes) }, // 0 = use available numa nodes, otherwise use at most N nodes. - { 0, UNINIT, MI_OPTION(limit_os_alloc) }, // 1 = do not use OS memory for allocation (but only reserved arenas) - { 100, UNINIT, MI_OPTION(os_tag) }, // only apple specific for now but might serve more or less related purpose - { 16, UNINIT, MI_OPTION(max_errors) }, // maximum errors that are output - { 16, UNINIT, MI_OPTION(max_warnings) }, // maximum warnings that are output - { 8, UNINIT, MI_OPTION(max_segment_reclaim)}, // max. number of segment reclaims from the abandoned segments per try. - { 0, UNINIT, MI_OPTION(destroy_on_exit)}, // release all OS memory on process exit; careful with dangling pointer or after-exit frees! - #if (MI_INTPTR_SIZE>4) - { 1024L * 1024L, UNINIT, MI_OPTION(arena_reserve) }, // reserve memory N KiB at a time - #else - { 128L * 1024L, UNINIT, MI_OPTION(arena_reserve) }, - #endif - { 10, UNINIT, MI_OPTION(arena_purge_mult) }, // purge delay multiplier for arena's - { 1, UNINIT, MI_OPTION_LEGACY(purge_extend_delay, decommit_extend_delay) }, -}; - -static void mi_option_init(mi_option_desc_t* desc); - -void _mi_options_init(void) { - // called on process load; should not be called before the CRT is initialized! - // (e.g. do not call this from process_init as that may run before CRT initialization) - mi_add_stderr_output(); // now it safe to use stderr for output - for(int i = 0; i < _mi_option_last; i++ ) { - mi_option_t option = (mi_option_t)i; - long l = mi_option_get(option); MI_UNUSED(l); // initialize - // if (option != mi_option_verbose) - { - mi_option_desc_t* desc = &options[option]; - _mi_verbose_message("option '%s': %ld\n", desc->name, desc->value); - } - } - mi_max_error_count = mi_option_get(mi_option_max_errors); - mi_max_warning_count = mi_option_get(mi_option_max_warnings); -} - -mi_decl_nodiscard long mi_option_get(mi_option_t option) { - mi_assert(option >= 0 && option < _mi_option_last); - if (option < 0 || option >= _mi_option_last) return 0; - mi_option_desc_t* desc = &options[option]; - mi_assert(desc->option == option); // index should match the option - if mi_unlikely(desc->init == UNINIT) { - mi_option_init(desc); - } - return desc->value; -} - -mi_decl_nodiscard long mi_option_get_clamp(mi_option_t option, long min, long max) { - long x = mi_option_get(option); - return (x < min ? min : (x > max ? max : x)); -} - -mi_decl_nodiscard size_t mi_option_get_size(mi_option_t option) { - mi_assert_internal(option == mi_option_reserve_os_memory || option == mi_option_arena_reserve); - long x = mi_option_get(option); - return (x < 0 ? 0 : (size_t)x * MI_KiB); -} - -void mi_option_set(mi_option_t option, long value) { - mi_assert(option >= 0 && option < _mi_option_last); - if (option < 0 || option >= _mi_option_last) return; - mi_option_desc_t* desc = &options[option]; - mi_assert(desc->option == option); // index should match the option - desc->value = value; - desc->init = INITIALIZED; -} - -void mi_option_set_default(mi_option_t option, long value) { - mi_assert(option >= 0 && option < _mi_option_last); - if (option < 0 || option >= _mi_option_last) return; - mi_option_desc_t* desc = &options[option]; - if (desc->init != INITIALIZED) { - desc->value = value; - } -} - -mi_decl_nodiscard bool mi_option_is_enabled(mi_option_t option) { - return (mi_option_get(option) != 0); -} - -void mi_option_set_enabled(mi_option_t option, bool enable) { - mi_option_set(option, (enable ? 1 : 0)); -} - -void mi_option_set_enabled_default(mi_option_t option, bool enable) { - mi_option_set_default(option, (enable ? 1 : 0)); -} - -void mi_option_enable(mi_option_t option) { - mi_option_set_enabled(option,true); -} - -void mi_option_disable(mi_option_t option) { - mi_option_set_enabled(option,false); -} - -static void mi_cdecl mi_out_stderr(const char* msg, void* arg) { - MI_UNUSED(arg); - if (msg != NULL && msg[0] != 0) { - _mi_prim_out_stderr(msg); - } -} - -// Since an output function can be registered earliest in the `main` -// function we also buffer output that happens earlier. When -// an output function is registered it is called immediately with -// the output up to that point. -#ifndef MI_MAX_DELAY_OUTPUT -#define MI_MAX_DELAY_OUTPUT ((size_t)(32*1024)) -#endif -static char out_buf[MI_MAX_DELAY_OUTPUT+1]; -static _Atomic(size_t) out_len; - -static void mi_cdecl mi_out_buf(const char* msg, void* arg) { - MI_UNUSED(arg); - if (msg==NULL) return; - if (mi_atomic_load_relaxed(&out_len)>=MI_MAX_DELAY_OUTPUT) return; - size_t n = _mi_strlen(msg); - if (n==0) return; - // claim space - size_t start = mi_atomic_add_acq_rel(&out_len, n); - if (start >= MI_MAX_DELAY_OUTPUT) return; - // check bound - if (start+n >= MI_MAX_DELAY_OUTPUT) { - n = MI_MAX_DELAY_OUTPUT-start-1; - } - _mi_memcpy(&out_buf[start], msg, n); -} - -static void mi_out_buf_flush(mi_output_fun* out, bool no_more_buf, void* arg) { - if (out==NULL) return; - // claim (if `no_more_buf == true`, no more output will be added after this point) - size_t count = mi_atomic_add_acq_rel(&out_len, (no_more_buf ? MI_MAX_DELAY_OUTPUT : 1)); - // and output the current contents - if (count>MI_MAX_DELAY_OUTPUT) count = MI_MAX_DELAY_OUTPUT; - out_buf[count] = 0; - out(out_buf,arg); - if (!no_more_buf) { - out_buf[count] = '\n'; // if continue with the buffer, insert a newline - } -} - - -// Once this module is loaded, switch to this routine -// which outputs to stderr and the delayed output buffer. -static void mi_cdecl mi_out_buf_stderr(const char* msg, void* arg) { - mi_out_stderr(msg,arg); - mi_out_buf(msg,arg); -} - - - -// -------------------------------------------------------- -// Default output handler -// -------------------------------------------------------- - -// Should be atomic but gives errors on many platforms as generally we cannot cast a function pointer to a uintptr_t. -// For now, don't register output from multiple threads. -static mi_output_fun* volatile mi_out_default; // = NULL -static _Atomic(void*) mi_out_arg; // = NULL - -static mi_output_fun* mi_out_get_default(void** parg) { - if (parg != NULL) { *parg = mi_atomic_load_ptr_acquire(void,&mi_out_arg); } - mi_output_fun* out = mi_out_default; - return (out == NULL ? &mi_out_buf : out); -} - -void mi_register_output(mi_output_fun* out, void* arg) mi_attr_noexcept { - mi_out_default = (out == NULL ? &mi_out_stderr : out); // stop using the delayed output buffer - mi_atomic_store_ptr_release(void,&mi_out_arg, arg); - if (out!=NULL) mi_out_buf_flush(out,true,arg); // output all the delayed output now -} - -// add stderr to the delayed output after the module is loaded -static void mi_add_stderr_output(void) { - mi_assert_internal(mi_out_default == NULL); - mi_out_buf_flush(&mi_out_stderr, false, NULL); // flush current contents to stderr - mi_out_default = &mi_out_buf_stderr; // and add stderr to the delayed output -} - -// -------------------------------------------------------- -// Messages, all end up calling `_mi_fputs`. -// -------------------------------------------------------- -static _Atomic(size_t) error_count; // = 0; // when >= max_error_count stop emitting errors -static _Atomic(size_t) warning_count; // = 0; // when >= max_warning_count stop emitting warnings - -// When overriding malloc, we may recurse into mi_vfprintf if an allocation -// inside the C runtime causes another message. -// In some cases (like on macOS) the loader already allocates which -// calls into mimalloc; if we then access thread locals (like `recurse`) -// this may crash as the access may call _tlv_bootstrap that tries to -// (recursively) invoke malloc again to allocate space for the thread local -// variables on demand. This is why we use a _mi_preloading test on such -// platforms. However, C code generator may move the initial thread local address -// load before the `if` and we therefore split it out in a separate function. -static mi_decl_thread bool recurse = false; - -static mi_decl_noinline bool mi_recurse_enter_prim(void) { - if (recurse) return false; - recurse = true; - return true; -} - -static mi_decl_noinline void mi_recurse_exit_prim(void) { - recurse = false; -} - -static bool mi_recurse_enter(void) { - #if defined(__APPLE__) || defined(MI_TLS_RECURSE_GUARD) - if (_mi_preloading()) return false; - #endif - return mi_recurse_enter_prim(); -} - -static void mi_recurse_exit(void) { - #if defined(__APPLE__) || defined(MI_TLS_RECURSE_GUARD) - if (_mi_preloading()) return; - #endif - mi_recurse_exit_prim(); -} - -void _mi_fputs(mi_output_fun* out, void* arg, const char* prefix, const char* message) { - if (out==NULL || (void*)out==(void*)stdout || (void*)out==(void*)stderr) { // TODO: use mi_out_stderr for stderr? - if (!mi_recurse_enter()) return; - out = mi_out_get_default(&arg); - if (prefix != NULL) out(prefix, arg); - out(message, arg); - mi_recurse_exit(); - } - else { - if (prefix != NULL) out(prefix, arg); - out(message, arg); - } -} - -// Define our own limited `fprintf` that avoids memory allocation. -// We do this using `snprintf` with a limited buffer. -static void mi_vfprintf( mi_output_fun* out, void* arg, const char* prefix, const char* fmt, va_list args ) { - char buf[512]; - if (fmt==NULL) return; - if (!mi_recurse_enter()) return; - vsnprintf(buf,sizeof(buf)-1,fmt,args); - mi_recurse_exit(); - _mi_fputs(out,arg,prefix,buf); -} - -void _mi_fprintf( mi_output_fun* out, void* arg, const char* fmt, ... ) { - va_list args; - va_start(args,fmt); - mi_vfprintf(out,arg,NULL,fmt,args); - va_end(args); -} - -static void mi_vfprintf_thread(mi_output_fun* out, void* arg, const char* prefix, const char* fmt, va_list args) { - if (prefix != NULL && _mi_strnlen(prefix,33) <= 32 && !_mi_is_main_thread()) { - char tprefix[64]; - snprintf(tprefix, sizeof(tprefix), "%sthread 0x%llx: ", prefix, (unsigned long long)_mi_thread_id()); - mi_vfprintf(out, arg, tprefix, fmt, args); - } - else { - mi_vfprintf(out, arg, prefix, fmt, args); - } -} - -void _mi_trace_message(const char* fmt, ...) { - if (mi_option_get(mi_option_verbose) <= 1) return; // only with verbose level 2 or higher - va_list args; - va_start(args, fmt); - mi_vfprintf_thread(NULL, NULL, "mimalloc: ", fmt, args); - va_end(args); -} - -void _mi_verbose_message(const char* fmt, ...) { - if (!mi_option_is_enabled(mi_option_verbose)) return; - va_list args; - va_start(args,fmt); - mi_vfprintf(NULL, NULL, "mimalloc: ", fmt, args); - va_end(args); -} - -static void mi_show_error_message(const char* fmt, va_list args) { - if (!mi_option_is_enabled(mi_option_verbose)) { - if (!mi_option_is_enabled(mi_option_show_errors)) return; - if (mi_max_error_count >= 0 && (long)mi_atomic_increment_acq_rel(&error_count) > mi_max_error_count) return; - } - mi_vfprintf_thread(NULL, NULL, "mimalloc: error: ", fmt, args); -} - -void _mi_warning_message(const char* fmt, ...) { - if (!mi_option_is_enabled(mi_option_verbose)) { - if (!mi_option_is_enabled(mi_option_show_errors)) return; - if (mi_max_warning_count >= 0 && (long)mi_atomic_increment_acq_rel(&warning_count) > mi_max_warning_count) return; - } - va_list args; - va_start(args,fmt); - mi_vfprintf_thread(NULL, NULL, "mimalloc: warning: ", fmt, args); - va_end(args); -} - - -#if MI_DEBUG -void _mi_assert_fail(const char* assertion, const char* fname, unsigned line, const char* func ) { - _mi_fprintf(NULL, NULL, "mimalloc: assertion failed: at \"%s\":%u, %s\n assertion: \"%s\"\n", fname, line, (func==NULL?"":func), assertion); - abort(); -} -#endif - -// -------------------------------------------------------- -// Errors -// -------------------------------------------------------- - -static mi_error_fun* volatile mi_error_handler; // = NULL -static _Atomic(void*) mi_error_arg; // = NULL - -static void mi_error_default(int err) { - MI_UNUSED(err); -#if (MI_DEBUG>0) - if (err==EFAULT) { - #ifdef _MSC_VER - __debugbreak(); - #endif - abort(); - } -#endif -#if (MI_SECURE>0) - if (err==EFAULT) { // abort on serious errors in secure mode (corrupted meta-data) - abort(); - } -#endif -#if defined(MI_XMALLOC) - if (err==ENOMEM || err==EOVERFLOW) { // abort on memory allocation fails in xmalloc mode - abort(); - } -#endif -} - -void mi_register_error(mi_error_fun* fun, void* arg) { - mi_error_handler = fun; // can be NULL - mi_atomic_store_ptr_release(void,&mi_error_arg, arg); -} - -void _mi_error_message(int err, const char* fmt, ...) { - // show detailed error message - va_list args; - va_start(args, fmt); - mi_show_error_message(fmt, args); - va_end(args); - // and call the error handler which may abort (or return normally) - if (mi_error_handler != NULL) { - mi_error_handler(err, mi_atomic_load_ptr_acquire(void,&mi_error_arg)); - } - else { - mi_error_default(err); - } -} - -// -------------------------------------------------------- -// Initialize options by checking the environment -// -------------------------------------------------------- -char _mi_toupper(char c) { - if (c >= 'a' && c <= 'z') return (c - 'a' + 'A'); - else return c; -} - -int _mi_strnicmp(const char* s, const char* t, size_t n) { - if (n == 0) return 0; - for (; *s != 0 && *t != 0 && n > 0; s++, t++, n--) { - if (_mi_toupper(*s) != _mi_toupper(*t)) break; - } - return (n == 0 ? 0 : *s - *t); -} - -void _mi_strlcpy(char* dest, const char* src, size_t dest_size) { - if (dest==NULL || src==NULL || dest_size == 0) return; - // copy until end of src, or when dest is (almost) full - while (*src != 0 && dest_size > 1) { - *dest++ = *src++; - dest_size--; - } - // always zero terminate - *dest = 0; -} - -void _mi_strlcat(char* dest, const char* src, size_t dest_size) { - if (dest==NULL || src==NULL || dest_size == 0) return; - // find end of string in the dest buffer - while (*dest != 0 && dest_size > 1) { - dest++; - dest_size--; - } - // and catenate - _mi_strlcpy(dest, src, dest_size); -} - -size_t _mi_strlen(const char* s) { - if (s==NULL) return 0; - size_t len = 0; - while(s[len] != 0) { len++; } - return len; -} - -size_t _mi_strnlen(const char* s, size_t max_len) { - if (s==NULL) return 0; - size_t len = 0; - while(s[len] != 0 && len < max_len) { len++; } - return len; -} - -#ifdef MI_NO_GETENV -static bool mi_getenv(const char* name, char* result, size_t result_size) { - MI_UNUSED(name); - MI_UNUSED(result); - MI_UNUSED(result_size); - return false; -} -#else -static bool mi_getenv(const char* name, char* result, size_t result_size) { - if (name==NULL || result == NULL || result_size < 64) return false; - return _mi_prim_getenv(name,result,result_size); -} -#endif - -// TODO: implement ourselves to reduce dependencies on the C runtime -#include // strtol -#include // strstr - - -static void mi_option_init(mi_option_desc_t* desc) { - // Read option value from the environment - char s[64 + 1]; - char buf[64+1]; - _mi_strlcpy(buf, "mimalloc_", sizeof(buf)); - _mi_strlcat(buf, desc->name, sizeof(buf)); - bool found = mi_getenv(buf, s, sizeof(s)); - if (!found && desc->legacy_name != NULL) { - _mi_strlcpy(buf, "mimalloc_", sizeof(buf)); - _mi_strlcat(buf, desc->legacy_name, sizeof(buf)); - found = mi_getenv(buf, s, sizeof(s)); - if (found) { - _mi_warning_message("environment option \"mimalloc_%s\" is deprecated -- use \"mimalloc_%s\" instead.\n", desc->legacy_name, desc->name); - } - } - - if (found) { - size_t len = _mi_strnlen(s, sizeof(buf) - 1); - for (size_t i = 0; i < len; i++) { - buf[i] = _mi_toupper(s[i]); - } - buf[len] = 0; - if (buf[0] == 0 || strstr("1;TRUE;YES;ON", buf) != NULL) { - desc->value = 1; - desc->init = INITIALIZED; - } - else if (strstr("0;FALSE;NO;OFF", buf) != NULL) { - desc->value = 0; - desc->init = INITIALIZED; - } - else { - char* end = buf; - long value = strtol(buf, &end, 10); - if (desc->option == mi_option_reserve_os_memory || desc->option == mi_option_arena_reserve) { - // this option is interpreted in KiB to prevent overflow of `long` - if (*end == 'K') { end++; } - else if (*end == 'M') { value *= MI_KiB; end++; } - else if (*end == 'G') { value *= MI_MiB; end++; } - else { value = (value + MI_KiB - 1) / MI_KiB; } - if (end[0] == 'I' && end[1] == 'B') { end += 2; } - else if (*end == 'B') { end++; } - } - if (*end == 0) { - desc->value = value; - desc->init = INITIALIZED; - } - else { - // set `init` first to avoid recursion through _mi_warning_message on mimalloc_verbose. - desc->init = DEFAULTED; - if (desc->option == mi_option_verbose && desc->value == 0) { - // if the 'mimalloc_verbose' env var has a bogus value we'd never know - // (since the value defaults to 'off') so in that case briefly enable verbose - desc->value = 1; - _mi_warning_message("environment option mimalloc_%s has an invalid value.\n", desc->name); - desc->value = 0; - } - else { - _mi_warning_message("environment option mimalloc_%s has an invalid value.\n", desc->name); - } - } - } - mi_assert_internal(desc->init != UNINIT); - } - else if (!_mi_preloading()) { - desc->init = DEFAULTED; - } -} diff --git a/Objects/mimalloc/os.c b/Objects/mimalloc/os.c deleted file mode 100644 index c9103168c125077..000000000000000 --- a/Objects/mimalloc/os.c +++ /dev/null @@ -1,698 +0,0 @@ -/* ---------------------------------------------------------------------------- -Copyright (c) 2018-2023, Microsoft Research, Daan Leijen -This is free software; you can redistribute it and/or modify it under the -terms of the MIT license. A copy of the license can be found in the file -"LICENSE" at the root of this distribution. ------------------------------------------------------------------------------*/ -#include "mimalloc.h" -#include "mimalloc/internal.h" -#include "mimalloc/atomic.h" -#include "mimalloc/prim.h" - - -/* ----------------------------------------------------------- - Initialization. - On windows initializes support for aligned allocation and - large OS pages (if MIMALLOC_LARGE_OS_PAGES is true). ------------------------------------------------------------ */ - -static mi_os_mem_config_t mi_os_mem_config = { - 4096, // page size - 0, // large page size (usually 2MiB) - 4096, // allocation granularity - true, // has overcommit? (if true we use MAP_NORESERVE on mmap systems) - false, // must free whole? (on mmap systems we can free anywhere in a mapped range, but on Windows we must free the entire span) - true // has virtual reserve? (if true we can reserve virtual address space without using commit or physical memory) -}; - -bool _mi_os_has_overcommit(void) { - return mi_os_mem_config.has_overcommit; -} - -bool _mi_os_has_virtual_reserve(void) { - return mi_os_mem_config.has_virtual_reserve; -} - - -// OS (small) page size -size_t _mi_os_page_size(void) { - return mi_os_mem_config.page_size; -} - -// if large OS pages are supported (2 or 4MiB), then return the size, otherwise return the small page size (4KiB) -size_t _mi_os_large_page_size(void) { - return (mi_os_mem_config.large_page_size != 0 ? mi_os_mem_config.large_page_size : _mi_os_page_size()); -} - -bool _mi_os_use_large_page(size_t size, size_t alignment) { - // if we have access, check the size and alignment requirements - if (mi_os_mem_config.large_page_size == 0 || !mi_option_is_enabled(mi_option_allow_large_os_pages)) return false; - return ((size % mi_os_mem_config.large_page_size) == 0 && (alignment % mi_os_mem_config.large_page_size) == 0); -} - -// round to a good OS allocation size (bounded by max 12.5% waste) -size_t _mi_os_good_alloc_size(size_t size) { - size_t align_size; - if (size < 512*MI_KiB) align_size = _mi_os_page_size(); - else if (size < 2*MI_MiB) align_size = 64*MI_KiB; - else if (size < 8*MI_MiB) align_size = 256*MI_KiB; - else if (size < 32*MI_MiB) align_size = 1*MI_MiB; - else align_size = 4*MI_MiB; - if mi_unlikely(size >= (SIZE_MAX - align_size)) return size; // possible overflow? - return _mi_align_up(size, align_size); -} - -void _mi_os_init(void) { - _mi_prim_mem_init(&mi_os_mem_config); -} - - -/* ----------------------------------------------------------- - Util --------------------------------------------------------------- */ -bool _mi_os_decommit(void* addr, size_t size, mi_stats_t* stats); -bool _mi_os_commit(void* addr, size_t size, bool* is_zero, mi_stats_t* tld_stats); - -static void* mi_align_up_ptr(void* p, size_t alignment) { - return (void*)_mi_align_up((uintptr_t)p, alignment); -} - -static void* mi_align_down_ptr(void* p, size_t alignment) { - return (void*)_mi_align_down((uintptr_t)p, alignment); -} - - -/* ----------------------------------------------------------- - aligned hinting --------------------------------------------------------------- */ - -// On 64-bit systems, we can do efficient aligned allocation by using -// the 2TiB to 30TiB area to allocate those. -#if (MI_INTPTR_SIZE >= 8) -static mi_decl_cache_align _Atomic(uintptr_t)aligned_base; - -// Return a MI_SEGMENT_SIZE aligned address that is probably available. -// If this returns NULL, the OS will determine the address but on some OS's that may not be -// properly aligned which can be more costly as it needs to be adjusted afterwards. -// For a size > 1GiB this always returns NULL in order to guarantee good ASLR randomization; -// (otherwise an initial large allocation of say 2TiB has a 50% chance to include (known) addresses -// in the middle of the 2TiB - 6TiB address range (see issue #372)) - -#define MI_HINT_BASE ((uintptr_t)2 << 40) // 2TiB start -#define MI_HINT_AREA ((uintptr_t)4 << 40) // upto 6TiB (since before win8 there is "only" 8TiB available to processes) -#define MI_HINT_MAX ((uintptr_t)30 << 40) // wrap after 30TiB (area after 32TiB is used for huge OS pages) - -void* _mi_os_get_aligned_hint(size_t try_alignment, size_t size) -{ - if (try_alignment <= 1 || try_alignment > MI_SEGMENT_SIZE) return NULL; - size = _mi_align_up(size, MI_SEGMENT_SIZE); - if (size > 1*MI_GiB) return NULL; // guarantee the chance of fixed valid address is at most 1/(MI_HINT_AREA / 1<<30) = 1/4096. - #if (MI_SECURE>0) - size += MI_SEGMENT_SIZE; // put in `MI_SEGMENT_SIZE` virtual gaps between hinted blocks; this splits VLA's but increases guarded areas. - #endif - - uintptr_t hint = mi_atomic_add_acq_rel(&aligned_base, size); - if (hint == 0 || hint > MI_HINT_MAX) { // wrap or initialize - uintptr_t init = MI_HINT_BASE; - #if (MI_SECURE>0 || MI_DEBUG==0) // security: randomize start of aligned allocations unless in debug mode - mi_heap_t* heap = mi_prim_get_default_heap(); - // gh-123022: default heap may not be initialized in CPython in background threads - if (mi_heap_is_initialized(heap)) { - uintptr_t r = _mi_heap_random_next(heap); - init = init + ((MI_SEGMENT_SIZE * ((r>>17) & 0xFFFFF)) % MI_HINT_AREA); // (randomly 20 bits)*4MiB == 0 to 4TiB - } - #endif - uintptr_t expected = hint + size; - mi_atomic_cas_strong_acq_rel(&aligned_base, &expected, init); - hint = mi_atomic_add_acq_rel(&aligned_base, size); // this may still give 0 or > MI_HINT_MAX but that is ok, it is a hint after all - } - if (hint%try_alignment != 0) return NULL; - return (void*)hint; -} -#else -void* _mi_os_get_aligned_hint(size_t try_alignment, size_t size) { - MI_UNUSED(try_alignment); MI_UNUSED(size); - return NULL; -} -#endif - - -/* ----------------------------------------------------------- - Free memory --------------------------------------------------------------- */ - -static void mi_os_free_huge_os_pages(void* p, size_t size, mi_stats_t* stats); - -static void mi_os_prim_free(void* addr, size_t size, bool still_committed, mi_stats_t* tld_stats) { - MI_UNUSED(tld_stats); - mi_assert_internal((size % _mi_os_page_size()) == 0); - if (addr == NULL || size == 0) return; // || _mi_os_is_huge_reserved(addr) - int err = _mi_prim_free(addr, size); - if (err != 0) { - _mi_warning_message("unable to free OS memory (error: %d (0x%x), size: 0x%zx bytes, address: %p)\n", err, err, size, addr); - } - mi_stats_t* stats = &_mi_stats_main; - if (still_committed) { _mi_stat_decrease(&stats->committed, size); } - _mi_stat_decrease(&stats->reserved, size); -} - -void _mi_os_free_ex(void* addr, size_t size, bool still_committed, mi_memid_t memid, mi_stats_t* tld_stats) { - if (mi_memkind_is_os(memid.memkind)) { - size_t csize = _mi_os_good_alloc_size(size); - void* base = addr; - // different base? (due to alignment) - if (memid.mem.os.base != NULL) { - mi_assert(memid.mem.os.base <= addr); - mi_assert((uint8_t*)memid.mem.os.base + memid.mem.os.alignment >= (uint8_t*)addr); - base = memid.mem.os.base; - csize += ((uint8_t*)addr - (uint8_t*)memid.mem.os.base); - } - // free it - if (memid.memkind == MI_MEM_OS_HUGE) { - mi_assert(memid.is_pinned); - mi_os_free_huge_os_pages(base, csize, tld_stats); - } - else { - mi_os_prim_free(base, csize, still_committed, tld_stats); - } - } - else { - // nothing to do - mi_assert(memid.memkind < MI_MEM_OS); - } -} - -void _mi_os_free(void* p, size_t size, mi_memid_t memid, mi_stats_t* tld_stats) { - _mi_os_free_ex(p, size, true, memid, tld_stats); -} - - -/* ----------------------------------------------------------- - Primitive allocation from the OS. --------------------------------------------------------------- */ - -// Note: the `try_alignment` is just a hint and the returned pointer is not guaranteed to be aligned. -static void* mi_os_prim_alloc(size_t size, size_t try_alignment, bool commit, bool allow_large, bool* is_large, bool* is_zero, mi_stats_t* stats) { - mi_assert_internal(size > 0 && (size % _mi_os_page_size()) == 0); - mi_assert_internal(is_zero != NULL); - mi_assert_internal(is_large != NULL); - if (size == 0) return NULL; - if (!commit) { allow_large = false; } - if (try_alignment == 0) { try_alignment = 1; } // avoid 0 to ensure there will be no divide by zero when aligning - - *is_zero = false; - void* p = NULL; - int err = _mi_prim_alloc(size, try_alignment, commit, allow_large, is_large, is_zero, &p); - if (err != 0) { - _mi_warning_message("unable to allocate OS memory (error: %d (0x%x), size: 0x%zx bytes, align: 0x%zx, commit: %d, allow large: %d)\n", err, err, size, try_alignment, commit, allow_large); - } - mi_stat_counter_increase(stats->mmap_calls, 1); - if (p != NULL) { - _mi_stat_increase(&stats->reserved, size); - if (commit) { - _mi_stat_increase(&stats->committed, size); - // seems needed for asan (or `mimalloc-test-api` fails) - #ifdef MI_TRACK_ASAN - if (*is_zero) { mi_track_mem_defined(p,size); } - else { mi_track_mem_undefined(p,size); } - #endif - } - } - return p; -} - - -// Primitive aligned allocation from the OS. -// This function guarantees the allocated memory is aligned. -static void* mi_os_prim_alloc_aligned(size_t size, size_t alignment, bool commit, bool allow_large, bool* is_large, bool* is_zero, void** base, mi_stats_t* stats) { - mi_assert_internal(alignment >= _mi_os_page_size() && ((alignment & (alignment - 1)) == 0)); - mi_assert_internal(size > 0 && (size % _mi_os_page_size()) == 0); - mi_assert_internal(is_large != NULL); - mi_assert_internal(is_zero != NULL); - mi_assert_internal(base != NULL); - if (!commit) allow_large = false; - if (!(alignment >= _mi_os_page_size() && ((alignment & (alignment - 1)) == 0))) return NULL; - size = _mi_align_up(size, _mi_os_page_size()); - - // try first with a hint (this will be aligned directly on Win 10+ or BSD) - void* p = mi_os_prim_alloc(size, alignment, commit, allow_large, is_large, is_zero, stats); - if (p == NULL) return NULL; - - // aligned already? - if (((uintptr_t)p % alignment) == 0) { - *base = p; - } - else { - // if not aligned, free it, overallocate, and unmap around it - // NOTE(sgross): this warning causes issues in Python tests - // _mi_warning_message("unable to allocate aligned OS memory directly, fall back to over-allocation (size: 0x%zx bytes, address: %p, alignment: 0x%zx, commit: %d)\n", size, p, alignment, commit); - mi_os_prim_free(p, size, commit, stats); - if (size >= (SIZE_MAX - alignment)) return NULL; // overflow - const size_t over_size = size + alignment; - - if (mi_os_mem_config.must_free_whole) { // win32 virtualAlloc cannot free parts of an allocate block - // over-allocate uncommitted (virtual) memory - p = mi_os_prim_alloc(over_size, 1 /*alignment*/, false /* commit? */, false /* allow_large */, is_large, is_zero, stats); - if (p == NULL) return NULL; - - // set p to the aligned part in the full region - // note: this is dangerous on Windows as VirtualFree needs the actual base pointer - // this is handled though by having the `base` field in the memid's - *base = p; // remember the base - p = mi_align_up_ptr(p, alignment); - - // explicitly commit only the aligned part - if (commit) { - _mi_os_commit(p, size, NULL, stats); - } - } - else { // mmap can free inside an allocation - // overallocate... - p = mi_os_prim_alloc(over_size, 1, commit, false, is_large, is_zero, stats); - if (p == NULL) return NULL; - - // and selectively unmap parts around the over-allocated area. (noop on sbrk) - void* aligned_p = mi_align_up_ptr(p, alignment); - size_t pre_size = (uint8_t*)aligned_p - (uint8_t*)p; - size_t mid_size = _mi_align_up(size, _mi_os_page_size()); - size_t post_size = over_size - pre_size - mid_size; - mi_assert_internal(pre_size < over_size&& post_size < over_size&& mid_size >= size); - if (pre_size > 0) { mi_os_prim_free(p, pre_size, commit, stats); } - if (post_size > 0) { mi_os_prim_free((uint8_t*)aligned_p + mid_size, post_size, commit, stats); } - // we can return the aligned pointer on `mmap` (and sbrk) systems - p = aligned_p; - *base = aligned_p; // since we freed the pre part, `*base == p`. - } - } - - mi_assert_internal(p == NULL || (p != NULL && *base != NULL && ((uintptr_t)p % alignment) == 0)); - return p; -} - - -/* ----------------------------------------------------------- - OS API: alloc and alloc_aligned ------------------------------------------------------------ */ - -void* _mi_os_alloc(size_t size, mi_memid_t* memid, mi_stats_t* tld_stats) { - MI_UNUSED(tld_stats); - *memid = _mi_memid_none(); - mi_stats_t* stats = &_mi_stats_main; - if (size == 0) return NULL; - size = _mi_os_good_alloc_size(size); - bool os_is_large = false; - bool os_is_zero = false; - void* p = mi_os_prim_alloc(size, 0, true, false, &os_is_large, &os_is_zero, stats); - if (p != NULL) { - *memid = _mi_memid_create_os(true, os_is_zero, os_is_large); - } - return p; -} - -void* _mi_os_alloc_aligned(size_t size, size_t alignment, bool commit, bool allow_large, mi_memid_t* memid, mi_stats_t* tld_stats) -{ - MI_UNUSED(&_mi_os_get_aligned_hint); // suppress unused warnings - MI_UNUSED(tld_stats); - *memid = _mi_memid_none(); - if (size == 0) return NULL; - size = _mi_os_good_alloc_size(size); - alignment = _mi_align_up(alignment, _mi_os_page_size()); - - bool os_is_large = false; - bool os_is_zero = false; - void* os_base = NULL; - void* p = mi_os_prim_alloc_aligned(size, alignment, commit, allow_large, &os_is_large, &os_is_zero, &os_base, &_mi_stats_main /*tld->stats*/ ); - if (p != NULL) { - *memid = _mi_memid_create_os(commit, os_is_zero, os_is_large); - memid->mem.os.base = os_base; - memid->mem.os.alignment = alignment; - } - return p; -} - -/* ----------------------------------------------------------- - OS aligned allocation with an offset. This is used - for large alignments > MI_ALIGNMENT_MAX. We use a large mimalloc - page where the object can be aligned at an offset from the start of the segment. - As we may need to overallocate, we need to free such pointers using `mi_free_aligned` - to use the actual start of the memory region. ------------------------------------------------------------ */ - -void* _mi_os_alloc_aligned_at_offset(size_t size, size_t alignment, size_t offset, bool commit, bool allow_large, mi_memid_t* memid, mi_stats_t* tld_stats) { - mi_assert(offset <= MI_SEGMENT_SIZE); - mi_assert(offset <= size); - mi_assert((alignment % _mi_os_page_size()) == 0); - *memid = _mi_memid_none(); - if (offset > MI_SEGMENT_SIZE) return NULL; - if (offset == 0) { - // regular aligned allocation - return _mi_os_alloc_aligned(size, alignment, commit, allow_large, memid, tld_stats); - } - else { - // overallocate to align at an offset - const size_t extra = _mi_align_up(offset, alignment) - offset; - const size_t oversize = size + extra; - void* const start = _mi_os_alloc_aligned(oversize, alignment, commit, allow_large, memid, tld_stats); - if (start == NULL) return NULL; - - void* const p = (uint8_t*)start + extra; - mi_assert(_mi_is_aligned((uint8_t*)p + offset, alignment)); - // decommit the overallocation at the start - if (commit && extra > _mi_os_page_size()) { - _mi_os_decommit(start, extra, tld_stats); - } - return p; - } -} - -/* ----------------------------------------------------------- - OS memory API: reset, commit, decommit, protect, unprotect. ------------------------------------------------------------ */ - -// OS page align within a given area, either conservative (pages inside the area only), -// or not (straddling pages outside the area is possible) -static void* mi_os_page_align_areax(bool conservative, void* addr, size_t size, size_t* newsize) { - mi_assert(addr != NULL && size > 0); - if (newsize != NULL) *newsize = 0; - if (size == 0 || addr == NULL) return NULL; - - // page align conservatively within the range - void* start = (conservative ? mi_align_up_ptr(addr, _mi_os_page_size()) - : mi_align_down_ptr(addr, _mi_os_page_size())); - void* end = (conservative ? mi_align_down_ptr((uint8_t*)addr + size, _mi_os_page_size()) - : mi_align_up_ptr((uint8_t*)addr + size, _mi_os_page_size())); - ptrdiff_t diff = (uint8_t*)end - (uint8_t*)start; - if (diff <= 0) return NULL; - - mi_assert_internal((conservative && (size_t)diff <= size) || (!conservative && (size_t)diff >= size)); - if (newsize != NULL) *newsize = (size_t)diff; - return start; -} - -static void* mi_os_page_align_area_conservative(void* addr, size_t size, size_t* newsize) { - return mi_os_page_align_areax(true, addr, size, newsize); -} - -bool _mi_os_commit(void* addr, size_t size, bool* is_zero, mi_stats_t* tld_stats) { - MI_UNUSED(tld_stats); - mi_stats_t* stats = &_mi_stats_main; - if (is_zero != NULL) { *is_zero = false; } - _mi_stat_increase(&stats->committed, size); // use size for precise commit vs. decommit - _mi_stat_counter_increase(&stats->commit_calls, 1); - - // page align range - size_t csize; - void* start = mi_os_page_align_areax(false /* conservative? */, addr, size, &csize); - if (csize == 0) return true; - - // commit - bool os_is_zero = false; - int err = _mi_prim_commit(start, csize, &os_is_zero); - if (err != 0) { - _mi_warning_message("cannot commit OS memory (error: %d (0x%x), address: %p, size: 0x%zx bytes)\n", err, err, start, csize); - return false; - } - if (os_is_zero && is_zero != NULL) { - *is_zero = true; - mi_assert_expensive(mi_mem_is_zero(start, csize)); - } - // note: the following seems required for asan (otherwise `mimalloc-test-stress` fails) - #ifdef MI_TRACK_ASAN - if (os_is_zero) { mi_track_mem_defined(start,csize); } - else { mi_track_mem_undefined(start,csize); } - #endif - return true; -} - -static bool mi_os_decommit_ex(void* addr, size_t size, bool* needs_recommit, mi_stats_t* tld_stats) { - MI_UNUSED(tld_stats); - mi_stats_t* stats = &_mi_stats_main; - mi_assert_internal(needs_recommit!=NULL); - _mi_stat_decrease(&stats->committed, size); - - // page align - size_t csize; - void* start = mi_os_page_align_area_conservative(addr, size, &csize); - if (csize == 0) return true; - - // decommit - *needs_recommit = true; - int err = _mi_prim_decommit(start,csize,needs_recommit); - if (err != 0) { - _mi_warning_message("cannot decommit OS memory (error: %d (0x%x), address: %p, size: 0x%zx bytes)\n", err, err, start, csize); - } - mi_assert_internal(err == 0); - return (err == 0); -} - -bool _mi_os_decommit(void* addr, size_t size, mi_stats_t* tld_stats) { - bool needs_recommit; - return mi_os_decommit_ex(addr, size, &needs_recommit, tld_stats); -} - - -// Signal to the OS that the address range is no longer in use -// but may be used later again. This will release physical memory -// pages and reduce swapping while keeping the memory committed. -// We page align to a conservative area inside the range to reset. -bool _mi_os_reset(void* addr, size_t size, mi_stats_t* stats) { - // page align conservatively within the range - size_t csize; - void* start = mi_os_page_align_area_conservative(addr, size, &csize); - if (csize == 0) return true; // || _mi_os_is_huge_reserved(addr) - _mi_stat_increase(&stats->reset, csize); - _mi_stat_counter_increase(&stats->reset_calls, 1); - - #if (MI_DEBUG>1) && !MI_SECURE && !MI_TRACK_ENABLED // && !MI_TSAN - memset(start, 0, csize); // pretend it is eagerly reset - #endif - - int err = _mi_prim_reset(start, csize); - if (err != 0) { - _mi_warning_message("cannot reset OS memory (error: %d (0x%x), address: %p, size: 0x%zx bytes)\n", err, err, start, csize); - } - return (err == 0); -} - - -// either resets or decommits memory, returns true if the memory needs -// to be recommitted if it is to be re-used later on. -bool _mi_os_purge_ex(void* p, size_t size, bool allow_reset, mi_stats_t* stats) -{ - if (mi_option_get(mi_option_purge_delay) < 0) return false; // is purging allowed? - _mi_stat_counter_increase(&stats->purge_calls, 1); - _mi_stat_increase(&stats->purged, size); - - if (mi_option_is_enabled(mi_option_purge_decommits) && // should decommit? - !_mi_preloading()) // don't decommit during preloading (unsafe) - { - bool needs_recommit = true; - mi_os_decommit_ex(p, size, &needs_recommit, stats); - return needs_recommit; - } - else { - if (allow_reset) { // this can sometimes be not allowed if the range is not fully committed - _mi_os_reset(p, size, stats); - } - return false; // needs no recommit - } -} - -// either resets or decommits memory, returns true if the memory needs -// to be recommitted if it is to be re-used later on. -bool _mi_os_purge(void* p, size_t size, mi_stats_t * stats) { - return _mi_os_purge_ex(p, size, true, stats); -} - -// Protect a region in memory to be not accessible. -static bool mi_os_protectx(void* addr, size_t size, bool protect) { - // page align conservatively within the range - size_t csize = 0; - void* start = mi_os_page_align_area_conservative(addr, size, &csize); - if (csize == 0) return false; - /* - if (_mi_os_is_huge_reserved(addr)) { - _mi_warning_message("cannot mprotect memory allocated in huge OS pages\n"); - } - */ - int err = _mi_prim_protect(start,csize,protect); - if (err != 0) { - _mi_warning_message("cannot %s OS memory (error: %d (0x%x), address: %p, size: 0x%zx bytes)\n", (protect ? "protect" : "unprotect"), err, err, start, csize); - } - return (err == 0); -} - -bool _mi_os_protect(void* addr, size_t size) { - return mi_os_protectx(addr, size, true); -} - -bool _mi_os_unprotect(void* addr, size_t size) { - return mi_os_protectx(addr, size, false); -} - - - -/* ---------------------------------------------------------------------------- -Support for allocating huge OS pages (1Gib) that are reserved up-front -and possibly associated with a specific NUMA node. (use `numa_node>=0`) ------------------------------------------------------------------------------*/ -#define MI_HUGE_OS_PAGE_SIZE (MI_GiB) - - -#if (MI_INTPTR_SIZE >= 8) -// To ensure proper alignment, use our own area for huge OS pages -static mi_decl_cache_align _Atomic(uintptr_t) mi_huge_start; // = 0 - -// Claim an aligned address range for huge pages -static uint8_t* mi_os_claim_huge_pages(size_t pages, size_t* total_size) { - if (total_size != NULL) *total_size = 0; - const size_t size = pages * MI_HUGE_OS_PAGE_SIZE; - - uintptr_t start = 0; - uintptr_t end = 0; - uintptr_t huge_start = mi_atomic_load_relaxed(&mi_huge_start); - do { - start = huge_start; - if (start == 0) { - // Initialize the start address after the 32TiB area - start = ((uintptr_t)32 << 40); // 32TiB virtual start address - #if (MI_SECURE>0 || MI_DEBUG==0) // security: randomize start of huge pages unless in debug mode - mi_heap_t* heap = mi_prim_get_default_heap(); - // gh-123022: default heap may not be initialized in CPython in background threads - if (mi_heap_is_initialized(heap)) { - uintptr_t r = _mi_heap_random_next(heap); - start = start + ((uintptr_t)MI_HUGE_OS_PAGE_SIZE * ((r>>17) & 0x0FFF)); // (randomly 12bits)*1GiB == between 0 to 4TiB - } - #endif - } - end = start + size; - mi_assert_internal(end % MI_SEGMENT_SIZE == 0); - } while (!mi_atomic_cas_strong_acq_rel(&mi_huge_start, &huge_start, end)); - - if (total_size != NULL) *total_size = size; - return (uint8_t*)start; -} -#else -static uint8_t* mi_os_claim_huge_pages(size_t pages, size_t* total_size) { - MI_UNUSED(pages); - if (total_size != NULL) *total_size = 0; - return NULL; -} -#endif - -// Allocate MI_SEGMENT_SIZE aligned huge pages -void* _mi_os_alloc_huge_os_pages(size_t pages, int numa_node, mi_msecs_t max_msecs, size_t* pages_reserved, size_t* psize, mi_memid_t* memid) { - *memid = _mi_memid_none(); - if (psize != NULL) *psize = 0; - if (pages_reserved != NULL) *pages_reserved = 0; - size_t size = 0; - uint8_t* start = mi_os_claim_huge_pages(pages, &size); - if (start == NULL) return NULL; // or 32-bit systems - - // Allocate one page at the time but try to place them contiguously - // We allocate one page at the time to be able to abort if it takes too long - // or to at least allocate as many as available on the system. - mi_msecs_t start_t = _mi_clock_start(); - size_t page = 0; - bool all_zero = true; - while (page < pages) { - // allocate a page - bool is_zero = false; - void* addr = start + (page * MI_HUGE_OS_PAGE_SIZE); - void* p = NULL; - int err = _mi_prim_alloc_huge_os_pages(addr, MI_HUGE_OS_PAGE_SIZE, numa_node, &is_zero, &p); - if (!is_zero) { all_zero = false; } - if (err != 0) { - _mi_warning_message("unable to allocate huge OS page (error: %d (0x%x), address: %p, size: %zx bytes)\n", err, err, addr, MI_HUGE_OS_PAGE_SIZE); - break; - } - - // Did we succeed at a contiguous address? - if (p != addr) { - // no success, issue a warning and break - if (p != NULL) { - _mi_warning_message("could not allocate contiguous huge OS page %zu at %p\n", page, addr); - mi_os_prim_free(p, MI_HUGE_OS_PAGE_SIZE, true, &_mi_stats_main); - } - break; - } - - // success, record it - page++; // increase before timeout check (see issue #711) - _mi_stat_increase(&_mi_stats_main.committed, MI_HUGE_OS_PAGE_SIZE); - _mi_stat_increase(&_mi_stats_main.reserved, MI_HUGE_OS_PAGE_SIZE); - - // check for timeout - if (max_msecs > 0) { - mi_msecs_t elapsed = _mi_clock_end(start_t); - if (page >= 1) { - mi_msecs_t estimate = ((elapsed / (page+1)) * pages); - if (estimate > 2*max_msecs) { // seems like we are going to timeout, break - elapsed = max_msecs + 1; - } - } - if (elapsed > max_msecs) { - _mi_warning_message("huge OS page allocation timed out (after allocating %zu page(s))\n", page); - break; - } - } - } - mi_assert_internal(page*MI_HUGE_OS_PAGE_SIZE <= size); - if (pages_reserved != NULL) { *pages_reserved = page; } - if (psize != NULL) { *psize = page * MI_HUGE_OS_PAGE_SIZE; } - if (page != 0) { - mi_assert(start != NULL); - *memid = _mi_memid_create_os(true /* is committed */, all_zero, true /* is_large */); - memid->memkind = MI_MEM_OS_HUGE; - mi_assert(memid->is_pinned); - #ifdef MI_TRACK_ASAN - if (all_zero) { mi_track_mem_defined(start,size); } - #endif - } - return (page == 0 ? NULL : start); -} - -// free every huge page in a range individually (as we allocated per page) -// note: needed with VirtualAlloc but could potentially be done in one go on mmap'd systems. -static void mi_os_free_huge_os_pages(void* p, size_t size, mi_stats_t* stats) { - if (p==NULL || size==0) return; - uint8_t* base = (uint8_t*)p; - while (size >= MI_HUGE_OS_PAGE_SIZE) { - mi_os_prim_free(base, MI_HUGE_OS_PAGE_SIZE, true, stats); - size -= MI_HUGE_OS_PAGE_SIZE; - base += MI_HUGE_OS_PAGE_SIZE; - } -} - -/* ---------------------------------------------------------------------------- -Support NUMA aware allocation ------------------------------------------------------------------------------*/ - -_Atomic(size_t) _mi_numa_node_count; // = 0 // cache the node count - -size_t _mi_os_numa_node_count_get(void) { - size_t count = mi_atomic_load_acquire(&_mi_numa_node_count); - if (count <= 0) { - long ncount = mi_option_get(mi_option_use_numa_nodes); // given explicitly? - if (ncount > 0) { - count = (size_t)ncount; - } - else { - count = _mi_prim_numa_node_count(); // or detect dynamically - if (count == 0) count = 1; - } - mi_atomic_store_release(&_mi_numa_node_count, count); // save it - _mi_verbose_message("using %zd numa regions\n", count); - } - return count; -} - -int _mi_os_numa_node_get(mi_os_tld_t* tld) { - MI_UNUSED(tld); - size_t numa_count = _mi_os_numa_node_count(); - if (numa_count<=1) return 0; // optimize on single numa node systems: always node 0 - // never more than the node count and >= 0 - size_t numa_node = _mi_prim_numa_node(); - if (numa_node >= numa_count) { numa_node = numa_node % numa_count; } - return (int)numa_node; -} diff --git a/Objects/mimalloc/page-queue.c b/Objects/mimalloc/page-queue.c deleted file mode 100644 index cb54b3740196e97..000000000000000 --- a/Objects/mimalloc/page-queue.c +++ /dev/null @@ -1,332 +0,0 @@ -/*---------------------------------------------------------------------------- -Copyright (c) 2018-2020, Microsoft Research, Daan Leijen -This is free software; you can redistribute it and/or modify it under the -terms of the MIT license. A copy of the license can be found in the file -"LICENSE" at the root of this distribution. ------------------------------------------------------------------------------*/ - -/* ----------------------------------------------------------- - Definition of page queues for each block size ------------------------------------------------------------ */ - -#ifndef MI_IN_PAGE_C -#error "this file should be included from 'page.c'" -#endif - -/* ----------------------------------------------------------- - Minimal alignment in machine words (i.e. `sizeof(void*)`) ------------------------------------------------------------ */ - -#if (MI_MAX_ALIGN_SIZE > 4*MI_INTPTR_SIZE) - #error "define alignment for more than 4x word size for this platform" -#elif (MI_MAX_ALIGN_SIZE > 2*MI_INTPTR_SIZE) - #define MI_ALIGN4W // 4 machine words minimal alignment -#elif (MI_MAX_ALIGN_SIZE > MI_INTPTR_SIZE) - #define MI_ALIGN2W // 2 machine words minimal alignment -#else - // ok, default alignment is 1 word -#endif - - -/* ----------------------------------------------------------- - Queue query ------------------------------------------------------------ */ - - -static inline bool mi_page_queue_is_huge(const mi_page_queue_t* pq) { - return (pq->block_size == (MI_MEDIUM_OBJ_SIZE_MAX+sizeof(uintptr_t))); -} - -static inline bool mi_page_queue_is_full(const mi_page_queue_t* pq) { - return (pq->block_size == (MI_MEDIUM_OBJ_SIZE_MAX+(2*sizeof(uintptr_t)))); -} - -static inline bool mi_page_queue_is_special(const mi_page_queue_t* pq) { - return (pq->block_size > MI_MEDIUM_OBJ_SIZE_MAX); -} - -/* ----------------------------------------------------------- - Bins ------------------------------------------------------------ */ - -// Return the bin for a given field size. -// Returns MI_BIN_HUGE if the size is too large. -// We use `wsize` for the size in "machine word sizes", -// i.e. byte size == `wsize*sizeof(void*)`. -static inline uint8_t mi_bin(size_t size) { - size_t wsize = _mi_wsize_from_size(size); - uint8_t bin; - if (wsize <= 1) { - bin = 1; - } - #if defined(MI_ALIGN4W) - else if (wsize <= 4) { - bin = (uint8_t)((wsize+1)&~1); // round to double word sizes - } - #elif defined(MI_ALIGN2W) - else if (wsize <= 8) { - bin = (uint8_t)((wsize+1)&~1); // round to double word sizes - } - #else - else if (wsize <= 8) { - bin = (uint8_t)wsize; - } - #endif - else if (wsize > MI_MEDIUM_OBJ_WSIZE_MAX) { - bin = MI_BIN_HUGE; - } - else { - #if defined(MI_ALIGN4W) - if (wsize <= 16) { wsize = (wsize+3)&~3; } // round to 4x word sizes - #endif - wsize--; - // find the highest bit - uint8_t b = (uint8_t)mi_bsr(wsize); // note: wsize != 0 - // and use the top 3 bits to determine the bin (~12.5% worst internal fragmentation). - // - adjust with 3 because we use do not round the first 8 sizes - // which each get an exact bin - bin = ((b << 2) + (uint8_t)((wsize >> (b - 2)) & 0x03)) - 3; - mi_assert_internal(bin < MI_BIN_HUGE); - } - mi_assert_internal(bin > 0 && bin <= MI_BIN_HUGE); - return bin; -} - - - -/* ----------------------------------------------------------- - Queue of pages with free blocks ------------------------------------------------------------ */ - -uint8_t _mi_bin(size_t size) { - return mi_bin(size); -} - -size_t _mi_bin_size(uint8_t bin) { - return _mi_heap_empty.pages[bin].block_size; -} - -// Good size for allocation -size_t mi_good_size(size_t size) mi_attr_noexcept { - if (size <= MI_MEDIUM_OBJ_SIZE_MAX) { - return _mi_bin_size(mi_bin(size)); - } - else { - return _mi_align_up(size,_mi_os_page_size()); - } -} - -#if (MI_DEBUG>1) -static bool mi_page_queue_contains(mi_page_queue_t* queue, const mi_page_t* page) { - mi_assert_internal(page != NULL); - mi_page_t* list = queue->first; - while (list != NULL) { - mi_assert_internal(list->next == NULL || list->next->prev == list); - mi_assert_internal(list->prev == NULL || list->prev->next == list); - if (list == page) break; - list = list->next; - } - return (list == page); -} - -#endif - -#if (MI_DEBUG>1) -static bool mi_heap_contains_queue(const mi_heap_t* heap, const mi_page_queue_t* pq) { - return (pq >= &heap->pages[0] && pq <= &heap->pages[MI_BIN_FULL]); -} -#endif - -static mi_page_queue_t* mi_page_queue_of(const mi_page_t* page) { - uint8_t bin = (mi_page_is_in_full(page) ? MI_BIN_FULL : mi_bin(page->xblock_size)); - mi_heap_t* heap = mi_page_heap(page); - mi_assert_internal(heap != NULL && bin <= MI_BIN_FULL); - mi_page_queue_t* pq = &heap->pages[bin]; - mi_assert_internal(bin >= MI_BIN_HUGE || page->xblock_size == pq->block_size); - mi_assert_expensive(mi_page_queue_contains(pq, page)); - return pq; -} - -static mi_page_queue_t* mi_heap_page_queue_of(mi_heap_t* heap, const mi_page_t* page) { - uint8_t bin = (mi_page_is_in_full(page) ? MI_BIN_FULL : mi_bin(page->xblock_size)); - mi_assert_internal(bin <= MI_BIN_FULL); - mi_page_queue_t* pq = &heap->pages[bin]; - mi_assert_internal(mi_page_is_in_full(page) || page->xblock_size == pq->block_size); - return pq; -} - -// The current small page array is for efficiency and for each -// small size (up to 256) it points directly to the page for that -// size without having to compute the bin. This means when the -// current free page queue is updated for a small bin, we need to update a -// range of entries in `_mi_page_small_free`. -static inline void mi_heap_queue_first_update(mi_heap_t* heap, const mi_page_queue_t* pq) { - mi_assert_internal(mi_heap_contains_queue(heap,pq)); - size_t size = pq->block_size; - if (size > MI_SMALL_SIZE_MAX) return; - - mi_page_t* page = pq->first; - if (pq->first == NULL) page = (mi_page_t*)&_mi_page_empty; - - // find index in the right direct page array - size_t start; - size_t idx = _mi_wsize_from_size(size); - mi_page_t** pages_free = heap->pages_free_direct; - - if (pages_free[idx] == page) return; // already set - - // find start slot - if (idx<=1) { - start = 0; - } - else { - // find previous size; due to minimal alignment upto 3 previous bins may need to be skipped - uint8_t bin = mi_bin(size); - const mi_page_queue_t* prev = pq - 1; - while( bin == mi_bin(prev->block_size) && prev > &heap->pages[0]) { - prev--; - } - start = 1 + _mi_wsize_from_size(prev->block_size); - if (start > idx) start = idx; - } - - // set size range to the right page - mi_assert(start <= idx); - for (size_t sz = start; sz <= idx; sz++) { - pages_free[sz] = page; - } -} - -/* -static bool mi_page_queue_is_empty(mi_page_queue_t* queue) { - return (queue->first == NULL); -} -*/ - -static void mi_page_queue_remove(mi_page_queue_t* queue, mi_page_t* page) { - mi_assert_internal(page != NULL); - mi_assert_expensive(mi_page_queue_contains(queue, page)); - mi_assert_internal(page->xblock_size == queue->block_size || (page->xblock_size > MI_MEDIUM_OBJ_SIZE_MAX && mi_page_queue_is_huge(queue)) || (mi_page_is_in_full(page) && mi_page_queue_is_full(queue))); - mi_heap_t* heap = mi_page_heap(page); - - if (page->prev != NULL) page->prev->next = page->next; - if (page->next != NULL) page->next->prev = page->prev; - if (page == queue->last) queue->last = page->prev; - if (page == queue->first) { - queue->first = page->next; - // update first - mi_assert_internal(mi_heap_contains_queue(heap, queue)); - mi_heap_queue_first_update(heap,queue); - } - heap->page_count--; - page->next = NULL; - page->prev = NULL; - // mi_atomic_store_ptr_release(mi_atomic_cast(void*, &page->heap), NULL); - mi_page_set_in_full(page,false); -} - - -static void mi_page_queue_push(mi_heap_t* heap, mi_page_queue_t* queue, mi_page_t* page) { - mi_assert_internal(mi_page_heap(page) == heap); - mi_assert_internal(!mi_page_queue_contains(queue, page)); - #if MI_HUGE_PAGE_ABANDON - mi_assert_internal(_mi_page_segment(page)->kind != MI_SEGMENT_HUGE); - #endif - mi_assert_internal(page->xblock_size == queue->block_size || - (page->xblock_size > MI_MEDIUM_OBJ_SIZE_MAX) || - (mi_page_is_in_full(page) && mi_page_queue_is_full(queue))); - - mi_page_set_in_full(page, mi_page_queue_is_full(queue)); - // mi_atomic_store_ptr_release(mi_atomic_cast(void*, &page->heap), heap); - page->next = queue->first; - page->prev = NULL; - if (queue->first != NULL) { - mi_assert_internal(queue->first->prev == NULL); - queue->first->prev = page; - queue->first = page; - } - else { - queue->first = queue->last = page; - } - - // update direct - mi_heap_queue_first_update(heap, queue); - heap->page_count++; -} - - -static void mi_page_queue_enqueue_from(mi_page_queue_t* to, mi_page_queue_t* from, mi_page_t* page) { - mi_assert_internal(page != NULL); - mi_assert_expensive(mi_page_queue_contains(from, page)); - mi_assert_expensive(!mi_page_queue_contains(to, page)); - - mi_assert_internal((page->xblock_size == to->block_size && page->xblock_size == from->block_size) || - (page->xblock_size == to->block_size && mi_page_queue_is_full(from)) || - (page->xblock_size == from->block_size && mi_page_queue_is_full(to)) || - (page->xblock_size > MI_LARGE_OBJ_SIZE_MAX && mi_page_queue_is_huge(to)) || - (page->xblock_size > MI_LARGE_OBJ_SIZE_MAX && mi_page_queue_is_full(to))); - - mi_heap_t* heap = mi_page_heap(page); - if (page->prev != NULL) page->prev->next = page->next; - if (page->next != NULL) page->next->prev = page->prev; - if (page == from->last) from->last = page->prev; - if (page == from->first) { - from->first = page->next; - // update first - mi_assert_internal(mi_heap_contains_queue(heap, from)); - mi_heap_queue_first_update(heap, from); - } - - page->prev = to->last; - page->next = NULL; - if (to->last != NULL) { - mi_assert_internal(heap == mi_page_heap(to->last)); - to->last->next = page; - to->last = page; - } - else { - to->first = page; - to->last = page; - mi_heap_queue_first_update(heap, to); - } - - mi_page_set_in_full(page, mi_page_queue_is_full(to)); -} - -// Only called from `mi_heap_absorb`. -size_t _mi_page_queue_append(mi_heap_t* heap, mi_page_queue_t* pq, mi_page_queue_t* append) { - mi_assert_internal(mi_heap_contains_queue(heap,pq)); - mi_assert_internal(pq->block_size == append->block_size); - - if (append->first==NULL) return 0; - - // set append pages to new heap and count - size_t count = 0; - for (mi_page_t* page = append->first; page != NULL; page = page->next) { - // inline `mi_page_set_heap` to avoid wrong assertion during absorption; - // in this case it is ok to be delayed freeing since both "to" and "from" heap are still alive. - mi_atomic_store_release(&page->xheap, (uintptr_t)heap); - // set the flag to delayed free (not overriding NEVER_DELAYED_FREE) which has as a - // side effect that it spins until any DELAYED_FREEING is finished. This ensures - // that after appending only the new heap will be used for delayed free operations. - _mi_page_use_delayed_free(page, MI_USE_DELAYED_FREE, false); - count++; - } - - if (pq->last==NULL) { - // take over afresh - mi_assert_internal(pq->first==NULL); - pq->first = append->first; - pq->last = append->last; - mi_heap_queue_first_update(heap, pq); - } - else { - // append to end - mi_assert_internal(pq->last!=NULL); - mi_assert_internal(append->first!=NULL); - pq->last->next = append->first; - append->first->prev = pq->last; - pq->last = append->last; - } - return count; -} diff --git a/Objects/mimalloc/page.c b/Objects/mimalloc/page.c deleted file mode 100644 index ded59f8eb1ccaac..000000000000000 --- a/Objects/mimalloc/page.c +++ /dev/null @@ -1,976 +0,0 @@ -/*---------------------------------------------------------------------------- -Copyright (c) 2018-2020, Microsoft Research, Daan Leijen -This is free software; you can redistribute it and/or modify it under the -terms of the MIT license. A copy of the license can be found in the file -"LICENSE" at the root of this distribution. ------------------------------------------------------------------------------*/ - -/* ----------------------------------------------------------- - The core of the allocator. Every segment contains - pages of a certain block size. The main function - exported is `mi_malloc_generic`. ------------------------------------------------------------ */ - -#include "mimalloc.h" -#include "mimalloc/internal.h" -#include "mimalloc/atomic.h" - -/* ----------------------------------------------------------- - Definition of page queues for each block size ------------------------------------------------------------ */ - -#define MI_IN_PAGE_C -#include "page-queue.c" -#undef MI_IN_PAGE_C - - -/* ----------------------------------------------------------- - Page helpers ------------------------------------------------------------ */ - -// Index a block in a page -static inline mi_block_t* mi_page_block_at(const mi_page_t* page, void* page_start, size_t block_size, size_t i) { - MI_UNUSED(page); - mi_assert_internal(page != NULL); - mi_assert_internal(i <= page->reserved); - return (mi_block_t*)((uint8_t*)page_start + (i * block_size)); -} - -static void mi_page_init(mi_heap_t* heap, mi_page_t* page, size_t size, mi_tld_t* tld); -static void mi_page_extend_free(mi_heap_t* heap, mi_page_t* page, mi_tld_t* tld); - -#if (MI_DEBUG>=3) -static size_t mi_page_list_count(mi_page_t* page, mi_block_t* head) { - size_t count = 0; - while (head != NULL) { - mi_assert_internal(page == _mi_ptr_page(head)); - count++; - head = mi_block_next(page, head); - } - return count; -} - -/* -// Start of the page available memory -static inline uint8_t* mi_page_area(const mi_page_t* page) { - return _mi_page_start(_mi_page_segment(page), page, NULL); -} -*/ - -static bool mi_page_list_is_valid(mi_page_t* page, mi_block_t* p) { - size_t psize; - uint8_t* page_area = _mi_page_start(_mi_page_segment(page), page, &psize); - mi_block_t* start = (mi_block_t*)page_area; - mi_block_t* end = (mi_block_t*)(page_area + psize); - while(p != NULL) { - if (p < start || p >= end) return false; - p = mi_block_next(page, p); - } -#if MI_DEBUG>3 // generally too expensive to check this - if (page->free_is_zero) { - const size_t ubsize = mi_page_usable_block_size(page); - for (mi_block_t* block = page->free; block != NULL; block = mi_block_next(page, block)) { - mi_assert_expensive(mi_mem_is_zero(block + 1, ubsize - sizeof(mi_block_t))); - } - } -#endif - return true; -} - -static bool mi_page_is_valid_init(mi_page_t* page) { - mi_assert_internal(page->xblock_size > 0); - mi_assert_internal(page->used <= page->capacity); - mi_assert_internal(page->capacity <= page->reserved); - - mi_segment_t* segment = _mi_page_segment(page); - uint8_t* start = _mi_page_start(segment,page,NULL); - mi_assert_internal(start == _mi_segment_page_start(segment,page,NULL)); - //const size_t bsize = mi_page_block_size(page); - //mi_assert_internal(start + page->capacity*page->block_size == page->top); - - mi_assert_internal(mi_page_list_is_valid(page,page->free)); - mi_assert_internal(mi_page_list_is_valid(page,page->local_free)); - - #if MI_DEBUG>3 // generally too expensive to check this - if (page->free_is_zero) { - const size_t ubsize = mi_page_usable_block_size(page); - for(mi_block_t* block = page->free; block != NULL; block = mi_block_next(page,block)) { - mi_assert_expensive(mi_mem_is_zero(block + 1, ubsize - sizeof(mi_block_t))); - } - } - #endif - - #if !MI_TRACK_ENABLED && !MI_TSAN - mi_block_t* tfree = mi_page_thread_free(page); - mi_assert_internal(mi_page_list_is_valid(page, tfree)); - //size_t tfree_count = mi_page_list_count(page, tfree); - //mi_assert_internal(tfree_count <= page->thread_freed + 1); - #endif - - size_t free_count = mi_page_list_count(page, page->free) + mi_page_list_count(page, page->local_free); - mi_assert_internal(page->used + free_count == page->capacity); - - return true; -} - -extern bool _mi_process_is_initialized; // has mi_process_init been called? - -bool _mi_page_is_valid(mi_page_t* page) { - mi_assert_internal(mi_page_is_valid_init(page)); - #if MI_SECURE - mi_assert_internal(page->keys[0] != 0); - #endif - if (mi_page_heap(page)!=NULL) { - mi_segment_t* segment = _mi_page_segment(page); - - mi_assert_internal(!_mi_process_is_initialized || segment->thread_id==0 || segment->thread_id == mi_page_heap(page)->thread_id); - #if MI_HUGE_PAGE_ABANDON - if (segment->kind != MI_SEGMENT_HUGE) - #endif - { - mi_page_queue_t* pq = mi_page_queue_of(page); - mi_assert_internal(mi_page_queue_contains(pq, page)); - mi_assert_internal(pq->block_size==mi_page_block_size(page) || mi_page_block_size(page) > MI_MEDIUM_OBJ_SIZE_MAX || mi_page_is_in_full(page)); - mi_assert_internal(mi_heap_contains_queue(mi_page_heap(page),pq)); - } - } - return true; -} -#endif - -void _mi_page_use_delayed_free(mi_page_t* page, mi_delayed_t delay, bool override_never) { - while (!_mi_page_try_use_delayed_free(page, delay, override_never)) { - mi_atomic_yield(); - } -} - -bool _mi_page_try_use_delayed_free(mi_page_t* page, mi_delayed_t delay, bool override_never) { - mi_thread_free_t tfreex; - mi_delayed_t old_delay; - mi_thread_free_t tfree; - size_t yield_count = 0; - do { - tfree = mi_atomic_load_acquire(&page->xthread_free); // note: must acquire as we can break/repeat this loop and not do a CAS; - tfreex = mi_tf_set_delayed(tfree, delay); - old_delay = mi_tf_delayed(tfree); - if mi_unlikely(old_delay == MI_DELAYED_FREEING) { - if (yield_count >= 4) return false; // give up after 4 tries - yield_count++; - mi_atomic_yield(); // delay until outstanding MI_DELAYED_FREEING are done. - // tfree = mi_tf_set_delayed(tfree, MI_NO_DELAYED_FREE); // will cause CAS to busy fail - } - else if (delay == old_delay) { - break; // avoid atomic operation if already equal - } - else if (!override_never && old_delay == MI_NEVER_DELAYED_FREE) { - break; // leave never-delayed flag set - } - } while ((old_delay == MI_DELAYED_FREEING) || - !mi_atomic_cas_weak_release(&page->xthread_free, &tfree, tfreex)); - - return true; // success -} - -/* ----------------------------------------------------------- - Page collect the `local_free` and `thread_free` lists ------------------------------------------------------------ */ - -// Collect the local `thread_free` list using an atomic exchange. -// Note: The exchange must be done atomically as this is used right after -// moving to the full list in `mi_page_collect_ex` and we need to -// ensure that there was no race where the page became unfull just before the move. -static void _mi_page_thread_free_collect(mi_page_t* page) -{ - mi_block_t* head; - mi_thread_free_t tfreex; - mi_thread_free_t tfree = mi_atomic_load_relaxed(&page->xthread_free); - do { - head = mi_tf_block(tfree); - tfreex = mi_tf_set_block(tfree,NULL); - } while (!mi_atomic_cas_weak_acq_rel(&page->xthread_free, &tfree, tfreex)); - - // return if the list is empty - if (head == NULL) return; - - // find the tail -- also to get a proper count (without data races) - uint32_t max_count = page->capacity; // cannot collect more than capacity - uint32_t count = 1; - mi_block_t* tail = head; - mi_block_t* next; - while ((next = mi_block_next(page,tail)) != NULL && count <= max_count) { - count++; - tail = next; - } - // if `count > max_count` there was a memory corruption (possibly infinite list due to double multi-threaded free) - if (count > max_count) { - _mi_error_message(EFAULT, "corrupted thread-free list\n"); - return; // the thread-free items cannot be freed - } - - // and append the current local free list - mi_block_set_next(page,tail, page->local_free); - page->local_free = head; - - // update counts now - page->used -= count; - - if (page->used == 0) { - // The page may have had a QSBR goal set from a previous point when it - // was all-free. That goal is no longer valid because the page was - // allocated from and then freed again by other threads. - _PyMem_mi_page_clear_qsbr(page); - } -} - -void _mi_page_free_collect(mi_page_t* page, bool force) { - mi_assert_internal(page!=NULL); - - // collect the thread free list - if (force || mi_page_thread_free(page) != NULL) { // quick test to avoid an atomic operation - _mi_page_thread_free_collect(page); - } - - // and the local free list - if (page->local_free != NULL) { - if mi_likely(page->free == NULL) { - // usual case - page->free = page->local_free; - page->local_free = NULL; - page->free_is_zero = false; - } - else if (force) { - // append -- only on shutdown (force) as this is a linear operation - mi_block_t* tail = page->local_free; - mi_block_t* next; - while ((next = mi_block_next(page, tail)) != NULL) { - tail = next; - } - mi_block_set_next(page, tail, page->free); - page->free = page->local_free; - page->local_free = NULL; - page->free_is_zero = false; - } - } - - mi_assert_internal(!force || page->local_free == NULL); -} - - - -/* ----------------------------------------------------------- - Page fresh and retire ------------------------------------------------------------ */ - -// called from segments when reclaiming abandoned pages -void _mi_page_reclaim(mi_heap_t* heap, mi_page_t* page) { - mi_assert_expensive(mi_page_is_valid_init(page)); - - mi_assert_internal(mi_page_heap(page) == heap); - mi_assert_internal(mi_page_thread_free_flag(page) != MI_NEVER_DELAYED_FREE); - #if MI_HUGE_PAGE_ABANDON - mi_assert_internal(_mi_page_segment(page)->kind != MI_SEGMENT_HUGE); - #endif - - // TODO: push on full queue immediately if it is full? - mi_page_queue_t* pq = mi_page_queue(heap, mi_page_block_size(page)); - mi_page_queue_push(heap, pq, page); - _PyMem_mi_page_reclaimed(page); - mi_assert_expensive(_mi_page_is_valid(page)); -} - -// allocate a fresh page from a segment -static mi_page_t* mi_page_fresh_alloc(mi_heap_t* heap, mi_page_queue_t* pq, size_t block_size, size_t page_alignment) { - #if !MI_HUGE_PAGE_ABANDON - mi_assert_internal(pq != NULL); - mi_assert_internal(mi_heap_contains_queue(heap, pq)); - mi_assert_internal(page_alignment > 0 || block_size > MI_MEDIUM_OBJ_SIZE_MAX || block_size == pq->block_size); - #endif - mi_page_t* page = _mi_segment_page_alloc(heap, block_size, page_alignment, &heap->tld->segments, &heap->tld->os); - if (page == NULL) { - // this may be out-of-memory, or an abandoned page was reclaimed (and in our queue) - return NULL; - } - mi_assert_internal(page_alignment >0 || block_size > MI_MEDIUM_OBJ_SIZE_MAX || _mi_page_segment(page)->kind != MI_SEGMENT_HUGE); - mi_assert_internal(pq!=NULL || page->xblock_size != 0); - mi_assert_internal(pq!=NULL || mi_page_block_size(page) >= block_size); - // a fresh page was found, initialize it - const size_t full_block_size = ((pq == NULL || mi_page_queue_is_huge(pq)) ? mi_page_block_size(page) : block_size); // see also: mi_segment_huge_page_alloc - mi_assert_internal(full_block_size >= block_size); - mi_page_init(heap, page, full_block_size, heap->tld); - mi_heap_stat_increase(heap, pages, 1); - if (pq != NULL) { mi_page_queue_push(heap, pq, page); } - mi_assert_expensive(_mi_page_is_valid(page)); - return page; -} - -// Get a fresh page to use -static mi_page_t* mi_page_fresh(mi_heap_t* heap, mi_page_queue_t* pq) { - mi_assert_internal(mi_heap_contains_queue(heap, pq)); - mi_page_t* page = mi_page_fresh_alloc(heap, pq, pq->block_size, 0); - if (page==NULL) return NULL; - mi_assert_internal(pq->block_size==mi_page_block_size(page)); - mi_assert_internal(pq==mi_page_queue(heap, mi_page_block_size(page))); - return page; -} - -/* ----------------------------------------------------------- - Do any delayed frees - (put there by other threads if they deallocated in a full page) ------------------------------------------------------------ */ -void _mi_heap_delayed_free_all(mi_heap_t* heap) { - while (!_mi_heap_delayed_free_partial(heap)) { - mi_atomic_yield(); - } -} - -// returns true if all delayed frees were processed -bool _mi_heap_delayed_free_partial(mi_heap_t* heap) { - // take over the list (note: no atomic exchange since it is often NULL) - mi_block_t* block = mi_atomic_load_ptr_relaxed(mi_block_t, &heap->thread_delayed_free); - while (block != NULL && !mi_atomic_cas_ptr_weak_acq_rel(mi_block_t, &heap->thread_delayed_free, &block, NULL)) { /* nothing */ }; - bool all_freed = true; - - // and free them all - while(block != NULL) { - mi_block_t* next = mi_block_nextx(heap,block, heap->keys); - // use internal free instead of regular one to keep stats etc correct - if (!_mi_free_delayed_block(block)) { - // we might already start delayed freeing while another thread has not yet - // reset the delayed_freeing flag; in that case delay it further by reinserting the current block - // into the delayed free list - all_freed = false; - mi_block_t* dfree = mi_atomic_load_ptr_relaxed(mi_block_t, &heap->thread_delayed_free); - do { - mi_block_set_nextx(heap, block, dfree, heap->keys); - } while (!mi_atomic_cas_ptr_weak_release(mi_block_t,&heap->thread_delayed_free, &dfree, block)); - } - block = next; - } - return all_freed; -} - -/* ----------------------------------------------------------- - Unfull, abandon, free and retire ------------------------------------------------------------ */ - -// Move a page from the full list back to a regular list -void _mi_page_unfull(mi_page_t* page) { - mi_assert_internal(page != NULL); - mi_assert_expensive(_mi_page_is_valid(page)); - mi_assert_internal(mi_page_is_in_full(page)); - if (!mi_page_is_in_full(page)) return; - - mi_heap_t* heap = mi_page_heap(page); - mi_page_queue_t* pqfull = &heap->pages[MI_BIN_FULL]; - mi_page_set_in_full(page, false); // to get the right queue - mi_page_queue_t* pq = mi_heap_page_queue_of(heap, page); - mi_page_set_in_full(page, true); - mi_page_queue_enqueue_from(pq, pqfull, page); -} - -static void mi_page_to_full(mi_page_t* page, mi_page_queue_t* pq) { - mi_assert_internal(pq == mi_page_queue_of(page)); - mi_assert_internal(!mi_page_immediate_available(page)); - mi_assert_internal(!mi_page_is_in_full(page)); - - if (mi_page_is_in_full(page)) return; - mi_page_queue_enqueue_from(&mi_page_heap(page)->pages[MI_BIN_FULL], pq, page); - _mi_page_free_collect(page,false); // try to collect right away in case another thread freed just before MI_USE_DELAYED_FREE was set -} - - -// Abandon a page with used blocks at the end of a thread. -// Note: only call if it is ensured that no references exist from -// the `page->heap->thread_delayed_free` into this page. -// Currently only called through `mi_heap_collect_ex` which ensures this. -void _mi_page_abandon(mi_page_t* page, mi_page_queue_t* pq) { - mi_assert_internal(page != NULL); - mi_assert_expensive(_mi_page_is_valid(page)); - mi_assert_internal(pq == mi_page_queue_of(page)); - mi_assert_internal(mi_page_heap(page) != NULL); - - mi_heap_t* pheap = mi_page_heap(page); - -#ifdef Py_GIL_DISABLED - if (page->qsbr_node.next != NULL) { - // remove from QSBR queue, but keep the goal - llist_remove(&page->qsbr_node); - } -#endif - - // remove from our page list - mi_segments_tld_t* segments_tld = &pheap->tld->segments; - mi_page_queue_remove(pq, page); - - // page is no longer associated with our heap - mi_assert_internal(mi_page_thread_free_flag(page)==MI_NEVER_DELAYED_FREE); - mi_page_set_heap(page, NULL); - -#if (MI_DEBUG>1) && !MI_TRACK_ENABLED - // check there are no references left.. - for (mi_block_t* block = (mi_block_t*)pheap->thread_delayed_free; block != NULL; block = mi_block_nextx(pheap, block, pheap->keys)) { - mi_assert_internal(_mi_ptr_page(block) != page); - } -#endif - - // and abandon it - mi_assert_internal(mi_page_heap(page) == NULL); - _mi_segment_page_abandon(page,segments_tld); -} - - -// Free a page with no more free blocks -void _mi_page_free(mi_page_t* page, mi_page_queue_t* pq, bool force) { - mi_assert_internal(page != NULL); - mi_assert_expensive(_mi_page_is_valid(page)); - mi_assert_internal(pq == mi_page_queue_of(page)); - mi_assert_internal(mi_page_all_free(page)); - mi_assert_internal(mi_page_thread_free_flag(page)!=MI_DELAYED_FREEING); - - // no more aligned blocks in here - mi_page_set_has_aligned(page, false); - - mi_heap_t* heap = mi_page_heap(page); - -#ifdef Py_GIL_DISABLED - mi_assert_internal(page->qsbr_goal == 0); - mi_assert_internal(page->qsbr_node.next == NULL); -#endif - - // remove from the page list - // (no need to do _mi_heap_delayed_free first as all blocks are already free) - mi_segments_tld_t* segments_tld = &heap->tld->segments; - mi_page_queue_remove(pq, page); - - // and free it - mi_page_set_heap(page,NULL); - _mi_segment_page_free(page, force, segments_tld); -} - -// Retire parameters -#define MI_MAX_RETIRE_SIZE (MI_MEDIUM_OBJ_SIZE_MAX) -#define MI_RETIRE_CYCLES (16) - -// Retire a page with no more used blocks -// Important to not retire too quickly though as new -// allocations might coming. -// Note: called from `mi_free` and benchmarks often -// trigger this due to freeing everything and then -// allocating again so careful when changing this. -void _mi_page_retire(mi_page_t* page) mi_attr_noexcept { - mi_assert_internal(page != NULL); - mi_assert_expensive(_mi_page_is_valid(page)); - mi_assert_internal(mi_page_all_free(page)); - - mi_page_set_has_aligned(page, false); - - // any previous QSBR goals are no longer valid because we reused the page - _PyMem_mi_page_clear_qsbr(page); - - // don't retire too often.. - // (or we end up retiring and re-allocating most of the time) - // NOTE: refine this more: we should not retire if this - // is the only page left with free blocks. It is not clear - // how to check this efficiently though... - // for now, we don't retire if it is the only page left of this size class. - mi_page_queue_t* pq = mi_page_queue_of(page); - if mi_likely(page->xblock_size <= MI_MAX_RETIRE_SIZE && !mi_page_queue_is_special(pq)) { // not too large && not full or huge queue? - if (pq->last==page && pq->first==page) { // the only page in the queue? - mi_stat_counter_increase(_mi_stats_main.page_no_retire,1); - page->retire_expire = 1 + (page->xblock_size <= MI_SMALL_OBJ_SIZE_MAX ? MI_RETIRE_CYCLES : MI_RETIRE_CYCLES/4); - mi_heap_t* heap = mi_page_heap(page); - mi_assert_internal(pq >= heap->pages); - const size_t index = pq - heap->pages; - mi_assert_internal(index < MI_BIN_FULL && index < MI_BIN_HUGE); - if (index < heap->page_retired_min) heap->page_retired_min = index; - if (index > heap->page_retired_max) heap->page_retired_max = index; - mi_assert_internal(mi_page_all_free(page)); - return; // don't free after all - } - } - _PyMem_mi_page_maybe_free(page, pq, false); -} - -// free retired pages: we don't need to look at the entire queues -// since we only retire pages that are at the head position in a queue. -void _mi_heap_collect_retired(mi_heap_t* heap, bool force) { - size_t min = MI_BIN_FULL; - size_t max = 0; - for(size_t bin = heap->page_retired_min; bin <= heap->page_retired_max; bin++) { - mi_page_queue_t* pq = &heap->pages[bin]; - mi_page_t* page = pq->first; - if (page != NULL && page->retire_expire != 0) { - if (mi_page_all_free(page)) { - page->retire_expire--; - if (force || page->retire_expire == 0) { -#ifdef Py_GIL_DISABLED - mi_assert_internal(page->qsbr_goal == 0); -#endif - _PyMem_mi_page_maybe_free(page, pq, force); - } - else { - // keep retired, update min/max - if (bin < min) min = bin; - if (bin > max) max = bin; - } - } - else { - page->retire_expire = 0; - } - } - } - heap->page_retired_min = min; - heap->page_retired_max = max; -} - - -/* ----------------------------------------------------------- - Initialize the initial free list in a page. - In secure mode we initialize a randomized list by - alternating between slices. ------------------------------------------------------------ */ - -#define MI_MAX_SLICE_SHIFT (6) // at most 64 slices -#define MI_MAX_SLICES (1UL << MI_MAX_SLICE_SHIFT) -#define MI_MIN_SLICES (2) - -static void mi_page_free_list_extend_secure(mi_heap_t* const heap, mi_page_t* const page, const size_t bsize, const size_t extend, mi_stats_t* const stats) { - MI_UNUSED(stats); - #if (MI_SECURE<=2) - mi_assert_internal(page->free == NULL); - mi_assert_internal(page->local_free == NULL); - #endif - mi_assert_internal(page->capacity + extend <= page->reserved); - mi_assert_internal(bsize == mi_page_block_size(page)); - void* const page_area = _mi_page_start(_mi_page_segment(page), page, NULL); - - // initialize a randomized free list - // set up `slice_count` slices to alternate between - size_t shift = MI_MAX_SLICE_SHIFT; - while ((extend >> shift) == 0) { - shift--; - } - const size_t slice_count = (size_t)1U << shift; - const size_t slice_extend = extend / slice_count; - mi_assert_internal(slice_extend >= 1); - mi_block_t* blocks[MI_MAX_SLICES]; // current start of the slice - size_t counts[MI_MAX_SLICES]; // available objects in the slice - for (size_t i = 0; i < slice_count; i++) { - blocks[i] = mi_page_block_at(page, page_area, bsize, page->capacity + i*slice_extend); - counts[i] = slice_extend; - } - counts[slice_count-1] += (extend % slice_count); // final slice holds the modulus too (todo: distribute evenly?) - - // and initialize the free list by randomly threading through them - // set up first element - const uintptr_t r = _mi_heap_random_next(heap); - size_t current = r % slice_count; - counts[current]--; - mi_block_t* const free_start = blocks[current]; - // and iterate through the rest; use `random_shuffle` for performance - uintptr_t rnd = _mi_random_shuffle(r|1); // ensure not 0 - for (size_t i = 1; i < extend; i++) { - // call random_shuffle only every INTPTR_SIZE rounds - const size_t round = i%MI_INTPTR_SIZE; - if (round == 0) rnd = _mi_random_shuffle(rnd); - // select a random next slice index - size_t next = ((rnd >> 8*round) & (slice_count-1)); - while (counts[next]==0) { // ensure it still has space - next++; - if (next==slice_count) next = 0; - } - // and link the current block to it - counts[next]--; - mi_block_t* const block = blocks[current]; - blocks[current] = (mi_block_t*)((uint8_t*)block + bsize); // bump to the following block - mi_block_set_next(page, block, blocks[next]); // and set next; note: we may have `current == next` - current = next; - } - // prepend to the free list (usually NULL) - mi_block_set_next(page, blocks[current], page->free); // end of the list - page->free = free_start; -} - -static mi_decl_noinline void mi_page_free_list_extend( mi_page_t* const page, const size_t bsize, const size_t extend, mi_stats_t* const stats) -{ - MI_UNUSED(stats); - #if (MI_SECURE <= 2) - mi_assert_internal(page->free == NULL); - mi_assert_internal(page->local_free == NULL); - #endif - mi_assert_internal(page->capacity + extend <= page->reserved); - mi_assert_internal(bsize == mi_page_block_size(page)); - void* const page_area = _mi_page_start(_mi_page_segment(page), page, NULL ); - - mi_block_t* const start = mi_page_block_at(page, page_area, bsize, page->capacity); - - // initialize a sequential free list - mi_block_t* const last = mi_page_block_at(page, page_area, bsize, page->capacity + extend - 1); - mi_block_t* block = start; - while(block <= last) { - mi_block_t* next = (mi_block_t*)((uint8_t*)block + bsize); - mi_block_set_next(page,block,next); - block = next; - } - // prepend to free list (usually `NULL`) - mi_block_set_next(page, last, page->free); - page->free = start; -} - -/* ----------------------------------------------------------- - Page initialize and extend the capacity ------------------------------------------------------------ */ - -#define MI_MAX_EXTEND_SIZE (4*1024) // heuristic, one OS page seems to work well. -#if (MI_SECURE>0) -#define MI_MIN_EXTEND (8*MI_SECURE) // extend at least by this many -#else -#define MI_MIN_EXTEND (4) -#endif - -// Extend the capacity (up to reserved) by initializing a free list -// We do at most `MI_MAX_EXTEND` to avoid touching too much memory -// Note: we also experimented with "bump" allocation on the first -// allocations but this did not speed up any benchmark (due to an -// extra test in malloc? or cache effects?) -static void mi_page_extend_free(mi_heap_t* heap, mi_page_t* page, mi_tld_t* tld) { - MI_UNUSED(tld); - mi_assert_expensive(mi_page_is_valid_init(page)); - #if (MI_SECURE<=2) - mi_assert(page->free == NULL); - mi_assert(page->local_free == NULL); - if (page->free != NULL) return; - #endif - if (page->capacity >= page->reserved) return; - - size_t page_size; - _mi_page_start(_mi_page_segment(page), page, &page_size); - mi_stat_counter_increase(tld->stats.pages_extended, 1); - - // calculate the extend count - const size_t bsize = (page->xblock_size < MI_HUGE_BLOCK_SIZE ? page->xblock_size : page_size); - size_t extend = page->reserved - page->capacity; - mi_assert_internal(extend > 0); - - size_t max_extend = (bsize >= MI_MAX_EXTEND_SIZE ? MI_MIN_EXTEND : MI_MAX_EXTEND_SIZE/(uint32_t)bsize); - if (max_extend < MI_MIN_EXTEND) { max_extend = MI_MIN_EXTEND; } - mi_assert_internal(max_extend > 0); - - if (extend > max_extend) { - // ensure we don't touch memory beyond the page to reduce page commit. - // the `lean` benchmark tests this. Going from 1 to 8 increases rss by 50%. - extend = max_extend; - } - - mi_assert_internal(extend > 0 && extend + page->capacity <= page->reserved); - mi_assert_internal(extend < (1UL<<16)); - - // and append the extend the free list - if (extend < MI_MIN_SLICES || MI_SECURE==0) { //!mi_option_is_enabled(mi_option_secure)) { - mi_page_free_list_extend(page, bsize, extend, &tld->stats ); - } - else { - mi_page_free_list_extend_secure(heap, page, bsize, extend, &tld->stats); - } - // enable the new free list - page->capacity += (uint16_t)extend; - mi_stat_increase(tld->stats.page_committed, extend * bsize); - mi_assert_expensive(mi_page_is_valid_init(page)); -} - -// Initialize a fresh page -static void mi_page_init(mi_heap_t* heap, mi_page_t* page, size_t block_size, mi_tld_t* tld) { - mi_assert(page != NULL); - mi_segment_t* segment = _mi_page_segment(page); - mi_assert(segment != NULL); - mi_assert_internal(block_size > 0); - // set fields - mi_page_set_heap(page, heap); - page->tag = heap->tag; - page->use_qsbr = heap->page_use_qsbr; - page->debug_offset = heap->debug_offset; - page->xblock_size = (block_size < MI_HUGE_BLOCK_SIZE ? (uint32_t)block_size : MI_HUGE_BLOCK_SIZE); // initialize before _mi_segment_page_start - size_t page_size; - const void* page_start = _mi_segment_page_start(segment, page, &page_size); - MI_UNUSED(page_start); - mi_track_mem_noaccess(page_start,page_size); - mi_assert_internal(mi_page_block_size(page) <= page_size); - mi_assert_internal(page_size <= page->slice_count*MI_SEGMENT_SLICE_SIZE); - mi_assert_internal(page_size / block_size < (1L<<16)); - page->reserved = (uint16_t)(page_size / block_size); - mi_assert_internal(page->reserved > 0); - #if (MI_PADDING || MI_ENCODE_FREELIST) - page->keys[0] = _mi_heap_random_next(heap); - page->keys[1] = _mi_heap_random_next(heap); - #endif - page->free_is_zero = page->is_zero_init; - #if MI_DEBUG>2 - if (page->is_zero_init) { - mi_track_mem_defined(page_start, page_size); - mi_assert_expensive(mi_mem_is_zero(page_start, page_size)); - } - #endif - - mi_assert_internal(page->is_committed); - mi_assert_internal(page->capacity == 0); - mi_assert_internal(page->free == NULL); - mi_assert_internal(page->used == 0); - mi_assert_internal(page->xthread_free == 0); - mi_assert_internal(page->next == NULL); - mi_assert_internal(page->prev == NULL); -#ifdef Py_GIL_DISABLED - mi_assert_internal(page->qsbr_goal == 0); - mi_assert_internal(page->qsbr_node.next == NULL); -#endif - mi_assert_internal(page->retire_expire == 0); - mi_assert_internal(!mi_page_has_aligned(page)); - #if (MI_PADDING || MI_ENCODE_FREELIST) - mi_assert_internal(page->keys[0] != 0); - mi_assert_internal(page->keys[1] != 0); - #endif - mi_assert_expensive(mi_page_is_valid_init(page)); - - // initialize an initial free list - mi_page_extend_free(heap,page,tld); - mi_assert(mi_page_immediate_available(page)); -} - - -/* ----------------------------------------------------------- - Find pages with free blocks --------------------------------------------------------------*/ - -// Find a page with free blocks of `page->block_size`. -static mi_page_t* mi_page_queue_find_free_ex(mi_heap_t* heap, mi_page_queue_t* pq, bool first_try) -{ - // search through the pages in "next fit" order - #if MI_STAT - size_t count = 0; - #endif - mi_page_t* page = pq->first; - while (page != NULL) - { - mi_page_t* next = page->next; // remember next - #if MI_STAT - count++; - #endif - - // 0. collect freed blocks by us and other threads - _mi_page_free_collect(page, false); - - // 1. if the page contains free blocks, we are done - if (mi_page_immediate_available(page)) { - break; // pick this one - } - - // 2. Try to extend - if (page->capacity < page->reserved) { - mi_page_extend_free(heap, page, heap->tld); - mi_assert_internal(mi_page_immediate_available(page)); - break; - } - - // 3. If the page is completely full, move it to the `mi_pages_full` - // queue so we don't visit long-lived pages too often. - mi_assert_internal(!mi_page_is_in_full(page) && !mi_page_immediate_available(page)); - mi_page_to_full(page, pq); - - page = next; - } // for each page - - mi_heap_stat_counter_increase(heap, searches, count); - - if (page == NULL) { - _PyMem_mi_heap_collect_qsbr(heap); // some pages might be safe to free now - _mi_heap_collect_retired(heap, false); // perhaps make a page available? - page = mi_page_fresh(heap, pq); - if (page == NULL && first_try) { - // out-of-memory _or_ an abandoned page with free blocks was reclaimed, try once again - page = mi_page_queue_find_free_ex(heap, pq, false); - } - } - else { - mi_assert(pq->first == page); - page->retire_expire = 0; - _PyMem_mi_page_clear_qsbr(page); - } - mi_assert_internal(page == NULL || mi_page_immediate_available(page)); - return page; -} - - - -// Find a page with free blocks of `size`. -static inline mi_page_t* mi_find_free_page(mi_heap_t* heap, size_t size) { - mi_page_queue_t* pq = mi_page_queue(heap,size); - mi_page_t* page = pq->first; - if (page != NULL) { - #if (MI_SECURE>=3) // in secure mode, we extend half the time to increase randomness - if (page->capacity < page->reserved && ((_mi_heap_random_next(heap) & 1) == 1)) { - mi_page_extend_free(heap, page, heap->tld); - mi_assert_internal(mi_page_immediate_available(page)); - } - else - #endif - { - _mi_page_free_collect(page,false); - } - - if (mi_page_immediate_available(page)) { - page->retire_expire = 0; - _PyMem_mi_page_clear_qsbr(page); - return page; // fast path - } - } - return mi_page_queue_find_free_ex(heap, pq, true); -} - - -/* ----------------------------------------------------------- - Users can register a deferred free function called - when the `free` list is empty. Since the `local_free` - is separate this is deterministically called after - a certain number of allocations. ------------------------------------------------------------ */ - -static mi_deferred_free_fun* volatile deferred_free = NULL; -static _Atomic(void*) deferred_arg; // = NULL - -void _mi_deferred_free(mi_heap_t* heap, bool force) { - heap->tld->heartbeat++; - if (deferred_free != NULL && !heap->tld->recurse) { - heap->tld->recurse = true; - deferred_free(force, heap->tld->heartbeat, mi_atomic_load_ptr_relaxed(void,&deferred_arg)); - heap->tld->recurse = false; - } -} - -void mi_register_deferred_free(mi_deferred_free_fun* fn, void* arg) mi_attr_noexcept { - deferred_free = fn; - mi_atomic_store_ptr_release(void,&deferred_arg, arg); -} - - -/* ----------------------------------------------------------- - General allocation ------------------------------------------------------------ */ - -// Large and huge page allocation. -// Huge pages are allocated directly without being in a queue. -// Because huge pages contain just one block, and the segment contains -// just that page, we always treat them as abandoned and any thread -// that frees the block can free the whole page and segment directly. -// Huge pages are also use if the requested alignment is very large (> MI_ALIGNMENT_MAX). -static mi_page_t* mi_large_huge_page_alloc(mi_heap_t* heap, size_t size, size_t page_alignment) { - size_t block_size = _mi_os_good_alloc_size(size); - mi_assert_internal(mi_bin(block_size) == MI_BIN_HUGE || page_alignment > 0); - bool is_huge = (block_size > MI_LARGE_OBJ_SIZE_MAX || page_alignment > 0); - #if MI_HUGE_PAGE_ABANDON - mi_page_queue_t* pq = (is_huge ? NULL : mi_page_queue(heap, block_size)); - #else - mi_page_queue_t* pq = mi_page_queue(heap, is_huge ? MI_HUGE_BLOCK_SIZE : block_size); // not block_size as that can be low if the page_alignment > 0 - mi_assert_internal(!is_huge || mi_page_queue_is_huge(pq)); - #endif - mi_page_t* page = mi_page_fresh_alloc(heap, pq, block_size, page_alignment); - if (page != NULL) { - mi_assert_internal(mi_page_immediate_available(page)); - - if (is_huge) { - mi_assert_internal(_mi_page_segment(page)->kind == MI_SEGMENT_HUGE); - mi_assert_internal(_mi_page_segment(page)->used==1); - #if MI_HUGE_PAGE_ABANDON - mi_assert_internal(_mi_page_segment(page)->thread_id==0); // abandoned, not in the huge queue - mi_page_set_heap(page, NULL); - #endif - } - else { - mi_assert_internal(_mi_page_segment(page)->kind != MI_SEGMENT_HUGE); - } - - const size_t bsize = mi_page_usable_block_size(page); // note: not `mi_page_block_size` to account for padding - if (bsize <= MI_LARGE_OBJ_SIZE_MAX) { - mi_heap_stat_increase(heap, large, bsize); - mi_heap_stat_counter_increase(heap, large_count, 1); - } - else { - mi_heap_stat_increase(heap, huge, bsize); - mi_heap_stat_counter_increase(heap, huge_count, 1); - } - } - return page; -} - - -// Allocate a page -// Note: in debug mode the size includes MI_PADDING_SIZE and might have overflowed. -static mi_page_t* mi_find_page(mi_heap_t* heap, size_t size, size_t huge_alignment) mi_attr_noexcept { - // huge allocation? - const size_t req_size = size - MI_PADDING_SIZE; // correct for padding_size in case of an overflow on `size` - if mi_unlikely(req_size > (MI_MEDIUM_OBJ_SIZE_MAX - MI_PADDING_SIZE) || huge_alignment > 0) { - if mi_unlikely(req_size > PTRDIFF_MAX) { // we don't allocate more than PTRDIFF_MAX (see ) - _mi_error_message(EOVERFLOW, "allocation request is too large (%zu bytes)\n", req_size); - return NULL; - } - else { - _PyMem_mi_heap_collect_qsbr(heap); - return mi_large_huge_page_alloc(heap,size,huge_alignment); - } - } - else { - // otherwise find a page with free blocks in our size segregated queues - #if MI_PADDING - mi_assert_internal(size >= MI_PADDING_SIZE); - #endif - return mi_find_free_page(heap, size); - } -} - -// Generic allocation routine if the fast path (`alloc.c:mi_page_malloc`) does not succeed. -// Note: in debug mode the size includes MI_PADDING_SIZE and might have overflowed. -// The `huge_alignment` is normally 0 but is set to a multiple of MI_SEGMENT_SIZE for -// very large requested alignments in which case we use a huge segment. -void* _mi_malloc_generic(mi_heap_t* heap, size_t size, bool zero, size_t huge_alignment) mi_attr_noexcept -{ - mi_assert_internal(heap != NULL); - - // initialize if necessary - if mi_unlikely(!mi_heap_is_initialized(heap)) { - heap = mi_heap_get_default(); // calls mi_thread_init - if mi_unlikely(!mi_heap_is_initialized(heap)) { return NULL; } - } - mi_assert_internal(mi_heap_is_initialized(heap)); - - // call potential deferred free routines - _mi_deferred_free(heap, false); - - // free delayed frees from other threads (but skip contended ones) - _mi_heap_delayed_free_partial(heap); - - // find (or allocate) a page of the right size - mi_page_t* page = mi_find_page(heap, size, huge_alignment); - if mi_unlikely(page == NULL) { // first time out of memory, try to collect and retry the allocation once more - mi_heap_collect(heap, true /* force */); - page = mi_find_page(heap, size, huge_alignment); - } - - if mi_unlikely(page == NULL) { // out of memory - const size_t req_size = size - MI_PADDING_SIZE; // correct for padding_size in case of an overflow on `size` - _mi_error_message(ENOMEM, "unable to allocate memory (%zu bytes)\n", req_size); - return NULL; - } - - mi_assert_internal(mi_page_immediate_available(page)); - mi_assert_internal(mi_page_block_size(page) >= size); - - // and try again, this time succeeding! (i.e. this should never recurse through _mi_page_malloc) - if mi_unlikely(zero && page->xblock_size == 0) { - // note: we cannot call _mi_page_malloc with zeroing for huge blocks; we zero it afterwards in that case. - void* p = _mi_page_malloc(heap, page, size, false); - mi_assert_internal(p != NULL); - _mi_memzero_aligned(p, mi_page_usable_block_size(page)); - return p; - } - else { - return _mi_page_malloc(heap, page, size, zero); - } -} diff --git a/Objects/mimalloc/prim/emscripten/prim.c b/Objects/mimalloc/prim/emscripten/prim.c new file mode 100644 index 000000000000000..14eef47cff2da74 --- /dev/null +++ b/Objects/mimalloc/prim/emscripten/prim.c @@ -0,0 +1,266 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen, Alon Zakai +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +// This file is included in `src/prim/prim.c` + +#include "mimalloc.h" +#include "mimalloc/prim.h" +#include "mimalloc/heap/init.h" + +#include // sched_yield +#include // getentropy + +// Design +// ====== +// +// mimalloc is built on top of emmalloc. emmalloc is a minimal allocator on top +// of sbrk. The reason for having three layers here is that we want mimalloc to +// be able to allocate and release system memory properly, the same way it would +// when using VirtualAlloc on Windows or mmap on POSIX, and sbrk is too limited. +// Specifically, sbrk can only go up and down, and not "skip" over regions, and +// so we end up either never freeing memory to the system, or we can get stuck +// with holes. +// +// Atm wasm generally does *not* free memory back the system: once grown, we do +// not shrink back down (https://github.com/WebAssembly/design/issues/1397). +// However, that is expected to improve +// (https://github.com/WebAssembly/memory-control/blob/main/proposals/memory-control/Overview.md) +// and so we do not want to bake those limitations in here. +// +// Even without that issue, we want our system allocator to handle holes, that +// is, it should merge freed regions and allow allocating new content there of +// the full size, etc., so that we do not waste space. That means that the +// system allocator really does need to handle the general problem of allocating +// and freeing variable-sized chunks of memory in a random order, like malloc/ +// free do. And so it makes sense to layer mimalloc on top of such an +// implementation. +// +// emmalloc makes sense for the lower level because it is small and simple while +// still fully handling merging of holes etc. It is not the most efficient +// allocator, but our assumption is that mimalloc needs to be fast while the +// system allocator underneath it is called much less frequently. +// + +//--------------------------------------------- +// init +//--------------------------------------------- + +void _mi_prim_mem_init( mi_os_mem_config_t* config) { + config->page_size = 64*MI_KiB; // WebAssembly has a fixed page size: 64KiB + config->alloc_granularity = 16; + config->has_overcommit = false; + config->has_partial_free = false; + config->has_virtual_reserve = false; +} + +// CPython: use the builtin allocator (dlmalloc by default) rather than requiring +// the process to link with -sMALLOC=emmalloc. +#include // emscripten_builtin_memalign, emscripten_builtin_free + +int _mi_prim_free(void* addr, size_t size) { + emscripten_builtin_free(addr); + return 0; +} + + +//--------------------------------------------- +// Allocation +//--------------------------------------------- + +// Note: the `try_alignment` is just a hint and the returned pointer is not guaranteed to be aligned. +int _mi_prim_alloc(void* hint_addr, size_t size, size_t try_alignment, bool commit, bool allow_large, bool* is_large, bool* is_zero, void** addr) { + MI_UNUSED(try_alignment); MI_UNUSED(allow_large); MI_UNUSED(commit); MI_UNUSED(hint_addr); + *is_large = false; + // todo: Track the highest address ever seen; first uses of it are zeroes. + // That assumes no one else uses sbrk but us (they could go up, + // scribble, and then down), but we could assert on that perhaps. + *is_zero = false; + // emmalloc has a minimum alignment size. + #define MIN_EMMALLOC_ALIGN 8 + if (try_alignment < MIN_EMMALLOC_ALIGN) { + try_alignment = MIN_EMMALLOC_ALIGN; + } + void* p = emscripten_builtin_memalign(try_alignment, size); + *addr = p; + if (p == 0) { + return ENOMEM; + } + return 0; +} + + +//--------------------------------------------- +// Commit/Reset +//--------------------------------------------- + +int _mi_prim_commit(void* addr, size_t size, bool* is_zero) { + MI_UNUSED(addr); MI_UNUSED(size); + // See todo above. + *is_zero = false; + return 0; +} + +int _mi_prim_decommit(void* addr, size_t size, bool* needs_recommit) { + MI_UNUSED(addr); MI_UNUSED(size); + *needs_recommit = false; + return 0; +} + +int _mi_prim_reset(void* addr, size_t size) { + MI_UNUSED(addr); MI_UNUSED(size); + return 0; +} + +int _mi_prim_reuse(void* addr, size_t size) { + MI_UNUSED(addr); MI_UNUSED(size); + return 0; +} + +int _mi_prim_protect(void* addr, size_t size, bool protect) { + MI_UNUSED(addr); MI_UNUSED(size); MI_UNUSED(protect); + return 0; +} + + +//--------------------------------------------- +// Huge pages and NUMA nodes +//--------------------------------------------- + +int _mi_prim_alloc_huge_os_pages(void* hint_addr, size_t size, int numa_node, bool* is_zero, void** addr) { + MI_UNUSED(hint_addr); MI_UNUSED(size); MI_UNUSED(numa_node); + *is_zero = true; + *addr = NULL; + return ENOSYS; +} + +size_t _mi_prim_numa_node(void) { + return 0; +} + +size_t _mi_prim_numa_node_count(void) { + return 1; +} + + +//---------------------------------------------------------------- +// Clock +//---------------------------------------------------------------- + +#include + +mi_msecs_t _mi_prim_clock_now(void) { + // todo: use a monotonic clock instead + return emscripten_date_now(); +} + + +//---------------------------------------------------------------- +// Process info +//---------------------------------------------------------------- + +void _mi_prim_process_info(mi_process_info_t* pinfo) +{ + // use defaults + MI_UNUSED(pinfo); +} + + +//---------------------------------------------------------------- +// Output +//---------------------------------------------------------------- + +#include + +void _mi_prim_out_stderr( const char* msg) { + emscripten_console_error(msg); +} + + +//---------------------------------------------------------------- +// Environment +//---------------------------------------------------------------- + +int _mi_prim_getenv(const char* name, char* result, size_t result_size) { + // For code size reasons, do not support environ customization for now. + MI_UNUSED(name); + MI_UNUSED(result); + MI_UNUSED(result_size); + return 0; // not found +} + + +//---------------------------------------------------------------- +// Random +//---------------------------------------------------------------- + +bool _mi_prim_random_buf(void* buf, size_t buf_len) { + int err = getentropy(buf, buf_len); + return !err; +} + + +//---------------------------------------------------------------- +// Thread init/done +//---------------------------------------------------------------- + +#if defined(MI_USE_PTHREADS) + +// use pthread local storage keys to detect thread ending +// (and used with MI_TLS_PTHREADS for the default theap) +pthread_key_t _mi_heap_default_key = MI_PTHREAD_KEY_INVALID; + +static void mi_pthread_done(void* value) { + if (value!=NULL) { + _mi_thread_done((mi_theap_t*)value); + } +} + +void _mi_prim_thread_init_auto_done(void) { + mi_assert_internal(_mi_heap_default_key == MI_PTHREAD_KEY_INVALID); + pthread_key_create(&_mi_heap_default_key, &mi_pthread_done); +} + +void _mi_prim_thread_done_auto_done(void) { + pthread_key_t key = _mi_heap_default_key; + if (key != MI_PTHREAD_KEY_INVALID) { // do not leak the key, see issue #809 + _mi_heap_default_key = MI_PTHREAD_KEY_INVALID; + pthread_key_delete(key); + } +} + +void _mi_prim_thread_associate_default_theap(mi_theap_t* theap) { + if (_mi_heap_default_key != MI_PTHREAD_KEY_INVALID) { // can happen during recursive invocation on freeBSD + pthread_setspecific(_mi_heap_default_key, theap); + } +} + +#else + +void _mi_prim_thread_init_auto_done(void) { + // nothing +} + +void _mi_prim_thread_done_auto_done(void) { + // nothing +} + +void _mi_prim_thread_associate_default_theap(mi_theap_t* theap) { + MI_UNUSED(theap); +} +#endif + +bool _mi_prim_thread_is_in_threadpool(void) { + return false; +} + +void _mi_prim_thread_yield(void) { + sched_yield(); +} + +bool _mi_prim_process_is_killed(void) { + return false; +} diff --git a/Objects/mimalloc/prim/os.c b/Objects/mimalloc/prim/os.c new file mode 100644 index 000000000000000..1904f1317c0d232 --- /dev/null +++ b/Objects/mimalloc/prim/os.c @@ -0,0 +1,956 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#include "mimalloc.h" +#include "mimalloc/prim.h" +#include "mimalloc/track.h" +#include "mimalloc/prim/prim-tls.h" // _mi_theap_default for random +#include "mimalloc/util/libc.h" +#include "mimalloc/util/options.h" +#include "mimalloc/util/stats.h" +#include "mimalloc/prim/os.h" + +/* ----------------------------------------------------------- + Initialization. +----------------------------------------------------------- */ +#ifndef MI_DEFAULT_PHYSICAL_MEMORY_IN_KIB +#if MI_SIZE_SIZE < 8 +#define MI_DEFAULT_PHYSICAL_MEMORY_IN_KIB 4*MI_MiB // 4 GiB +#else +#define MI_DEFAULT_PHYSICAL_MEMORY_IN_KIB 32*MI_MiB // 32 GiB +#endif +#endif + +static mi_os_mem_config_t mi_os_mem_config = { + 4096, // page size + 0, // large page size (usually 2MiB) + 4096, // allocation granularity + MI_DEFAULT_PHYSICAL_MEMORY_IN_KIB, + MI_MAX_VABITS, // in `bits.h` + true, // has overcommit? (if true we use MAP_NORESERVE on mmap systems) + false, // can we partially free allocated blocks? (on mmap systems we can free anywhere in a mapped range, but on Windows we must free the entire span) + true, // has virtual reserve? (if true we can reserve virtual address space without using commit or physical memory) + false // has transparent huge pages? (if true we purge in (aligned) large page size chunks only to not fragment such pages) +}; + +bool _mi_os_has_overcommit(void) { + return mi_os_mem_config.has_overcommit; +} + +bool _mi_os_has_virtual_reserve(void) { + return mi_os_mem_config.has_virtual_reserve; +} + +bool _mi_os_canuse_thp(void) { + return mi_os_mem_config.has_transparent_huge_pages; +} + +// OS (small) page size +size_t _mi_os_page_size(void) { + return mi_os_mem_config.page_size; +} + +// if large OS pages are supported (2 or 4MiB), then return the size, otherwise return the small page size (4KiB) +size_t _mi_os_large_page_size(void) { + return (mi_os_mem_config.large_page_size != 0 ? mi_os_mem_config.large_page_size : _mi_os_page_size()); +} + +// minimal purge size. Can be larger than the page size if transparent huge pages are enabled. +size_t _mi_os_minimal_purge_size(void) { + size_t minsize = mi_option_get_size(mi_option_minimal_purge_size); + if (minsize != 0) { + // set by user + return mi_align_up(minsize, _mi_os_page_size()); + } + else if (mi_os_mem_config.has_transparent_huge_pages && mi_option_get(mi_option_allow_thp) == 2) { + // don't break up THP pages; + // we don't do this by default as that can lead to increased memory usage (see issue #1282). + // on the other hand, breaking up THP pages can lead to performance degradation and perhaps + // allow_thp should be 2 by default (and the user can set the OS THP setting to [never]). + return _mi_os_large_page_size(); + } + else { + // OS page size by default + return _mi_os_page_size(); + } +} + +size_t _mi_os_guard_page_size(void) { + const size_t gsize = _mi_os_page_size(); + mi_assert(gsize <= (MI_ARENA_SLICE_SIZE/4)); // issue #1166 + return gsize; +} + +size_t _mi_os_virtual_address_bits(void) { + const size_t vbits = mi_os_mem_config.virtual_address_bits; + mi_assert(vbits <= MI_MAX_VABITS); + return vbits; +} + +bool _mi_os_canuse_large_page(size_t size, size_t alignment) { + // if we have access, check the size and alignment requirements + if (mi_os_mem_config.large_page_size == 0) return false; + return ((size % mi_os_mem_config.large_page_size) == 0 && (alignment % mi_os_mem_config.large_page_size) == 0); +} + +// round to a good OS allocation size (bounded by max 12.5% waste) +size_t _mi_os_good_alloc_size(size_t size) { + size_t align_size; + if (size < 512*MI_KiB) align_size = _mi_os_page_size(); + else if (size < 2*MI_MiB) align_size = 64*MI_KiB; + else if (size < 8*MI_MiB) align_size = 256*MI_KiB; + else if (size < 32*MI_MiB) align_size = 1*MI_MiB; + else align_size = 4*MI_MiB; + if mi_unlikely(size >= (SIZE_MAX - align_size)) return size; // possible overflow? + return mi_align_up(size, align_size); +} + +void _mi_os_init(void) { + _mi_prim_mem_init(&mi_os_mem_config); +} + + +/* ----------------------------------------------------------- + Util +-------------------------------------------------------------- */ +bool _mi_os_decommit(mi_subproc_t* subproc, void* addr, size_t size); +bool _mi_os_commit(mi_subproc_t* subproc, void* addr, size_t size, bool* is_zero); + +// On systems with enough virtual address bits, we can do efficient aligned allocation by using +// the 2TiB to 30TiB area to allocate those. If we have at least 46 bits of virtual address +// space (64TiB) we use this technique. (but see issue #939) +#if (MI_SIZE_SIZE >= 8) && !defined(MI_NO_ALIGNED_HINT) // && !defined(WIN32) && !defined(ANDROID) + +// Return a `try_alignment` aligned address that is probably available. +// If this returns NULL, the OS will determine the address but on some OS's that may not be +// properly aligned which can be more costly as it needs to be adjusted afterwards. +// In secure mode, for a size > 16GiB this always returns NULL in order to guarantee good ASLR randomization; +// (otherwise an initial large allocation of say 2TiB has a 50% chance to include (known) addresses +// in the middle of the 2TiB - 6TiB address range (see issue #372)) + +#define MI_HINT_BASE ((uintptr_t)2 << 40) // 2TiB start +#define MI_HINT_AREA ((uintptr_t)4 << 40) // upto (2+4) 6TiB (since before win8 there is "only" 8TiB available to processes) +#define MI_HINT_MAX ((uintptr_t)30 << 40) // wrap after 30TiB (area after 32TiB is used for huge OS pages) + +void* _mi_os_get_aligned_hint(size_t try_alignment, size_t sze) +{ + static mi_decl_cache_align _Atomic(uintptr_t) aligned_base; // = 0 + + // todo: perhaps only do alignment hints if THP is enabled? + if (try_alignment <= mi_os_mem_config.alloc_granularity || try_alignment > 16*MI_GiB) return NULL; + if (mi_os_mem_config.virtual_address_bits < 46) return NULL; // < 64TiB virtual address space + + size_t req_size = sze + _mi_os_page_size(); // always reserve a bit more to create virtual gaps between hinted blocks. + req_size += (try_alignment - 1); // ensure we can align in the requested size + req_size = mi_align_up(req_size, _mi_os_large_page_size()); + #if (MI_SECURE>=1) + if (req_size > 32*MI_GiB) return NULL; // guarantee the chance of fixed valid address is at most 1/(MI_HINT_AREA / 1<<34) = 1/256 + #endif + + uintptr_t hint = mi_atomic_add_acq_rel(&aligned_base, req_size); + if (hint == 0 || hint > MI_HINT_MAX) { // wrap or initialize + uintptr_t init = MI_HINT_BASE; + #if (MI_SECURE>=1 || !MI_DEBUG) // security: randomize start of aligned allocations unless in debug mode + mi_theap_t* const theap = _mi_theap_default(); // don't use `mi_theap_get_default()` as that can cause allocation recursively (issue #1267) + if (!mi_theap_is_initialized(theap)) return NULL; // no hint as we lack randomness at this point + const uintptr_t r = _mi_theap_random_next(theap); + init = init + ((MI_MiB * ((r>>17) & 0x3FFFFF)) % MI_HINT_AREA); // (randomly 22 bits)* 1MiB == 0 to 4TiB + #endif + uintptr_t expected = hint + req_size; + mi_atomic_cas_strong_acq_rel(&aligned_base, &expected, init); + hint = mi_atomic_add_acq_rel(&aligned_base, req_size); // this may still give 0 or > MI_HINT_MAX but that is ok, it is a hint after all + if (hint==0) return NULL; + } + const uintptr_t hint_align = mi_align_up(hint,try_alignment); + mi_assert_internal(hint_align + sze < hint + req_size); + return (void*)hint_align; +} +#else +void* _mi_os_get_aligned_hint(size_t try_alignment, size_t size) { + MI_UNUSED(try_alignment); MI_UNUSED(size); + return NULL; +} +#endif + + +/* ----------------------------------------------------------- + Guard page allocation +----------------------------------------------------------- */ + +// In secure mode, return the size of a guard page, otherwise 0 +size_t _mi_os_secure_guard_page_size(void) { + #if MI_SECURE > 0 + return _mi_os_guard_page_size(); + #else + return 0; + #endif +} + +// In secure mode, try to decommit an area and output a warning if this fails. +bool _mi_os_secure_guard_page_set_at(mi_subproc_t* subproc, void* addr, mi_memid_t memid) { + if (addr == NULL) return true; + #if MI_SECURE > 0 + bool ok = false; + if (!memid.is_pinned) { + mi_arena_t* const arena = mi_memid_arena(memid); + if (arena != NULL && arena->commit_fun != NULL) { + ok = (*(arena->commit_fun))(false /* decommit */, addr, _mi_os_secure_guard_page_size(), NULL, arena->commit_fun_arg); + } + else { + ok = _mi_os_decommit(subproc, addr, _mi_os_secure_guard_page_size()); + } + } + if (!ok) { + _mi_error_message(EINVAL, "secure level %d, but failed to commit guard page (at %p of size %zu)\n", MI_SECURE, addr, _mi_os_secure_guard_page_size()); + } + return ok; + #else + MI_UNUSED(subproc); MI_UNUSED(memid); + return true; + #endif +} + +// In secure mode, try to decommit an area and output a warning if this fails. +bool _mi_os_secure_guard_page_set_before(mi_subproc_t* subproc, void* addr, mi_memid_t memid) { + return _mi_os_secure_guard_page_set_at(subproc, (uint8_t*)addr - _mi_os_secure_guard_page_size(), memid); +} + +// In secure mode, try to recommit an area +bool _mi_os_secure_guard_page_reset_at(mi_subproc_t* subproc, void* addr, mi_memid_t memid) { + if (addr == NULL) return true; + #if MI_SECURE > 0 + if (!memid.is_pinned) { + mi_arena_t* const arena = mi_memid_arena(memid); + if (arena != NULL && arena->commit_fun != NULL) { + return (*(arena->commit_fun))(true, addr, _mi_os_secure_guard_page_size(), NULL, arena->commit_fun_arg); + } + else { + return _mi_os_commit(subproc, addr, _mi_os_secure_guard_page_size(), NULL); + } + } + #else + MI_UNUSED(subproc); MI_UNUSED(memid); + #endif + return true; +} + +// In secure mode, try to recommit an area +bool _mi_os_secure_guard_page_reset_before(mi_subproc_t* subproc, void* addr, mi_memid_t memid) { + return _mi_os_secure_guard_page_reset_at(subproc, (uint8_t*)addr - _mi_os_secure_guard_page_size(), memid); +} + + +/* ----------------------------------------------------------- + Free memory +-------------------------------------------------------------- */ + +static void mi_os_free_huge_os_pages(mi_subproc_t* subproc, void* p, size_t size); + +static void mi_os_prim_free(mi_subproc_t* subproc, void* addr, size_t size, size_t commit_size, bool adjust) { + mi_assert_internal(subproc!=NULL); + mi_assert_internal((size % _mi_os_page_size()) == 0); + if (addr == NULL) return; // || _mi_os_is_huge_reserved(addr) + int err = _mi_prim_free(addr, size); // allow size==0 (issue #1041) + if (err != 0) { + _mi_warning_message("unable to free OS memory (error: %d (0x%x), size: 0x%zx bytes, address: %p)\n", err, err, size, addr); + } + if (adjust) { + if (commit_size>0) { mi_subproc_stat_adjust_decrease(subproc, committed, commit_size); } + mi_subproc_stat_adjust_decrease(subproc, reserved, size); + } + else { + if (commit_size>0) { mi_subproc_stat_decrease(subproc, committed, commit_size); } + mi_subproc_stat_decrease(subproc, reserved, size); + } +} + +void _mi_os_free_ex(mi_subproc_t* subproc, void* addr, size_t size, bool still_committed, mi_memid_t memid) { + if (mi_memkind_is_os(memid.memkind)) { + size_t csize = memid.mem.os.size; + if (csize==0) { csize = _mi_os_good_alloc_size(size); } + mi_assert_internal(csize >= size); + size_t commit_size = (still_committed ? csize : 0); + void* base = addr; + // different base? (due to alignment) + if (memid.mem.os.base != base) { + mi_assert(memid.mem.os.base <= addr); + base = memid.mem.os.base; + const size_t diff = (uint8_t*)addr - (uint8_t*)memid.mem.os.base; + if (memid.mem.os.size==0) { + csize += diff; + } + if (still_committed) { + commit_size -= diff; // the (addr-base) part was already un-committed + } + } + // free it + if (memid.memkind == MI_MEM_OS_HUGE) { + mi_assert(memid.is_pinned); + mi_os_free_huge_os_pages(subproc, base, csize); + } + else { + mi_os_prim_free(subproc, base, csize, (still_committed ? commit_size : 0), false); + } + } + else { + // nothing to do + mi_assert(memid.memkind < MI_MEM_OS); + } +} + +void _mi_os_free(mi_subproc_t* subproc, void* p, size_t size, mi_memid_t memid) { + _mi_os_free_ex(subproc, p, size, true, memid); +} + + +/* ----------------------------------------------------------- + Primitive allocation from the OS. +-------------------------------------------------------------- */ + +// Note: the `try_alignment` is just a hint and the returned pointer is not guaranteed to be aligned. +// Also `hint_addr` is a hint and may be ignored. +static void* mi_os_prim_alloc_at(mi_subproc_t* subproc, void* hint_addr, size_t size, size_t try_alignment, bool commit, bool allow_large, bool* is_large, bool* is_zero) { + mi_assert_internal(size > 0 && (size % _mi_os_page_size()) == 0); + mi_assert_internal(is_zero != NULL); + mi_assert_internal(is_large != NULL); + if (size == 0) return NULL; + if (!commit) { allow_large = false; } + if (try_alignment == 0) { try_alignment = 1; } // avoid 0 to ensure there will be no divide by zero when aligning + + // try to align along large OS page size for larger allocations + const size_t large_page_size = mi_os_mem_config.large_page_size; + if (large_page_size > 0 && hint_addr == NULL && size >= 8*large_page_size && mi_is_power_of_two(try_alignment) && try_alignment < large_page_size) { + try_alignment = large_page_size; + } + + *is_zero = false; + void* p = NULL; + int err = _mi_prim_alloc(hint_addr, size, try_alignment, commit, allow_large, is_large, is_zero, &p); + if (err != 0) { + _mi_warning_message("unable to allocate OS memory (error: %d (0x%x), addr: %p, size: 0x%zx bytes, align: 0x%zx, commit: %d, allow large: %d)\n", err, err, hint_addr, size, try_alignment, commit, allow_large); + } + + mi_subproc_stat_counter_increase(subproc, mmap_calls, 1); + if (p != NULL) { + mi_subproc_stat_increase(subproc, reserved, size); + if (commit) { + mi_subproc_stat_increase(subproc, committed, size); + // seems needed for asan (or `mimalloc-test-api` fails) + #ifdef MI_TRACK_ASAN + if (*is_zero) { mi_track_mem_defined(p,size); } + else { mi_track_mem_undefined(p,size); } + #endif + } + } + return p; +} + +static void* mi_os_prim_alloc(mi_subproc_t* subproc, size_t size, size_t try_alignment, bool commit, bool allow_large, bool* is_large, bool* is_zero) { + return mi_os_prim_alloc_at(subproc, NULL /* hint addr */, size, try_alignment, commit, allow_large, is_large, is_zero); +} + + +// Primitive aligned allocation from the OS. +// This function guarantees the allocated memory is aligned. +static void* mi_os_prim_alloc_aligned(mi_subproc_t* subproc, size_t size, size_t alignment, bool commit, bool allow_large, mi_memid_t* memid) { + mi_assert_internal(memid!=NULL); + mi_assert_internal(alignment >= _mi_os_page_size() && ((alignment & (alignment - 1)) == 0)); + mi_assert_internal(size > 0 && (size % _mi_os_page_size()) == 0); + *memid = _mi_memid_none(); + if (!commit) allow_large = false; + if (!(alignment >= _mi_os_page_size() && ((alignment & (alignment - 1)) == 0))) return NULL; + size = mi_align_up(size, _mi_os_page_size()); + + #if MI_SIZE_SIZE >= 8 + const bool try_direct_alloc = true; + #else + // try a direct allocation if the alignment is below the default, or less than or equal to 1/4 fraction of the size. + const bool try_direct_alloc = (alignment <= mi_os_mem_config.alloc_granularity || alignment <= size/4); + #endif + + bool os_is_large = false; + bool os_is_zero = false; + void* os_base = NULL; + size_t os_size = size; + void* p = NULL; + if (try_direct_alloc) { + p = mi_os_prim_alloc(subproc, size, alignment, commit, allow_large, &os_is_large, &os_is_zero); + } + + // aligned already? + if (p != NULL && mi_is_aligned(p,alignment)) { + os_base = p; + } + else { + // if not aligned, free it, overallocate, and unmap around it + #if !MI_TRACK_ASAN + if (try_direct_alloc) { + _mi_warning_message("unable to allocate aligned OS memory directly, fall back to over-allocation (size: 0x%zx bytes, address: %p, alignment: 0x%zx, commit: %d)\n", size, p, alignment, commit); + } + #endif + if (p != NULL) { mi_os_prim_free(subproc, p, size, (commit ? size : 0), true /* adjust so we "forget" the previous reservation */); } + if (size >= (SIZE_MAX - alignment)) return NULL; // overflow + const size_t over_size = size + alignment; + + if (!mi_os_mem_config.has_partial_free) { // win32 virtualAlloc cannot free parts of an allocated block + // over-allocate uncommitted (virtual) memory + p = mi_os_prim_alloc(subproc, over_size, 1 /*alignment*/, false /* commit? */, false /* allow_large */, &os_is_large, &os_is_zero); + if (p == NULL) return NULL; + + // set p to the aligned part in the full region + // note: Windows VirtualFree needs the actual base pointer + // this is handled though by having the `base` field in the memid + os_base = p; // remember the base + os_size = over_size; // todo: use size instead as now we over-decrement commit stats on free? + p = mi_align_up_ptr(p, alignment); + + // explicitly commit only the aligned part + if (commit) { + if (!_mi_os_commit(subproc, p, size, NULL)) { + mi_os_prim_free(subproc, os_base, over_size, 0, true); + return NULL; + } + } + } + else { // mmap can free inside an allocation + // overallocate... + // note: we keep `allow_large` but this only works if we can partially free any large OS pages later on. This seems to be the case though. + p = mi_os_prim_alloc(subproc, over_size, 1, commit, allow_large /* or false? */, &os_is_large, &os_is_zero); + if (p == NULL) return NULL; + + // and selectively unmap parts around the over-allocated area. + void* const aligned_p = mi_align_up_ptr(p, alignment); + const size_t pre_size = (uint8_t*)aligned_p - (uint8_t*)p; + const size_t mid_size = mi_align_up(size, _mi_os_page_size()); + const size_t post_size = over_size - pre_size - mid_size; + mi_assert_internal(pre_size < over_size&& post_size < over_size&& mid_size >= size); + if (pre_size > 0) { mi_os_prim_free(subproc, p, pre_size, (commit ? pre_size : 0), true /* adjust */); } + if (post_size > 0) { mi_os_prim_free(subproc, (uint8_t*)aligned_p + mid_size, post_size, (commit ? post_size : 0), true /* adjust */); } + // we can return the aligned pointer on `mmap` systems + p = aligned_p; + os_base = aligned_p; // since we freed the pre part, `*base == p`. + os_size = mid_size; + } + } + + mi_assert_internal(p != NULL && os_base != NULL && mi_is_aligned(p,alignment)); + mi_assert_internal(os_base <= p && size <= os_size); + *memid = _mi_memid_create_os(os_base,os_size,commit,os_is_zero,os_is_large); + return p; +} + + +/* ----------------------------------------------------------- + OS API: alloc and alloc_aligned +----------------------------------------------------------- */ + +void* _mi_os_alloc(mi_subproc_t* subproc, size_t size, mi_memid_t* memid) { + *memid = _mi_memid_none(); + if (size == 0) return NULL; + size = _mi_os_good_alloc_size(size); + bool os_is_large = false; + bool os_is_zero = false; + void* p = mi_os_prim_alloc(subproc, size, 0, true, false, &os_is_large, &os_is_zero); + if (p == NULL) return NULL; + + *memid = _mi_memid_create_os(p, size, true, os_is_zero, os_is_large); + mi_assert_internal(memid->mem.os.size >= size); + mi_assert_internal(memid->initially_committed); + return p; +} + +void* _mi_os_alloc_aligned(mi_subproc_t* subproc, size_t size, size_t alignment, bool commit, bool allow_large, mi_memid_t* memid) +{ + MI_UNUSED(&_mi_os_get_aligned_hint); // suppress unused warnings + *memid = _mi_memid_none(); + if (size == 0) return NULL; + size = _mi_os_good_alloc_size(size); + alignment = mi_align_up(alignment, _mi_os_page_size()); + + void* p = mi_os_prim_alloc_aligned(subproc, size, alignment, commit, allow_large, memid ); + if (p == NULL) return NULL; + + mi_assert_internal(memid->mem.os.size >= size); + mi_assert_internal(mi_is_aligned(p,alignment)); + if (commit) { mi_assert_internal(memid->initially_committed); } + return p; +} + + +mi_decl_nodiscard static void* mi_os_ensure_zero(mi_subproc_t* subproc, void* p, size_t size, mi_memid_t* memid) { + if (p==NULL || size==0) return p; + // ensure committed + if (!memid->initially_committed) { + bool is_zero = false; + if (!_mi_os_commit(subproc, p, size, &is_zero)) { + _mi_os_free(subproc, p, size, *memid); + return NULL; + } + memid->initially_committed = true; + } + // ensure zero'd + if (memid->initially_zero) return p; + mi_memzero_aligned(p,size); + memid->initially_zero = true; + return p; +} + +void* _mi_os_zalloc(mi_subproc_t* subproc, size_t size, mi_memid_t* memid) { + void* p = _mi_os_alloc(subproc, size,memid); + return mi_os_ensure_zero(subproc, p, size, memid); +} + +/* ----------------------------------------------------------- + OS aligned allocation with an offset. This is used + for large alignments > MI_BLOCK_ALIGNMENT_MAX. We use a large mimalloc + page where the object can be aligned at an offset from the start of the segment. + As we may need to overallocate, we need to free such pointers using `mi_free_aligned` + to use the actual start of the memory region. +----------------------------------------------------------- */ + +void* _mi_os_alloc_aligned_at_offset(mi_subproc_t* subproc, size_t size, size_t alignment, size_t offset, bool commit, bool allow_large, mi_memid_t* memid) { + mi_assert(offset <= size); + mi_assert((alignment % _mi_os_page_size()) == 0); + *memid = _mi_memid_none(); + if (offset > size) return NULL; + if (offset == 0) { + // regular aligned allocation + return _mi_os_alloc_aligned(subproc, size, alignment, commit, allow_large, memid); + } + else { + // overallocate to align at an offset + const size_t extra = mi_align_up(offset, alignment) - offset; + if (size >= SIZE_MAX - extra) return NULL; // too large + const size_t oversize = size + extra; + void* const start = _mi_os_alloc_aligned(subproc, oversize, alignment, commit, allow_large, memid); + if (start == NULL) return NULL; + + void* const p = (uint8_t*)start + extra; + mi_assert(mi_is_aligned((uint8_t*)p + offset, alignment)); + // decommit the overallocation at the start + // note: this double counts the decommit when freeing `memid`. Should we keep commit size in the memid as well? + if (commit && extra >= _mi_os_page_size()) { + _mi_os_decommit(subproc, start, extra); + } + return p; + } +} + +/* ----------------------------------------------------------- + OS memory API: reset, commit, decommit, protect, unprotect. +----------------------------------------------------------- */ + +// OS page align within a given area, either conservative (pages inside the area only), +// or not (straddling pages outside the area is possible) +static void* mi_os_page_align_areax(bool conservative, void* addr, size_t size, size_t* newsize) { + mi_assert(addr != NULL && size > 0); + if (newsize != NULL) *newsize = 0; + if (size == 0 || addr == NULL) return NULL; + + // page align conservatively within the range, or liberally straddling pages outside the range + void* start = (conservative ? mi_align_up_ptr(addr, _mi_os_page_size()) + : mi_align_down_ptr(addr, _mi_os_page_size())); + void* end = (conservative ? mi_align_down_ptr((uint8_t*)addr + size, _mi_os_page_size()) + : mi_align_up_ptr((uint8_t*)addr + size, _mi_os_page_size())); + ptrdiff_t diff = (uint8_t*)end - (uint8_t*)start; + if (diff <= 0) return NULL; + + mi_assert_internal((conservative && (size_t)diff <= size) || (!conservative && (size_t)diff >= size)); + if (newsize != NULL) *newsize = (size_t)diff; + return start; +} + +static void* mi_os_page_align_area_conservative(void* addr, size_t size, size_t* newsize) { + return mi_os_page_align_areax(true, addr, size, newsize); +} + +bool _mi_os_commit_ex(mi_subproc_t* subproc, void* addr, size_t size, bool* is_zero, size_t stat_already_committed) { + mi_assert_internal(size >= stat_already_committed); + if (is_zero != NULL) { *is_zero = false; } + mi_subproc_stat_counter_increase(subproc, commit_calls, 1); + + // page align range + size_t csize; + void* start = mi_os_page_align_areax(false /* conservative? */, addr, size, &csize); + if (csize == 0) return true; + + // commit + bool os_is_zero = false; + int err = _mi_prim_commit(start, csize, &os_is_zero); + if (err != 0) { + _mi_warning_message("cannot commit OS memory (error: %d (0x%x), address: %p, size: 0x%zx bytes)\n", err, err, start, csize); + return false; + } + if (os_is_zero && is_zero != NULL) { + *is_zero = true; + mi_assert_expensive(mi_mem_is_zero(start, csize)); + } + // note: the following seems required for asan (otherwise `mimalloc-test-stress` fails) + #ifdef MI_TRACK_ASAN + if (os_is_zero) { mi_track_mem_defined(start,csize); } + else { mi_track_mem_undefined(start,csize); } + #endif + mi_subproc_stat_increase(subproc, committed, size - stat_already_committed); // for precise commit vs. decommit + return true; +} + +bool _mi_os_commit(mi_subproc_t* subproc, void* addr, size_t size, bool* is_zero) { + return _mi_os_commit_ex(subproc, addr, size, is_zero, 0); +} + +static bool mi_os_decommit_ex(mi_subproc_t* subproc, void* addr, size_t size, bool* needs_recommit, size_t stat_size) { + mi_assert_internal(needs_recommit!=NULL); + + // page align + size_t csize; + void* start = mi_os_page_align_area_conservative(addr, size, &csize); + if (csize == 0) return true; + + // decommit + *needs_recommit = true; + int err = _mi_prim_decommit(start,csize,needs_recommit); + if (err != 0) { + _mi_warning_message("cannot decommit OS memory (error: %d (0x%x), address: %p, size: 0x%zx bytes)\n", err, err, start, csize); + } + else if (*needs_recommit) { + mi_subproc_stat_decrease(subproc, committed, stat_size); + } + mi_assert_internal(err == 0); + return (err == 0); +} + +bool _mi_os_decommit(mi_subproc_t* subproc, void* addr, size_t size) { + bool needs_recommit; + return mi_os_decommit_ex(subproc, addr, size, &needs_recommit, size); +} + + +// Signal to the OS that the address range is no longer in use +// but may be used later again. This will release physical memory +// pages and reduce swapping while keeping the memory committed. +// We page align to a conservative area inside the range to reset. +bool _mi_os_reset(mi_subproc_t* subproc, void* addr, size_t size) { + // page align conservatively within the range + size_t csize; + void* start = mi_os_page_align_area_conservative(addr, size, &csize); + if (csize == 0) return true; // || _mi_os_is_huge_reserved(addr) + mi_subproc_stat_counter_increase(subproc, reset, csize); + mi_subproc_stat_counter_increase(subproc, reset_calls, 1); + + #if (MI_DEBUG>1) && !MI_SECURE && !MI_TRACK_ENABLED // && !MI_TSAN + memset(start, 0, csize); // pretend it is eagerly reset + #endif + + int err = _mi_prim_reset(start, csize); + if (err != 0) { + _mi_warning_message("cannot reset OS memory (error: %d (0x%x), address: %p, size: 0x%zx bytes)\n", err, err, start, csize); + } + return (err == 0); +} + + +void _mi_os_reuse( mi_subproc_t* subproc, void* addr, size_t size ) { + MI_UNUSED(subproc); + // page align conservatively within the range + size_t csize = 0; + void* const start = mi_os_page_align_area_conservative(addr, size, &csize); + if (csize == 0) return; + const int err = _mi_prim_reuse(start, csize); + if (err != 0) { + _mi_warning_message("cannot reuse OS memory (error: %d (0x%x), address: %p, size: 0x%zx bytes)\n", err, err, start, csize); + } +} + +// either resets or decommits memory, returns true if the memory needs +// to be recommitted if it is to be re-used later on. +bool _mi_os_purge_ex(mi_subproc_t* subproc, void* p, size_t size, bool allow_reset, size_t stat_size, mi_commit_fun_t* commit_fun, void* commit_fun_arg) +{ + if (mi_option_get(mi_option_purge_delay) < 0) return false; // is purging allowed? + mi_subproc_stat_counter_increase(subproc, purge_calls, 1); + mi_subproc_stat_counter_increase(subproc, purged, size); + + if (commit_fun != NULL) { + bool decommitted = (*commit_fun)(false, p, size, NULL, commit_fun_arg); + return decommitted; // needs_recommit? + } + else if (mi_option_is_enabled(mi_option_purge_decommits) && // should decommit? + !_mi_preloading()) // don't decommit during preloading (unsafe) + { + bool needs_recommit = true; + mi_os_decommit_ex(subproc, p, size, &needs_recommit, stat_size); + return needs_recommit; + } + else { + if (allow_reset) { // this can sometimes be not allowed if the range is not fully committed (on Windows, we cannot reset uncommitted memory) + _mi_os_reset(subproc, p, size); + } + return false; // needs no recommit + } +} + +// either resets or decommits memory, returns true if the memory needs +// to be recommitted if it is to be re-used later on. +bool _mi_os_purge(mi_subproc_t* subproc, void* p, size_t size) { + return _mi_os_purge_ex(subproc, p, size, true, size, NULL, NULL); +} + + +// Protect a region in memory to be not accessible. +static bool mi_os_protectx(void* addr, size_t size, bool protect) { + // page align conservatively within the range + size_t csize = 0; + void* start = mi_os_page_align_area_conservative(addr, size, &csize); + if (csize == 0) return false; + /* + if (_mi_os_is_huge_reserved(addr)) { + _mi_warning_message("cannot mprotect memory allocated in huge OS pages\n"); + } + */ + int err = _mi_prim_protect(start,csize,protect); + if (err != 0) { + _mi_warning_message("cannot %s OS memory (error: %d (0x%x), address: %p, size: 0x%zx bytes)\n", (protect ? "protect" : "unprotect"), err, err, start, csize); + } + return (err == 0); +} + +bool _mi_os_protect(void* addr, size_t size) { + return mi_os_protectx(addr, size, true); +} + +bool _mi_os_unprotect(void* addr, size_t size) { + return mi_os_protectx(addr, size, false); +} + + + +/* ---------------------------------------------------------------------------- +Support for allocating huge OS pages (1Gib) that are reserved up-front +and possibly associated with a specific NUMA node. (use `numa_node>=0`) +-----------------------------------------------------------------------------*/ +#define MI_HUGE_OS_PAGE_SIZE (MI_GiB) + + +#if (MI_SIZE_SIZE >= 8) +// To ensure proper alignment, use our own area for huge OS pages +static mi_decl_cache_align _Atomic(uintptr_t) mi_huge_start; // = 0 + +// Claim an aligned address range for huge pages +static uint8_t* mi_os_claim_huge_pages(size_t pages, size_t* total_size) { + if (total_size != NULL) *total_size = 0; + size_t size = 0; + if (mi_mul_overflow(pages,MI_HUGE_OS_PAGE_SIZE,&size)) { + _mi_warning_message("too many huge pages requested: %zu\n", pages); + return NULL; + } + + uintptr_t start = 0; + uintptr_t end = 0; + uintptr_t huge_start = mi_atomic_load_relaxed(&mi_huge_start); + do { + start = huge_start; + if (start == 0) { + // Initialize the start address after the 32TiB area + start = ((uintptr_t)32 << 40); // 32TiB virtual start address (after addresses returned by _mi_os_get_aligned_hint) + #if (MI_SECURE>0 || MI_DEBUG==0) // security: randomize start of huge pages unless in debug mode + mi_theap_t* const theap = _mi_theap_default(); // don't use `mi_theap_get_default()` as that can cause allocation recursively (issue #1267) + if (mi_theap_is_initialized(theap)) { // todo: or no hint at all if we lack randomness? + const uintptr_t r = _mi_theap_random_next(theap); + start = start + ((uintptr_t)MI_HUGE_OS_PAGE_SIZE * ((r>>17) & 0x0FFF)); // (randomly 12bits)*1GiB == between 0 to 4TiB + } + else { + _mi_warning_message("failed to randomize the start address of huge pages allocation (%zu bytes at %p)", size, (void*)start); + } + #endif + } + end = start + size; + } while (!mi_atomic_cas_weak_acq_rel(&mi_huge_start, &huge_start, end)); + + if (total_size != NULL) *total_size = size; + return (uint8_t*)start; +} +#else +static uint8_t* mi_os_claim_huge_pages(size_t pages, size_t* total_size) { + MI_UNUSED(pages); + if (total_size != NULL) *total_size = 0; + return NULL; +} +#endif + +// Allocate MI_ARENA_SLICE_ALIGN aligned huge pages +void* _mi_os_alloc_huge_os_pages(mi_subproc_t* subproc, size_t pages, int numa_node, mi_msecs_t max_msecs, size_t* pages_reserved, size_t* psize, mi_memid_t* memid) { + *memid = _mi_memid_none(); + if (psize != NULL) *psize = 0; + if (pages_reserved != NULL) *pages_reserved = 0; + size_t size = 0; + uint8_t* const start = mi_os_claim_huge_pages(pages, &size); + if (start == NULL) return NULL; // or 32-bit systems + + // Allocate one page at the time but try to place them contiguously + // We allocate one page at the time to be able to abort if it takes too long + // or to at least allocate as many as available on the system. + mi_msecs_t start_t = _mi_clock_start(); + size_t page = 0; + bool all_zero = true; + while (page < pages) { + // allocate a page + bool is_zero = false; + void* addr = start + (page * MI_HUGE_OS_PAGE_SIZE); + void* p = NULL; + int err = _mi_prim_alloc_huge_os_pages(addr, MI_HUGE_OS_PAGE_SIZE, numa_node, &is_zero, &p); + if (!is_zero) { all_zero = false; } + if (err != 0) { + _mi_warning_message("unable to allocate huge OS page (error: %d (0x%x), address: %p, size: %zx bytes)\n", err, err, addr, MI_HUGE_OS_PAGE_SIZE); + break; + } + + // Did we succeed at a contiguous address? + if (p != addr) { + // no success, issue a warning and break + if (p != NULL) { + _mi_warning_message("could not allocate contiguous huge OS page %zu at %p\n", page, addr); + mi_os_prim_free(subproc, p, MI_HUGE_OS_PAGE_SIZE, MI_HUGE_OS_PAGE_SIZE, true /* adjust */); + } + break; + } + + // success, record it + page++; // increase before timeout check (see issue #711) + mi_subproc_stat_increase(subproc, committed, MI_HUGE_OS_PAGE_SIZE); + mi_subproc_stat_increase(subproc, reserved, MI_HUGE_OS_PAGE_SIZE); + + // check for timeout + if (max_msecs > 0) { + mi_msecs_t elapsed = _mi_clock_end(start_t); + if (page >= 1) { + mi_msecs_t estimate = ((elapsed / (page==0 ? 1 : page)) * pages); + if (estimate > 2*max_msecs) { // seems like we are going to timeout, break + elapsed = max_msecs + 1; + } + } + if (elapsed > max_msecs) { + _mi_warning_message("huge OS page allocation timed out (after allocating %zu page(s))\n", page); + break; + } + } + } + const size_t allocated = page * MI_HUGE_OS_PAGE_SIZE; + mi_assert_internal(allocated <= size); + if (pages_reserved != NULL) { *pages_reserved = page; } + if (psize != NULL) { *psize = allocated; } + if (page != 0) { + mi_assert(start != NULL); + *memid = _mi_memid_create_os(start, allocated, true /* is committed */, all_zero, true /* is_large */); + memid->memkind = MI_MEM_OS_HUGE; + mi_assert(memid->is_pinned); + #ifdef MI_TRACK_ASAN + if (all_zero) { mi_track_mem_defined(start,allocated); } + #endif + } + return (page == 0 ? NULL : start); +} + +// free every huge page in a range individually (as we allocated per page) +// note: needed with VirtualAlloc but could potentially be done in one go on mmap'd systems. +static void mi_os_free_huge_os_pages(mi_subproc_t* subproc, void* p, size_t size) { + if (p==NULL || size==0) return; + uint8_t* base = (uint8_t*)p; + while (size >= MI_HUGE_OS_PAGE_SIZE) { + mi_os_prim_free(subproc, base, MI_HUGE_OS_PAGE_SIZE, MI_HUGE_OS_PAGE_SIZE, false /* adjust? */); + size -= MI_HUGE_OS_PAGE_SIZE; + base += MI_HUGE_OS_PAGE_SIZE; + } +} + + +/* ---------------------------------------------------------------------------- +Support NUMA aware allocation +-----------------------------------------------------------------------------*/ + +static _Atomic(size_t) mi_numa_node_count; // = 0 // cache the node count + +int _mi_os_numa_node_count(void) { + size_t count = mi_atomic_load_acquire(&mi_numa_node_count); + if mi_unlikely(count == 0) { + long ncount = mi_option_get(mi_option_use_numa_nodes); // given explicitly? + if (ncount > 0 && ncount < INT_MAX) { + count = (size_t)ncount; + } + else { + const size_t n = _mi_prim_numa_node_count(); // or detect dynamically + if (n == 0 || n > INT_MAX) { count = 1; } + else { count = n; } + } + mi_atomic_store_release(&mi_numa_node_count, count); // save it + if (count>1) { _mi_verbose_message("using %zd numa regions\n", count); } + } + mi_assert_internal(count > 0 && count <= INT_MAX); + return (int)count; +} + +static int mi_os_numa_node_get(void) { + int numa_count = _mi_os_numa_node_count(); + if (numa_count<=1) return 0; // optimize on single numa node systems: always node 0 + // never more than the node count and >= 0 + const size_t n = _mi_prim_numa_node(); + int numa_node = (n < INT_MAX ? (int)n : 0); + if (numa_node >= numa_count) { numa_node = numa_node % numa_count; } + return numa_node; +} + +int _mi_os_numa_node(void) { + if mi_likely(mi_atomic_load_relaxed(&mi_numa_node_count) == 1) { + return 0; + } + else { + return mi_os_numa_node_get(); + } +} + + +/* ---------------------------------------------------------------------------- + Public API +-----------------------------------------------------------------------------*/ +#if 0 +mi_decl_export void* mi_os_alloc(size_t size, bool commit, size_t* full_size) { + return mi_os_alloc_aligned(size, mi_os_mem_config.alloc_granularity, commit, NULL, full_size); +} + +static void* mi_os_alloc_aligned_ex(size_t size, size_t alignment, bool commit, bool allow_large, bool* is_committed, bool* is_pinned, void** base, size_t* full_size) { + mi_memid_t memid = _mi_memid_none(); + void* p = _mi_os_alloc_aligned(size, alignment, commit, allow_large, &memid); + if (p == NULL) return p; + if (is_committed != NULL) { *is_committed = memid.initially_committed; } + if (is_pinned != NULL) { *is_pinned = memid.is_pinned; } + if (base != NULL) { *base = memid.mem.os.base; } + if (full_size != NULL) { *full_size = memid.mem.os.size; } + if (!memid.initially_zero && memid.initially_committed) { + mi_memzero_aligned(memid.mem.os.base, memid.mem.os.size); + } + return p; +} + +mi_decl_export void* mi_os_alloc_aligned(size_t size, size_t alignment, bool commit, void** base, size_t* full_size) { + return mi_os_alloc_aligned_ex(size, alignment, commit, false, NULL, NULL, base, full_size); +} + +mi_decl_export void* mi_os_alloc_aligned_allow_large(size_t size, size_t alignment, bool commit, bool* is_committed, bool* is_pinned, void** base, size_t* full_size) { + return mi_os_alloc_aligned_ex(size, alignment, commit, true, is_committed, is_pinned, base, full_size); +} + +mi_decl_export void mi_os_free(void* p, size_t size) { + if (p==NULL || size == 0) return; + mi_memid_t memid = _mi_memid_create_os(p, size, true, false, false); + _mi_os_free(p, size, memid); +} + +mi_decl_export void mi_os_commit(void* p, size_t size) { + _mi_os_commit(p, size, NULL); +} + +mi_decl_export void mi_os_decommit(void* p, size_t size) { + _mi_os_decommit(p, size); +} +#endif diff --git a/Objects/mimalloc/prim/osx/alloc-override-zone.c b/Objects/mimalloc/prim/osx/alloc-override-zone.c index 0e0a99d9388c8ac..00ba0fa88ad26ba 100644 --- a/Objects/mimalloc/prim/osx/alloc-override-zone.c +++ b/Objects/mimalloc/prim/osx/alloc-override-zone.c @@ -6,8 +6,7 @@ terms of the MIT license. A copy of the license can be found in the file -----------------------------------------------------------------------------*/ #include "mimalloc.h" -#include "mimalloc/internal.h" - +#include "mimalloc/prim/os.h" #if defined(MI_MALLOC_OVERRIDE) #if !defined(__APPLE__) @@ -25,7 +24,6 @@ terms of the MIT license. A copy of the license can be found in the file #include #include -#include // memset #include #ifdef __cplusplus @@ -41,10 +39,18 @@ extern malloc_zone_t* malloc_default_purgeable_zone(void) __attribute__((weak_im malloc zone members ------------------------------------------------------ */ +static bool is_mimalloc_zone( malloc_zone_t* zone ); + static size_t zone_size(malloc_zone_t* zone, const void* p) { - MI_UNUSED(zone); - if (!mi_is_in_heap_region(p)){ return 0; } // not our pointer, bail out - return mi_usable_size(p); + if (mi_any_heap_contains(p)) { + return mi_usable_size(p); + } + else if (!is_mimalloc_zone(zone)) { // can happen due to interpose + return zone->size(zone,p); + } + else { + return 0; + } } static void* zone_malloc(malloc_zone_t* zone, size_t size) { @@ -63,13 +69,25 @@ static void* zone_valloc(malloc_zone_t* zone, size_t size) { } static void zone_free(malloc_zone_t* zone, void* p) { - MI_UNUSED(zone); - mi_cfree(p); + // during C++ thread shutdown `_pthread_tsd_cleanup` may call `zone_free` + // after mimalloc mi_thread_done, and also on a pointer that was allocated in another subproc. + if mi_unlikely(!mi_cfree(p)) { + if (!is_mimalloc_zone(zone)) { // can happen due to interpose + zone->free(zone,p); + } + } } static void* zone_realloc(malloc_zone_t* zone, void* p, size_t newsize) { - MI_UNUSED(zone); - return mi_realloc(p, newsize); + if (p == NULL || mi_any_heap_contains(p)) { + return mi_realloc(p, newsize); + } + else if (!is_mimalloc_zone(zone)) { // can happen due to interpose + return zone->realloc(zone,p,newsize); + } + else { + return NULL; + } } static void* zone_memalign(malloc_zone_t* zone, size_t alignment, size_t size) { @@ -78,12 +96,13 @@ static void* zone_memalign(malloc_zone_t* zone, size_t alignment, size_t size) { } static void zone_destroy(malloc_zone_t* zone) { - MI_UNUSED(zone); - // todo: ignore for now? + if (!is_mimalloc_zone(zone)) { + zone->destroy(zone); + } } static unsigned zone_batch_malloc(malloc_zone_t* zone, size_t size, void** ps, unsigned count) { - size_t i; + unsigned i; for (i = 0; i < count; i++) { ps[i] = zone_malloc(zone, size); if (ps[i] == NULL) break; @@ -114,7 +133,6 @@ static boolean_t zone_claimed_address(malloc_zone_t* zone, void* p) { return mi_is_in_heap_region(p); } - /* ------------------------------------------------------ Introspection members ------------------------------------------------------ */ @@ -174,6 +192,14 @@ static boolean_t intro_zone_locked(malloc_zone_t* zone) { return false; } +// Required whenever the zone advertises version >= 9: macOS calls this from the +// atfork_child handler (_malloc_fork_child) without a NULL check. mimalloc keeps +// no zone-level locks that need reinitializing after fork, so a no-op is safe. +// Leaving it NULL makes the forked child jump to address 0 and crash in fork(). +static void intro_reinit_lock(malloc_zone_t* zone) { + MI_UNUSED(zone); +} + /* ------------------------------------------------------ At process start, override the default allocator @@ -199,6 +225,9 @@ static malloc_introspection_t mi_introspect = { .statistics = &intro_statistics, .zone_locked = &intro_zone_locked, #endif +#if defined(MAC_OS_X_VERSION_10_12) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_12) && !defined(__ppc__) + .reinit_lock = &intro_reinit_lock, +#endif }; static malloc_zone_t mi_malloc_zone = { @@ -225,7 +254,9 @@ static malloc_zone_t mi_malloc_zone = { // switch to version 9+ on OSX 10.6 to support memalign. .memalign = &zone_memalign, .free_definite_size = &zone_free_definite_size, + #if defined(MAC_OS_X_VERSION_10_7) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_7) .pressure_relief = &zone_pressure_relief, + #endif #if defined(MAC_OS_X_VERSION_10_14) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_14) .claimed_address = &zone_claimed_address, #endif @@ -238,6 +269,9 @@ static malloc_zone_t mi_malloc_zone = { } #endif +static bool is_mimalloc_zone( malloc_zone_t* zone ) { + return (zone==NULL || zone==&mi_malloc_zone); +} #if defined(MI_OSX_INTERPOSE) && defined(MI_SHARED_LIB_EXPORT) @@ -246,16 +280,13 @@ static malloc_zone_t mi_malloc_zone = { // our mimalloc zone. Since even the loader uses malloc // on macOS, this ensures that all allocations go through // mimalloc (as all calls are interposed). -// The main `malloc`, `free`, etc calls are interposed in `alloc-override.c`, +// The main `malloc`, `free`, etc calls are interposed in `alloc/alloc-override.c`, // Here, we also override macOS specific API's like // `malloc_zone_calloc` etc. see // ------------------------------------------------------ -static inline malloc_zone_t* mi_get_default_zone(void) -{ - static bool init; - if mi_unlikely(!init) { - init = true; +static inline malloc_zone_t* mi_get_default_zone(void) { + mi_atomic_do_once { malloc_zone_register(&mi_malloc_zone); // by calling register we avoid a zone error on free (see ) } return &mi_malloc_zone; @@ -326,7 +357,7 @@ static bool zone_check(malloc_zone_t* zone) { static malloc_zone_t* zone_from_ptr(const void* p) { MI_UNUSED(p); - return mi_get_default_zone(); + return (mi_any_heap_contains(p) ? mi_get_default_zone() : NULL); } static void zone_log(malloc_zone_t* zone, void* p) { @@ -351,16 +382,9 @@ static void zone_unregister(malloc_zone_t* zone) { // use interposing so `DYLD_INSERT_LIBRARIES` works without `DYLD_FORCE_FLAT_NAMESPACE=1` // See: -struct mi_interpose_s { - const void* replacement; - const void* target; -}; -#define MI_INTERPOSE_FUN(oldfun,newfun) { (const void*)&newfun, (const void*)&oldfun } -#define MI_INTERPOSE_MI(fun) MI_INTERPOSE_FUN(fun,mi_##fun) #define MI_INTERPOSE_ZONE(fun) MI_INTERPOSE_FUN(malloc_##fun,fun) -__attribute__((used)) static const struct mi_interpose_s _mi_zone_interposes[] __attribute__((section("__DATA, __interpose"))) = +MI_INTERPOSE_DECLS(_mi_zone_interposes) = { - MI_INTERPOSE_MI(malloc_create_zone), MI_INTERPOSE_MI(malloc_default_purgeable_zone), MI_INTERPOSE_MI(malloc_default_zone), @@ -416,10 +440,11 @@ static inline malloc_zone_t* mi_get_default_zone(void) } #if defined(__clang__) -__attribute__((constructor(0))) +__attribute__((constructor(101))) // highest priority #else -__attribute__((constructor)) // seems not supported by g++-11 on the M1 +__attribute__((constructor)) // priority level is not supported by gcc #endif +__attribute__((used)) static void _mi_macos_override_malloc(void) { malloc_zone_t* purgeable_zone = NULL; diff --git a/Objects/mimalloc/prim/prim-tls.c b/Objects/mimalloc/prim/prim-tls.c new file mode 100644 index 000000000000000..ddaa5d4576fb2c8 --- /dev/null +++ b/Objects/mimalloc/prim/prim-tls.c @@ -0,0 +1,252 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +#include "mimalloc.h" +#include "mimalloc/prim.h" +#include "mimalloc/prim/prim-tls.h" +#include "mimalloc/util/options.h" // _mi_error_message + +// -------------------------------------------------------------------------- +// Implement fast access to the thread local storage for `_mi_theap_default()` +// and `mi_theap_cached()`. See `include/mimalloc/prim/prim-tls.h` for more info +// on the TLS models. +// -------------------------------------------------------------------------- + +void _mi_tls_slots_init(void); +void _mi_tls_slots_done(void); +void _mi_theap_default_set(mi_theap_t* theap); +void _mi_theap_cached_set(mi_theap_t* theap); + + +#if MI_TLS_MODEL_LOCAL +// the thread-local main theap for allocation +mi_decl_hidden mi_decl_thread mi_theap_t* __mi_theap_default = (mi_theap_t*)&_mi_theap_empty; +// the last used non-main theap +mi_decl_hidden mi_decl_thread mi_theap_t* __mi_theap_cached = (mi_theap_t*)&_mi_theap_empty; +#endif + +mi_decl_hidden mi_decl_thread void* __mi_thread_id_helper = NULL; + +mi_threadid_t _mi_thread_id(void) mi_attr_noexcept { + const mi_threadid_t tid = _mi_prim_thread_id(); + mi_assert_internal( (tid & MI_PAGE_FLAG_MASK) == 0 ); // mimalloc reserves the bottom 2 bits + return tid; +} + +// ---------------------------------------------------------------------------- +// Setting the default and cached theap +// ---------------------------------------------------------------------------- + +#if MI_TLS_MODEL_WIN32 + +// If we can, we use one of the 64 direct TLS slots (but fall back to expansion slots if needed) +// See for the offsets. +#if MI_SIZE_SIZE==4 +#define MI_TLS_DIRECT_FIRST (0x0E10 / MI_SIZE_SIZE) +#else +#define MI_TLS_DIRECT_FIRST (0x1480 / MI_SIZE_SIZE) +#endif +#define MI_TLS_DIRECT_SLOTS (64) +#define MI_TLS_EXPANSION_SLOTS (1024) + +#if !MI_WIN_DIRECT_TLS +// We initially use the last of the expansion slots as the default NULL. +// note: this will fail if the program allocates exactly 1024+64 slots with TlsAlloc +// before we are initialized :-( (but this seems quite unlikely). +// (todo: another approach could be to use slot 7 (EnvironmentPointer) as the initial slot as that seems to be always NULL) +#define MI_TLS_INITIAL_SLOT MI_TLS_EXPANSION_SLOT +#define MI_TLS_INITIAL_EXPANSION_SLOT (MI_TLS_EXPANSION_SLOTS-1) +#else +// With direct tls we need an initial NULL slot outside the expansion slots +#define MI_TLS_INITIAL_SLOT (5) // Arbitrary user pointer +#define MI_TLS_INITIAL_EXPANSION_SLOT (MI_TLS_EXPANSION_SLOTS-1) // unused +#endif + +// in case of errors assign fixed slots (but since we use EFAULT the program should fail anyways) +#define MI_TLS_ERROR_SLOT (5) // arbitrary user pointer +#define MI_TLS_ERROR_EXPANSION_SLOT (7) // environment pointer (only used for OS/2 emulation) + + +mi_decl_hidden mi_decl_cache_align _Atomic(size_t) _mi_theap_default_slot = MI_ATOMIC_VAR_INIT(MI_TLS_INITIAL_SLOT); +mi_decl_hidden _Atomic(size_t) _mi_theap_default_expansion_slot = MI_ATOMIC_VAR_INIT(MI_TLS_INITIAL_EXPANSION_SLOT); +mi_decl_hidden _Atomic(size_t) _mi_theap_cached_slot = MI_ATOMIC_VAR_INIT(MI_TLS_INITIAL_SLOT); +mi_decl_hidden _Atomic(size_t) _mi_theap_cached_expansion_slot = MI_ATOMIC_VAR_INIT(MI_TLS_INITIAL_EXPANSION_SLOT); + +static DWORD mi_tls_raw_index_default = TLS_OUT_OF_INDEXES; +static DWORD mi_tls_raw_index_cached = TLS_OUT_OF_INDEXES; + +static bool mi_win_tls_slot_alloc(_Atomic(size_t)* slot, _Atomic(size_t)* extended, DWORD* raw_index) { + // always write slot before extended due to concurrent readers + const DWORD index = TlsAlloc(); + *raw_index = index; + if (index==TLS_OUT_OF_INDEXES) { + mi_atomic_store_release(slot,MI_TLS_ERROR_SLOT); + mi_atomic_store_release(extended,MI_TLS_ERROR_EXPANSION_SLOT); + return false; + } + else if (index= MI_TLS_DIRECT_FIRST && slot < MI_TLS_DIRECT_FIRST + MI_TLS_DIRECT_SLOTS) || slot == MI_TLS_EXPANSION_SLOT); + if (slot < MI_TLS_DIRECT_FIRST + MI_TLS_DIRECT_SLOTS) { + mi_prim_tls_slot_set(slot, value); + } + else { + mi_assert_internal(extended_slot < MI_TLS_EXPANSION_SLOTS); + TlsSetValue((DWORD)(extended_slot + MI_TLS_DIRECT_SLOTS), value); // use TlsSetValue to initialize the TlsExpansion array if needed + } +} + +#elif MI_TLS_MODEL_PTHREADS + +// only for pthreads for now +mi_decl_hidden _Atomic(pthread_key_t) _mi_theap_default_key = MI_ATOMIC_VAR_INIT(MI_PTHREAD_KEY_INVALID); +mi_decl_hidden _Atomic(pthread_key_t) _mi_theap_cached_key = MI_ATOMIC_VAR_INIT(MI_PTHREAD_KEY_INVALID); + +static void mi_theap_cached_key_destroy(void* theapv) { + mi_theap_t* theap = (mi_theap_t*)theapv; + if (theap!=NULL) { + _mi_theap_decref(theap); + } +} + +void _mi_tls_slots_init(void) { + mi_atomic_do_once { + pthread_key_t key; + if (_mi_pthread_key_create(&key,NULL,NULL)) { mi_atomic_store_release(&_mi_theap_default_key,key); } + if (_mi_pthread_key_create(&key,&mi_theap_cached_key_destroy,NULL)) { mi_atomic_store_release(&_mi_theap_cached_key,key); } + } +} + +void _mi_tls_slots_done(void) { + pthread_key_t key = mi_atomic_exchange_relaxed(&_mi_theap_default_key,MI_PTHREAD_KEY_INVALID); + if (key!=MI_PTHREAD_KEY_INVALID) { pthread_key_delete(key); } + key = mi_atomic_exchange_relaxed(&_mi_theap_cached_key,MI_PTHREAD_KEY_INVALID); + if (key!=MI_PTHREAD_KEY_INVALID) { pthread_key_delete(key); } +} + +#elif MI_TLS_MODEL_FIXED + +void _mi_tls_slots_init(void) { + mi_atomic_do_once { + mi_theap_t* theap = _mi_theap_default(); + if (theap!=NULL) { + _mi_error_message(EINVAL,"fixed TLS slot is already in use (slot %d = %p)", MI_TLS_MODEL_FIXED_DEFAULT, theap); + } + theap = _mi_theap_cached(); + if (theap!=NULL) { + _mi_error_message(EINVAL,"fixed TLS slot is already in use (slot %d = %p)", MI_TLS_MODEL_FIXED_CACHED, theap); + } + } +} + +void _mi_tls_slots_done(void) { + // nothing +} + + +#else + +void _mi_tls_slots_init(void) { + // nothing +} + +void _mi_tls_slots_done(void) { + // nothing +} + +#endif + +void _mi_theap_cached_set(mi_theap_t* theap) { + mi_theap_t* prev = _mi_theap_cached(); + if (prev==theap) return; + // set + _mi_tls_slots_init(); + #if MI_TLS_MODEL_LOCAL + __mi_theap_cached = theap; + #elif MI_TLS_MODEL_FIXED + mi_prim_tls_slot_set(MI_TLS_MODEL_FIXED_CACHED, theap); + #elif MI_TLS_MODEL_WIN32 + mi_win_tls_slot_set(mi_atomic_load_relaxed(&_mi_theap_cached_slot), mi_atomic_load_relaxed(&_mi_theap_cached_expansion_slot), theap); + #elif MI_TLS_MODEL_PTHREADS + pthread_key_t key = mi_atomic_load_relaxed(&_mi_theap_cached_key); + if (key!=MI_PTHREAD_KEY_INVALID) { pthread_setspecific(key, theap); } + #endif + // update refcounts (so cached theap memory keeps available until no longer cached) + _mi_theap_incref(theap); + _mi_theap_decref(prev); +} + +void _mi_theap_default_set(mi_theap_t* theap) { + mi_assert_internal(theap != NULL); + mi_assert_internal(theap->tld != NULL); + mi_assert_internal(mi_theap_matches_thread(theap)); + _mi_tls_slots_init(); + #if MI_TLS_MODEL_LOCAL + __mi_theap_default = theap; + #elif MI_TLS_MODEL_FIXED + mi_prim_tls_slot_set(MI_TLS_MODEL_FIXED_DEFAULT, theap); + #elif MI_TLS_MODEL_WIN32 + mi_win_tls_slot_set(mi_atomic_load_relaxed(&_mi_theap_default_slot), mi_atomic_load_relaxed(&_mi_theap_default_expansion_slot), theap); + #elif MI_TLS_MODEL_PTHREADS + pthread_key_t key = mi_atomic_load_relaxed(&_mi_theap_default_key); + if (key!=MI_PTHREAD_KEY_INVALID) { pthread_setspecific(key, theap); } + #endif + + // set theap main if needed + if (mi_theap_is_initialized(theap)) { + // ensure the default theap is passed to `_mi_thread_done` as on some platforms we cannot access TLS at thread termination (as it would allocate again) + _mi_prim_thread_associate_default_theap(theap); + } +} diff --git a/Objects/mimalloc/prim/prim.c b/Objects/mimalloc/prim/prim.c index 9a597d8eb603e9d..eaec32d771decb6 100644 --- a/Objects/mimalloc/prim/prim.c +++ b/Objects/mimalloc/prim/prim.c @@ -1,10 +1,14 @@ /* ---------------------------------------------------------------------------- -Copyright (c) 2018-2023, Microsoft Research, Daan Leijen +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen This is free software; you can redistribute it and/or modify it under the terms of the MIT license. A copy of the license can be found in the file "LICENSE" at the root of this distribution. -----------------------------------------------------------------------------*/ +#include "mimalloc/heap/init.h" +#include "mimalloc/prim.h" + + // Select the implementation of the primitives // depending on the OS. @@ -18,7 +22,58 @@ terms of the MIT license. A copy of the license can be found in the file #define MI_USE_SBRK #include "wasi/prim.c" // memory-grow or sbrk (Wasm) +#elif defined(__EMSCRIPTEN__) +#include "emscripten/prim.c" // emmalloc_*, + pthread support + #else #include "unix/prim.c" // mmap() (Linux, macOSX, BSD, Illumnos, Haiku, DragonFly, etc.) #endif + +// Generic process initialization +#if !defined(MI_PRIM_HAS_PROCESS_ATTACH) +#if defined(__GNUC__) || defined(__clang__) + // gcc,clang: use the constructor/destructor attribute with the highest user priority (<=100 is reserved for the compiler itself) + #if defined(__clang__) || (__GNUC__ >= 5 && !defined(__APPLE__)) // gcc has support since 4.2 but not on macOS (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=119435) + #define mi_attr_constructor __attribute__((constructor(101))) + #define mi_attr_destructor __attribute__((destructor(101))) + #else + #define mi_attr_constructor __attribute__((constructor)) + #define mi_attr_destructor __attribute__((destructor)) + #endif + static void mi_attr_constructor mi_process_attach(void) { + _mi_auto_process_init(); + } + static void mi_attr_destructor mi_process_detach(void) { + _mi_auto_process_done(); + } +#elif defined(__cplusplus) + // C++: use static initialization to detect process start/end + // This is not guaranteed to be first/last but the best we can generally do? + struct mi_init_done_t { + mi_init_done_t() { + _mi_auto_process_init(); + } + ~mi_init_done_t() { + _mi_auto_process_done(); + } + }; + static mi_init_done_t mi_init_done; + #else + #pragma message("define a way to call _mi_auto_process_init/done on your platform") +#endif +#endif + +// Generic allocator init/done callback +#ifndef MI_PRIM_HAS_ALLOCATOR_INIT +bool _mi_is_redirected(void) { + return false; +} +bool _mi_allocator_init(const char** message) { + if (message != NULL) { *message = NULL; } + return true; +} +void _mi_allocator_done(void) { + // nothing to do +} +#endif diff --git a/Objects/mimalloc/prim/readme.md b/Objects/mimalloc/prim/readme.md index 380dd3a71784190..1917c1a3d4e4ec9 100644 --- a/Objects/mimalloc/prim/readme.md +++ b/Objects/mimalloc/prim/readme.md @@ -1,9 +1,8 @@ -## Portability Primitives +# OS and Portability Primitives -This is the portability layer where all primitives needed from the OS are defined. +- [`include/mimalloc/prim.h`](/src/include/mimalloc/prim.h): primitive portability API definition. +- [`prim.c`](prim.c): This is the portability layer where all primitives needed from the OS are defined. + Selects one of [`unix/prim.c`](unix/prim.c), [`windows/prim.c`](windows/prim.c), [`wasi/prim.c`](wasi/prim.c), or [`emscripten/prim.c`](emscripten/prim.c) depending on the host platform (and on macOS, [`osx/prim.c`](osx/prim.c) defers to [`unix/prim.c`](unix/prim.c)). +- [`prim-tls.c`](prim-tls.c): primitive TLS model (thread local, pthreads, etc.) +- [`os.c`](os.c): A general higher level API around [`prim.c`](prim.c) that supports OS allocation, (de)commit, huge page allocation, etc. -- `include/mimalloc/prim.h`: primitive portability API definition. -- `prim.c`: Selects one of `unix/prim.c`, `wasi/prim.c`, or `windows/prim.c` depending on the host platform - (and on macOS, `osx/prim.c` defers to `unix/prim.c`). - -Note: still work in progress, there may still be places in the sources that still depend on OS ifdef's. \ No newline at end of file diff --git a/Objects/mimalloc/prim/unix/prim.c b/Objects/mimalloc/prim/unix/prim.c index 6f78eb72789e8e5..6d1402ff5513475 100644 --- a/Objects/mimalloc/prim/unix/prim.c +++ b/Objects/mimalloc/prim/unix/prim.c @@ -1,5 +1,5 @@ /* ---------------------------------------------------------------------------- -Copyright (c) 2018-2023, Microsoft Research, Daan Leijen +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen This is free software; you can redistribute it and/or modify it under the terms of the MIT license. A copy of the license can be found in the file "LICENSE" at the root of this distribution. @@ -21,27 +21,45 @@ terms of the MIT license. A copy of the license can be found in the file #endif #include "mimalloc.h" -#include "mimalloc/internal.h" -#include "mimalloc/atomic.h" #include "mimalloc/prim.h" +#include "mimalloc/track.h" +#include "mimalloc/prim/os.h" +#include "mimalloc/util/libc.h" +#include "mimalloc/util/options.h" +#include "mimalloc/heap/init.h" #include // mmap -#include // sysconf +#include // sysconf, sleep #include // open, close, read, access +#include // getenv, arc4random_buf #if defined(__linux__) #include - #include + #include // THP disable, PR_SET_VMA + #include // sysinfo + #if defined(__GLIBC__) && !defined(PR_SET_VMA) + #include + #endif #if defined(__GLIBC__) - #include // linux mmap flags - #else - #include + #include // linux mmap flags + #endif + #if defined(__riscv) || defined(_M_RISCV) + #if defined(MI_HAS_SYS_HWPROBEH) + #include + #elif defined(MI_HAS_ASM_HWPROBEH) + #include + #endif #endif #elif defined(__APPLE__) + #include #include - #if !TARGET_IOS_IPHONE && !TARGET_IOS_SIMULATOR - #include + #if !defined(TARGET_OS_OSX) || TARGET_OS_OSX // see issue #879, used to be (!TARGET_IOS_IPHONE && !TARGET_IOS_SIMULATOR) + #include // VM_MAKE_TAG, VM_FLAGS_SUPERPAGE_SIZE_2MB, etc. + #endif + #if !defined(MAC_OS_X_VERSION_10_7) + #define MAC_OS_X_VERSION_10_7 1070 #endif + #include #elif defined(__FreeBSD__) || defined(__DragonFly__) #include #if __FreeBSD_version >= 1200000 @@ -51,44 +69,68 @@ terms of the MIT license. A copy of the license can be found in the file #include #endif -#if !defined(__HAIKU__) && !defined(__APPLE__) && !defined(__CYGWIN__) && !defined(_AIX) && !defined(__OpenBSD__) && !defined(__FreeBSD__) && !defined(__sun) && !defined(__NetBSD__) +#if (defined(__linux__) && !defined(__ANDROID__)) || defined(__FreeBSD__) #define MI_HAS_SYSCALL_H #include #endif +// MSan does not intercept raw syscalls, so memory they write must be marked as initialized +#if defined(__has_feature) + #if __has_feature(memory_sanitizer) + #include // __msan_unpoison + #define mi_msan_unpoison(p,size) __msan_unpoison(p,size) + #endif +#endif +#ifndef mi_msan_unpoison +#define mi_msan_unpoison(p,size) +#endif + +#if !defined(MADV_DONTNEED) && defined(POSIX_MADV_DONTNEED) // QNX +#define MADV_DONTNEED POSIX_MADV_DONTNEED +#endif +#if !defined(MADV_FREE) && defined(POSIX_MADV_FREE) // QNX +#define MADV_FREE POSIX_MADV_FREE +#endif + +#define MI_UNIX_LARGE_PAGE_SIZE (2*MI_MiB) // todo: can we query the OS for this? + //------------------------------------------------------------------------------------ // Use syscalls for some primitives to allow for libraries that override open/read/close etc. // and do allocation themselves; using syscalls prevents recursion when mimalloc is // still initializing (issue #713) +// Not used on Android, see issue python/cpython/pull/158530 +// Declare inline to avoid unused function warnings. //------------------------------------------------------------------------------------ -#if defined(MI_HAS_SYSCALL_H) && defined(SYS_open) && defined(SYS_close) && defined(SYS_read) && defined(SYS_access) +#if defined(MI_HAS_SYSCALL_H) && defined(SYS_open) && defined(SYS_close) && defined(SYS_read) && defined(SYS_access) && !defined(__ANDROID__) -static int mi_prim_open(const char* fpath, int open_flags) { +static inline int mi_prim_open(const char* fpath, int open_flags) { return syscall(SYS_open,fpath,open_flags,0); } -static ssize_t mi_prim_read(int fd, void* buf, size_t bufsize) { - return syscall(SYS_read,fd,buf,bufsize); +static inline ssize_t mi_prim_read(int fd, void* buf, size_t bufsize) { + ssize_t nread = syscall(SYS_read,fd,buf,bufsize); + if (nread > 0) { mi_msan_unpoison(buf, (size_t)nread); } + return nread; } -static int mi_prim_close(int fd) { +static inline int mi_prim_close(int fd) { return syscall(SYS_close,fd); } -static int mi_prim_access(const char *fpath, int mode) { +static inline int mi_prim_access(const char *fpath, int mode) { return syscall(SYS_access,fpath,mode); } -#elif !defined(__APPLE__) && !defined(_AIX) && !defined(__OpenBSD__) && !defined(__FreeBSD__) && !defined(__sun) && !defined(__NetBSD__) // avoid unused warnings +#else -static int mi_prim_open(const char* fpath, int open_flags) { +static inline int mi_prim_open(const char* fpath, int open_flags) { return open(fpath,open_flags); } -static ssize_t mi_prim_read(int fd, void* buf, size_t bufsize) { +static inline ssize_t mi_prim_read(int fd, void* buf, size_t bufsize) { return read(fd,buf,bufsize); } -static int mi_prim_close(int fd) { +static inline int mi_prim_close(int fd) { return close(fd); } -static int mi_prim_access(const char *fpath, int mode) { +static inline int mi_prim_access(const char *fpath, int mode) { return access(fpath,mode); } @@ -102,40 +144,152 @@ static int mi_prim_access(const char *fpath, int mode) { static bool unix_detect_overcommit(void) { bool os_overcommit = true; -#if defined(__linux__) - int fd = mi_prim_open("/proc/sys/vm/overcommit_memory", O_RDONLY); - if (fd >= 0) { - char buf[32] = {0}; + #if defined(__linux__) + int fd = mi_prim_open("/proc/sys/vm/overcommit_memory", O_RDONLY); + if (fd >= 0) { + char buf[32]; + ssize_t nread = mi_prim_read(fd, &buf, sizeof(buf)); + mi_prim_close(fd); + // + // 0: heuristic overcommit, 1: always overcommit, 2: never overcommit (ignore NORESERVE) + if (nread >= 1) { + os_overcommit = (buf[0] == '0' || buf[0] == '1'); + } + } + #elif defined(__FreeBSD__) + int val = 0; + size_t olen = sizeof(val); + if (sysctlbyname("vm.overcommit", &val, &olen, NULL, 0) == 0) { + os_overcommit = (val != 0); + } + #else + // default: overcommit is true + #endif + return os_overcommit; +} + +static bool unix_detect_thp(void) { + bool thp_enabled = false; + #if defined(__linux__) + int fd = mi_prim_open("/sys/kernel/mm/transparent_hugepage/enabled", O_RDONLY); + if (fd >= 0) { + char buf[64]; ssize_t nread = mi_prim_read(fd, &buf, sizeof(buf)); mi_prim_close(fd); - // - // 0: heuristic overcommit, 1: always overcommit, 2: never overcommit (ignore NORESERVE) + // + // between brackets is the current value, for example: always [madvise] never if (nread >= 1) { - os_overcommit = (buf[0] == '0' || buf[0] == '1'); + if (nread > 64) { nread = 64; } + thp_enabled = (_mi_strnstr(buf,nread,"[never]") == NULL); } } -#elif defined(__FreeBSD__) - int val = 0; - size_t olen = sizeof(val); - if (sysctlbyname("vm.overcommit", &val, &olen, NULL, 0) == 0) { - os_overcommit = (val != 0); - } -#else - // default: overcommit is true -#endif - return os_overcommit; + #endif + return thp_enabled; } -void _mi_prim_mem_init( mi_os_mem_config_t* config ) { +// try to detect the physical memory dynamically (if possible) +static void unix_detect_physical_memory( size_t page_size, size_t* physical_memory_in_kib ) { + #if defined(CTL_HW) && (defined(HW_PHYSMEM64) || defined(HW_MEMSIZE)) // freeBSD, macOS + MI_UNUSED(page_size); + int64_t physical_memory = 0; + size_t length = sizeof(int64_t); + #if defined(HW_PHYSMEM64) + int mib[2] = { CTL_HW, HW_PHYSMEM64 }; + #else + int mib[2] = { CTL_HW, HW_MEMSIZE }; + #endif + const int err = sysctl(mib, 2, &physical_memory, &length, NULL, 0); + if (err==0 && physical_memory > 0) { + const int64_t phys_in_kib = physical_memory / MI_KiB; + if (phys_in_kib > 0 && (uint64_t)phys_in_kib <= SIZE_MAX) { + *physical_memory_in_kib = (size_t)phys_in_kib; + } + } + #elif defined(__linux__) + MI_UNUSED(page_size); + struct sysinfo info; _mi_memzero_var(info); + const int err = sysinfo(&info); + if (err==0 && info.mem_unit > 0 && info.totalram <= SIZE_MAX) { + if (info.mem_unit==MI_KiB) { + *physical_memory_in_kib = (size_t)info.totalram; + } + else { + size_t total = 0; + if (!mi_mul_overflow((size_t)info.totalram, (size_t)info.mem_unit, &total)) { + *physical_memory_in_kib = (total / MI_KiB); + } + } + } + #elif defined(_SC_PHYS_PAGES) // do not use by default as it might cause allocation (by using `fopen` to parse /proc/meminfo) (issue #1100) + const long pphys = sysconf(_SC_PHYS_PAGES); + const size_t psize_in_kib = page_size / MI_KiB; + if (psize_in_kib > 0 && pphys > 0 && (unsigned long)pphys <= SIZE_MAX && (size_t)pphys <= (SIZE_MAX/psize_in_kib)) { + *physical_memory_in_kib = (size_t)pphys * psize_in_kib; + } + #endif +} + +// Detect the virtual address bits (currently Linux/RISC-V only) +static size_t unix_detect_virtual_address_bits(void) { + #if defined(__riscv) || defined(_M_RISCV) + #if defined(RISCV_HWPROBE_KEY_HIGHEST_VIRT_ADDRESS) + struct riscv_hwprobe probe = { .key = RISCV_HWPROBE_KEY_HIGHEST_VIRT_ADDRESS, }; + // Prefer the GNU libc interface if available, as it can also use the VDSO + #if defined(MI_HAS_SYS_HWPROBEH) + if (__riscv_hwprobe(&probe, 1, 0, NULL, 0) == 0) + #else + if (syscall(__NR_riscv_hwprobe, &probe, 1, 0, NULL, 0) == 0) + #endif + { + if (probe.key != -1) { // If a key is unknown to the kernel, its key field will be cleared to -1. + return (MI_SIZE_BITS - mi_clz((uintptr_t)probe.value)); + } + } + #endif + // Fallback to checking /proc/cpuinfo for older kernels + const int fd = mi_prim_open("/proc/cpuinfo", O_RDONLY); + if (fd >= 0) { + char buf[2048]; + const ssize_t nread = mi_prim_read(fd, &buf, sizeof(buf)); + mi_prim_close(fd); + if ((nread >= 1) && (nread <= (ssize_t)sizeof(buf))) { + if (_mi_strnstr(buf, nread, "sv39")) { return 39; } + else if (_mi_strnstr(buf, nread, "sv48")) { return 48; } + else if (_mi_strnstr(buf, nread, "sv57")) { return 57; } + } + } + #endif // riscv + // default + return MI_MAX_VABITS; +} + +void _mi_prim_mem_init( mi_os_mem_config_t* config ) +{ long psize = sysconf(_SC_PAGESIZE); - if (psize > 0) { + if (psize > 0 && (unsigned long)psize < SIZE_MAX) { config->page_size = (size_t)psize; config->alloc_granularity = (size_t)psize; + unix_detect_physical_memory(config->page_size, &config->physical_memory_in_kib); } - config->large_page_size = 2*MI_MiB; // TODO: can we query the OS for this? + config->large_page_size = MI_UNIX_LARGE_PAGE_SIZE; config->has_overcommit = unix_detect_overcommit(); - config->must_free_whole = false; // mmap can free in parts + config->has_partial_free = true; // mmap can free in parts config->has_virtual_reserve = true; // todo: check if this true for NetBSD? (for anonymous mmap with PROT_NONE) + config->has_transparent_huge_pages = unix_detect_thp(); + config->virtual_address_bits = unix_detect_virtual_address_bits(); + + // disable transparent huge pages for this process? + #if (defined(__linux__) || defined(__ANDROID__)) && defined(PR_GET_THP_DISABLE) + if (!mi_option_is_enabled(mi_option_allow_thp)) // disable THP if requested through an option + { + config->has_transparent_huge_pages = false; + if (prctl(PR_GET_THP_DISABLE, 0, 0, 0, 0) == 0) { // -1 on error, 1 if already disabled + // Most likely since distros often come with always/madvise settings. + // Disabling only for mimalloc process rather than touching system wide settings + (void)prctl(PR_SET_THP_DISABLE, 1, 0, 0, 0); + } + } + #endif } @@ -144,6 +298,7 @@ void _mi_prim_mem_init( mi_os_mem_config_t* config ) { //--------------------------------------------- int _mi_prim_free(void* addr, size_t size ) { + if (size==0) return 0; bool err = (munmap(addr, size) == -1); return (err ? errno : 0); } @@ -153,25 +308,41 @@ int _mi_prim_free(void* addr, size_t size ) { // mmap //--------------------------------------------- +// return errno on failure static int unix_madvise(void* addr, size_t size, int advice) { - #if defined(__sun) - return madvise((caddr_t)addr, size, advice); // Solaris needs cast (issue #520) + #if defined(__sun) || defined(_AIX) + const int res = madvise((caddr_t)addr, size, advice); // Solaris needs cast (issue #520) + return (res==0 ? 0 : errno); + #elif defined(__QNX__) + return posix_madvise(addr, size, advice); // posix returns errno #else - return madvise(addr, size, advice); + const int res = madvise(addr, size, advice); // linux returns -1 on failure and sets errno + return (res==0 ? 0 : errno); #endif } -static void* unix_mmap_prim(void* addr, size_t size, size_t try_alignment, int protect_flags, int flags, int fd) { +static void* unix_mmap_prim(void* addr, size_t size, int protect_flags, int flags, int fd) { + void* p = mmap(addr, size, protect_flags, flags, fd, 0 /* offset */); + #if defined(__linux__) && defined(PR_SET_VMA) + if (p!=MAP_FAILED && p!=NULL) { + prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, p, size, "mimalloc"); + } + #endif + return p; +} + +static void* unix_mmap_prim_aligned(void* addr, size_t size, size_t try_alignment, int protect_flags, int flags, int fd) { MI_UNUSED(try_alignment); void* p = NULL; #if defined(MAP_ALIGNED) // BSD if (addr == NULL && try_alignment > 1 && (try_alignment % _mi_os_page_size()) == 0) { - size_t n = mi_bsr(try_alignment); + size_t n = 0; + mi_bsr(try_alignment, &n); if (((size_t)1 << n) == try_alignment && n >= 12 && n <= 30) { // alignment is a power of 2 and 4096 <= alignment <= 1GiB - p = mmap(addr, size, protect_flags, flags | MAP_ALIGNED(n), fd, 0); - if (p==MAP_FAILED || !_mi_is_aligned(p,try_alignment)) { + p = unix_mmap_prim(addr, size, protect_flags, flags | MAP_ALIGNED(n), fd); + if (p==MAP_FAILED || !mi_is_aligned(p,try_alignment)) { int err = errno; - _mi_verbose_message("unable to directly request aligned OS memory (error: %d (0x%x), size: 0x%zx bytes, alignment: 0x%zx, hint address: %p)\n", err, err, size, try_alignment, addr); + _mi_trace_message("unable to directly request aligned OS memory (error: %d (0x%x), size: 0x%zx bytes, alignment: 0x%zx, hint address: %p)\n", err, err, size, try_alignment, addr); } if (p!=MAP_FAILED) return p; // fall back to regular mmap @@ -179,24 +350,24 @@ static void* unix_mmap_prim(void* addr, size_t size, size_t try_alignment, int p } #elif defined(MAP_ALIGN) // Solaris if (addr == NULL && try_alignment > 1 && (try_alignment % _mi_os_page_size()) == 0) { - p = mmap((void*)try_alignment, size, protect_flags, flags | MAP_ALIGN, fd, 0); // addr parameter is the required alignment + p = unix_mmap_prim((void*)try_alignment, size, protect_flags, flags | MAP_ALIGN, fd); // addr parameter is the required alignment if (p!=MAP_FAILED) return p; // fall back to regular mmap } #endif - #if (MI_INTPTR_SIZE >= 8) && !defined(MAP_ALIGNED) + #if (MI_SIZE_SIZE >= 8) && !defined(MAP_ALIGNED) // on 64-bit systems, use the virtual address area after 2TiB for 4MiB aligned allocations if (addr == NULL) { void* hint = _mi_os_get_aligned_hint(try_alignment, size); if (hint != NULL) { - p = mmap(hint, size, protect_flags, flags, fd, 0); - if (p==MAP_FAILED || !_mi_is_aligned(p,try_alignment)) { + p = unix_mmap_prim(hint, size, protect_flags, flags, fd); + if (p==MAP_FAILED || !mi_is_aligned(p,try_alignment)) { #if MI_TRACK_ENABLED // asan sometimes does not instrument errno correctly? int err = 0; #else int err = errno; #endif - _mi_verbose_message("unable to directly request hinted aligned OS memory (error: %d (0x%x), size: 0x%zx bytes, alignment: 0x%zx, hint address: %p)\n", err, err, size, try_alignment, hint); + _mi_trace_message("unable to directly request hinted aligned OS memory (error: %d (0x%x), size: 0x%zx bytes, alignment: 0x%zx, hint address: %p)\n", err, err, size, try_alignment, hint); } if (p!=MAP_FAILED) return p; // fall back to regular mmap @@ -204,7 +375,7 @@ static void* unix_mmap_prim(void* addr, size_t size, size_t try_alignment, int p } #endif // regular mmap - p = mmap(addr, size, protect_flags, flags, fd, 0); + p = unix_mmap_prim(addr, size, protect_flags, flags, fd); if (p!=MAP_FAILED) return p; // failed to allocate return NULL; @@ -214,13 +385,17 @@ static int unix_mmap_fd(void) { #if defined(VM_MAKE_TAG) // macOS: tracking anonymous page with a specific ID. (All up to 98 are taken officially but LLVM sanitizers had taken 99) int os_tag = (int)mi_option_get(mi_option_os_tag); - if (os_tag < 100 || os_tag > 255) { os_tag = 100; } + if (os_tag < 100 || os_tag > 255) { os_tag = 254; } return VM_MAKE_TAG(os_tag); #else return -1; #endif } +#if defined(MAP_ALIGNED_SUPER) || defined(MAP_HUGETLB) || defined(MAP_HUGE_1GB) || defined(MAP_HUGE_2MB) || defined(VM_FLAGS_SUPERPAGE_SIZE_2MB) +#define MI_OS_HAS_HUGE_PAGES 1 +#endif + static void* unix_mmap(void* addr, size_t size, size_t try_alignment, int protect_flags, bool large_only, bool allow_large, bool* is_large) { #if !defined(MAP_ANONYMOUS) #define MAP_ANONYMOUS MAP_ANON @@ -238,7 +413,8 @@ static void* unix_mmap(void* addr, size_t size, size_t try_alignment, int protec protect_flags |= PROT_MAX(PROT_READ | PROT_WRITE); // BSD #endif // huge page allocation - if ((large_only || _mi_os_use_large_page(size, try_alignment)) && allow_large) { + #if MI_OS_HAS_HUGE_PAGES + if (allow_large && (large_only || (_mi_os_canuse_large_page(size, try_alignment) && mi_option_is_enabled(mi_option_allow_large_os_pages)))) { static _Atomic(size_t) large_page_try_ok; // = 0; size_t try_ok = mi_atomic_load_acquire(&large_page_try_ok); if (!large_only && try_ok > 0) { @@ -258,8 +434,8 @@ static void* unix_mmap(void* addr, size_t size, size_t try_alignment, int protec lflags |= MAP_HUGETLB; #endif #ifdef MAP_HUGE_1GB - static bool mi_huge_pages_available = true; - if ((size % MI_GiB) == 0 && mi_huge_pages_available) { + static _Atomic(size_t) mi_huge_1gib_pages_unavailable; + if (large_only && (size % MI_GiB) == 0 && (mi_atomic_load_relaxed(&mi_huge_1gib_pages_unavailable)==0)) { lflags |= MAP_HUGE_1GB; } else @@ -275,13 +451,15 @@ static void* unix_mmap(void* addr, size_t size, size_t try_alignment, int protec if (large_only || lflags != flags) { // try large OS page allocation *is_large = true; - p = unix_mmap_prim(addr, size, try_alignment, protect_flags, lflags, lfd); + p = unix_mmap_prim_aligned(addr, size, try_alignment, protect_flags, lflags, lfd); #ifdef MAP_HUGE_1GB - if (p == NULL && (lflags & MAP_HUGE_1GB) != 0) { - mi_huge_pages_available = false; // don't try huge 1GiB pages again - _mi_warning_message("unable to allocate huge (1GiB) page, trying large (2MiB) pages instead (errno: %i)\n", errno); + if (p == NULL && (lflags & MAP_HUGE_1GB) == MAP_HUGE_1GB) { + mi_atomic_store_relaxed(&mi_huge_1gib_pages_unavailable,1); // don't try huge 1GiB pages again + if (large_only) { + _mi_warning_message("unable to allocate huge (1GiB) page, trying large (2MiB) pages instead (errno: %i)\n", errno); + } lflags = ((lflags & ~MAP_HUGE_1GB) | MAP_HUGE_2MB); - p = unix_mmap_prim(addr, size, try_alignment, protect_flags, lflags, lfd); + p = unix_mmap_prim_aligned(addr, size, try_alignment, protect_flags, lflags, lfd); } #endif if (large_only) return p; @@ -290,48 +468,50 @@ static void* unix_mmap(void* addr, size_t size, size_t try_alignment, int protec } } } - } + } // huge pages + #else + MI_UNUSED(large_only); + #endif // regular allocation if (p == NULL) { *is_large = false; - p = unix_mmap_prim(addr, size, try_alignment, protect_flags, flags, fd); - if (p != NULL) { + p = unix_mmap_prim_aligned(addr, size, try_alignment, protect_flags, flags, fd); + #if !defined(MI_NO_THP) + if (p != NULL && allow_large && mi_option_is_enabled(mi_option_allow_thp) && _mi_os_canuse_large_page(size, try_alignment)) { #if defined(MADV_HUGEPAGE) - // Many Linux systems don't allow MAP_HUGETLB but they support instead - // transparent huge pages (THP). Generally, it is not required to call `madvise` with MADV_HUGE - // though since properly aligned allocations will already use large pages if available - // in that case -- in particular for our large regions (in `memory.c`). - // However, some systems only allow THP if called with explicit `madvise`, so - // when large OS pages are enabled for mimalloc, we call `madvise` anyways. - if (allow_large && _mi_os_use_large_page(size, try_alignment)) { + if (_mi_os_canuse_thp()) { + // Many Linux systems don't allow MAP_HUGETLB but they support instead + // transparent huge pages (THP). Generally, it is not required to call `madvise` with MADV_HUGE + // though since properly aligned allocations will already use large pages if available + // in that case -- in particular for our large regions (in `memory.c`). + // However, some systems only allow THP if called with explicit `madvise`, so + // when large OS pages are enabled for mimalloc, we call `madvise` anyways. if (unix_madvise(p, size, MADV_HUGEPAGE) == 0) { - *is_large = true; // possibly + // *is_large = true; // possibly }; } #elif defined(__sun) - if (allow_large && _mi_os_use_large_page(size, try_alignment)) { - struct memcntl_mha cmd = {0}; - cmd.mha_pagesize = 2*MI_MiB; - cmd.mha_cmd = MHA_MAPSIZE_VA; - if (memcntl((caddr_t)p, size, MC_HAT_ADVISE, (caddr_t)&cmd, 0, 0) == 0) { - *is_large = true; - } + struct memcntl_mha cmd = {0}; + cmd.mha_pagesize = _mi_os_large_page_size(); + cmd.mha_cmd = MHA_MAPSIZE_VA; + if (memcntl((caddr_t)p, size, MC_HAT_ADVISE, (caddr_t)&cmd, 0, 0) == 0) { + // *is_large = true; // possibly } #endif } + #endif } return p; } // Note: the `try_alignment` is just a hint and the returned pointer is not guaranteed to be aligned. -int _mi_prim_alloc(size_t size, size_t try_alignment, bool commit, bool allow_large, bool* is_large, bool* is_zero, void** addr) { +int _mi_prim_alloc(void* hint_addr, size_t size, size_t try_alignment, bool commit, bool allow_large, bool* is_large, bool* is_zero, void** addr) { mi_assert_internal(size > 0 && (size % _mi_os_page_size()) == 0); mi_assert_internal(commit || !allow_large); mi_assert_internal(try_alignment > 0); - *is_zero = true; int protect_flags = (commit ? (PROT_WRITE | PROT_READ) : PROT_NONE); - *addr = unix_mmap(NULL, size, try_alignment, protect_flags, false, allow_large, is_large); + *addr = unix_mmap(hint_addr, size, try_alignment, protect_flags, false, allow_large, is_large); return (*addr != NULL ? 0 : errno); } @@ -341,7 +521,7 @@ int _mi_prim_alloc(size_t size, size_t try_alignment, bool commit, bool allow_la //--------------------------------------------- static void unix_mprotect_hint(int err) { - #if defined(__linux__) && (MI_SECURE>=2) // guard page around every mimalloc page + #if defined(__linux__) && (MI_SECURE>=5 || MI_GUARDED) // guard page around every mimalloc page if (err == ENOMEM) { _mi_warning_message("The next warning may be caused by a low memory map limit.\n" " On Linux this is controlled by the vm.max_map_count -- maybe increase it?\n" @@ -352,6 +532,10 @@ static void unix_mprotect_hint(int err) { #endif } + + + + int _mi_prim_commit(void* start, size_t size, bool* is_zero) { // commit: ensure we can access the area // note: we may think that *is_zero can be true since the memory @@ -367,43 +551,66 @@ int _mi_prim_commit(void* start, size_t size, bool* is_zero) { return err; } +int _mi_prim_reuse(void* start, size_t size) { + MI_UNUSED(start); MI_UNUSED(size); + #if defined(__APPLE__) && defined(MADV_FREE_REUSE) + return unix_madvise(start, size, MADV_FREE_REUSE); + #endif + return 0; +} + int _mi_prim_decommit(void* start, size_t size, bool* needs_recommit) { int err = 0; - // decommit: use MADV_DONTNEED as it decreases rss immediately (unlike MADV_FREE) - err = unix_madvise(start, size, MADV_DONTNEED); - #if !MI_DEBUG && !MI_SECURE - *needs_recommit = false; + #if 1 + #if defined(__APPLE__) && defined(MADV_FREE_REUSABLE) + // decommit on macOS: use MADV_FREE_REUSABLE as it does immediate rss accounting (issue #1097) + err = unix_madvise(start, size, MADV_FREE_REUSABLE); + if (err) { err = unix_madvise(start, size, MADV_DONTNEED); } + #else + // decommit: use MADV_DONTNEED as it decreases rss immediately (unlike MADV_FREE) + err = unix_madvise(start, size, MADV_DONTNEED); + #endif + #if !MI_DEBUG && MI_SECURE<=2 + *needs_recommit = false; + #else + *needs_recommit = true; + mprotect(start, size, PROT_NONE); + #endif #else + // decommit: use mmap with MAP_FIXED and PROT_NONE to discard the existing memory (and reduce rss) *needs_recommit = true; - mprotect(start, size, PROT_NONE); + const int fd = unix_mmap_fd(); + void* p = mmap(start, size, PROT_NONE, (MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE), fd, 0); + if (p != start) { err = errno; } #endif - /* - // decommit: use mmap with MAP_FIXED and PROT_NONE to discard the existing memory (and reduce rss) - *needs_recommit = true; - const int fd = unix_mmap_fd(); - void* p = mmap(start, size, PROT_NONE, (MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE), fd, 0); - if (p != start) { err = errno; } - */ return err; } int _mi_prim_reset(void* start, size_t size) { - // We try to use `MADV_FREE` as that is the fastest. A drawback though is that it + int err = 0; + + // on macOS can use MADV_FREE_REUSABLE (but we disable this for now as it seems slower) + #if 0 && defined(__APPLE__) && defined(MADV_FREE_REUSABLE) + err = unix_madvise(start, size, MADV_FREE_REUSABLE); + if (err==0) return 0; + // fall through + #endif + + #if defined(MADV_FREE) + // Otherwise, we try to use `MADV_FREE` as that is the fastest. A drawback though is that it // will not reduce the `rss` stats in tools like `top` even though the memory is available // to other processes. With the default `MIMALLOC_PURGE_DECOMMITS=1` we ensure that by // default `MADV_DONTNEED` is used though. - #if defined(MADV_FREE) static _Atomic(size_t) advice = MI_ATOMIC_VAR_INIT(MADV_FREE); int oadvice = (int)mi_atomic_load_relaxed(&advice); - int err; - while ((err = unix_madvise(start, size, oadvice)) != 0 && errno == EAGAIN) { errno = 0; }; - if (err != 0 && errno == EINVAL && oadvice == MADV_FREE) { + while ((err = unix_madvise(start, size, oadvice)) != 0 && err == EAGAIN) { /* try again */ }; + if (err == EINVAL && oadvice == MADV_FREE) { // if MADV_FREE is not supported, fall back to MADV_DONTNEED from now on mi_atomic_store_release(&advice, (size_t)MADV_DONTNEED); err = unix_madvise(start, size, MADV_DONTNEED); } #else - int err = unix_madvise(start, size, MADV_DONTNEED); + err = unix_madvise(start, size, MADV_DONTNEED); #endif return err; } @@ -421,7 +628,7 @@ int _mi_prim_protect(void* start, size_t size, bool protect) { // Huge page allocation //--------------------------------------------- -#if (MI_INTPTR_SIZE >= 8) && !defined(__HAIKU__) && !defined(__CYGWIN__) +#if (MI_SIZE_SIZE >= 8) && !defined(__HAIKU__) && !defined(__CYGWIN__) #ifndef MPOL_PREFERRED #define MPOL_PREFERRED 1 @@ -441,16 +648,16 @@ static long mi_prim_mbind(void* start, unsigned long len, unsigned long mode, co int _mi_prim_alloc_huge_os_pages(void* hint_addr, size_t size, int numa_node, bool* is_zero, void** addr) { bool is_large = true; *is_zero = true; - *addr = unix_mmap(hint_addr, size, MI_SEGMENT_SIZE, PROT_READ | PROT_WRITE, true, true, &is_large); - if (*addr != NULL && numa_node >= 0 && numa_node < 8*MI_INTPTR_SIZE) { // at most 64 nodes + *addr = unix_mmap(hint_addr, size, MI_ARENA_SLICE_ALIGN, PROT_READ | PROT_WRITE, true, true, &is_large); + if (*addr != NULL && numa_node >= 0 && numa_node < (8*MI_SIZE_SIZE - 1)) { // at most 63 nodes unsigned long numa_mask = (1UL << numa_node); - // TODO: does `mbind` work correctly for huge OS pages? should we + // todo: does `mbind` work correctly for huge OS pages? should we // use `set_mempolicy` before calling mmap instead? // see: - long err = mi_prim_mbind(*addr, size, MPOL_PREFERRED, &numa_mask, 8*MI_INTPTR_SIZE, 0); + long err = mi_prim_mbind(*addr, size, MPOL_PREFERRED, &numa_mask, 8*MI_SIZE_SIZE, 0); if (err != 0) { err = errno; - _mi_warning_message("failed to bind huge (1GiB) pages to numa node %d (error: %d (0x%x))\n", numa_node, err, err); + _mi_warning_message("failed to bind huge (1GiB) pages to numa node %d (error: %ld (0x%lx))\n", numa_node, err, err); } } return (*addr != NULL ? 0 : errno); @@ -473,13 +680,11 @@ int _mi_prim_alloc_huge_os_pages(void* hint_addr, size_t size, int numa_node, bo #if defined(__linux__) -#include // snprintf - size_t _mi_prim_numa_node(void) { #if defined(MI_HAS_SYSCALL_H) && defined(SYS_getcpu) - unsigned long node = 0; - unsigned long ncpu = 0; - long err = syscall(SYS_getcpu, &ncpu, &node, NULL); + unsigned int node = 0; + unsigned int ncpu = 0; + int err = syscall(SYS_getcpu, &ncpu, &node, NULL); if (err != 0) return 0; return node; #else @@ -489,13 +694,16 @@ size_t _mi_prim_numa_node(void) { size_t _mi_prim_numa_node_count(void) { char buf[128]; - unsigned node = 0; - for(node = 0; node < 256; node++) { + unsigned last_found = 0; + for(unsigned node = 1; node < 256; node++) { // enumerate node entries -- todo: it there a more efficient way to do this? (but ensure there is no allocation) - snprintf(buf, 127, "/sys/devices/system/node/node%u", node + 1); - if (mi_prim_access(buf,R_OK) != 0) break; + _mi_snprintf(buf, 127, "/sys/devices/system/node/node%u", node); + if (mi_prim_access(buf,R_OK) != 0) { + if (node - last_found > 4) break; // allow some sparseness of nodes but not more than 4 + } + else { last_found = node; } // highest found node } - return (node+1); + return last_found + 1; } #elif defined(__FreeBSD__) && __FreeBSD_version >= 1200000 @@ -521,7 +729,7 @@ size_t _mi_prim_numa_node_count(void) { #elif defined(__DragonFly__) size_t _mi_prim_numa_node(void) { - // TODO: DragonFly does not seem to provide any userland means to get this information. + // todo: DragonFly does not seem to provide any userland means to get this information. return 0ul; } @@ -551,35 +759,39 @@ size_t _mi_prim_numa_node_count(void) { #include -#if defined(CLOCK_REALTIME) || defined(CLOCK_MONOTONIC) - -mi_msecs_t _mi_prim_clock_now(void) { - struct timespec t; - #ifdef CLOCK_MONOTONIC - clock_gettime(CLOCK_MONOTONIC, &t); +// low resolution timer +static mi_msecs_t mi_prim_clock_now_lowres(void) { + const int64_t ticks = (int64_t)clock(); + #if !defined(CLOCKS_PER_SEC) + return ticks; #else - clock_gettime(CLOCK_REALTIME, &t); + if (CLOCKS_PER_SEC <= 0 || CLOCKS_PER_SEC == 1000) { + return ticks; + } + else if (CLOCKS_PER_SEC > 0 && CLOCKS_PER_SEC < 1000) { + return ticks * (1000 / (mi_msecs_t)CLOCKS_PER_SEC); + } + else { + return ticks / ((mi_msecs_t)CLOCKS_PER_SEC / 1000); + } #endif - return ((mi_msecs_t)t.tv_sec * 1000) + ((mi_msecs_t)t.tv_nsec / 1000000); } -#else - -// low resolution timer mi_msecs_t _mi_prim_clock_now(void) { - #if !defined(CLOCKS_PER_SEC) || (CLOCKS_PER_SEC == 1000) || (CLOCKS_PER_SEC == 0) - return (mi_msecs_t)clock(); - #elif (CLOCKS_PER_SEC < 1000) - return (mi_msecs_t)clock() * (1000 / (mi_msecs_t)CLOCKS_PER_SEC); - #else - return (mi_msecs_t)clock() / ((mi_msecs_t)CLOCKS_PER_SEC / 1000); + #if defined(CLOCK_REALTIME) || defined(CLOCK_MONOTONIC) + #ifdef CLOCK_MONOTONIC + const clockid_t clockid = CLOCK_MONOTONIC; + #else + const clockid_t clockid = CLOCK_REALTIME; + #endif + struct timespec t; + if (clock_gettime(clockid,&t) == 0) { + return ((mi_msecs_t)t.tv_sec * 1000) + ((mi_msecs_t)t.tv_nsec / 1000000L); + } #endif + return mi_prim_clock_now_lowres(); } -#endif - - - //---------------------------------------------------------------- // Process info @@ -587,7 +799,6 @@ mi_msecs_t _mi_prim_clock_now(void) { #if defined(__unix__) || defined(__unix) || defined(unix) || defined(__APPLE__) || defined(__HAIKU__) #include -#include #include #if defined(__APPLE__) @@ -605,41 +816,46 @@ static mi_msecs_t timeval_secs(const struct timeval* tv) { void _mi_prim_process_info(mi_process_info_t* pinfo) { struct rusage rusage; - getrusage(RUSAGE_SELF, &rusage); - pinfo->utime = timeval_secs(&rusage.ru_utime); - pinfo->stime = timeval_secs(&rusage.ru_stime); -#if !defined(__HAIKU__) - pinfo->page_faults = rusage.ru_majflt; -#endif -#if defined(__HAIKU__) - // Haiku does not have (yet?) a way to - // get these stats per process - thread_info tid; - area_info mem; - ssize_t c; - get_thread_info(find_thread(0), &tid); - while (get_next_area_info(tid.team, &c, &mem) == B_OK) { - pinfo->peak_rss += mem.ram_size; - } - pinfo->page_faults = 0; -#elif defined(__APPLE__) - pinfo->peak_rss = rusage.ru_maxrss; // macos reports in bytes - #ifdef MACH_TASK_BASIC_INFO - struct mach_task_basic_info info; - mach_msg_type_number_t infoCount = MACH_TASK_BASIC_INFO_COUNT; - if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &infoCount) == KERN_SUCCESS) { - pinfo->current_rss = (size_t)info.resident_size; - } - #else - struct task_basic_info info; - mach_msg_type_number_t infoCount = TASK_BASIC_INFO_COUNT; - if (task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &infoCount) == KERN_SUCCESS) { - pinfo->current_rss = (size_t)info.resident_size; + if (getrusage(RUSAGE_SELF, &rusage) == 0) { + pinfo->utime = timeval_secs(&rusage.ru_utime); + pinfo->stime = timeval_secs(&rusage.ru_stime); + #if !defined(__HAIKU__) + pinfo->page_faults = rusage.ru_majflt; + #endif + #if defined(__APPLE__) + pinfo->peak_rss = rusage.ru_maxrss; // macos reports in bytes + #else + pinfo->peak_rss = rusage.ru_maxrss * 1024; // Linux/BSD report in KiB + #endif } + + #if defined(__HAIKU__) + // Haiku does not have (yet?) a way to + // get these stats per process + thread_info tid; + if (get_thread_info(find_thread(0), &tid) == B_OK) { + area_info mem; + ssize_t c; + while (get_next_area_info(tid.team, &c, &mem) == B_OK) { + pinfo->peak_rss += mem.ram_size; + } + } + pinfo->page_faults = 0; + #elif defined(__APPLE__) + #ifdef MACH_TASK_BASIC_INFO + struct mach_task_basic_info info; + mach_msg_type_number_t infoCount = MACH_TASK_BASIC_INFO_COUNT; + if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &infoCount) == KERN_SUCCESS) { + pinfo->current_rss = (size_t)info.resident_size; + } + #else + struct task_basic_info info; + mach_msg_type_number_t infoCount = TASK_BASIC_INFO_COUNT; + if (task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &infoCount) == KERN_SUCCESS) { + pinfo->current_rss = (size_t)info.resident_size; + } + #endif #endif -#else - pinfo->peak_rss = rusage.ru_maxrss * 1024; // Linux/BSD report in KiB -#endif // use defaults for commit } @@ -686,28 +902,28 @@ static char** mi_get_environ(void) { return environ; } #endif -bool _mi_prim_getenv(const char* name, char* result, size_t result_size) { - if (name==NULL) return false; +int _mi_prim_getenv(const char* name, char* result, size_t result_size) { + if (name==NULL) return -1; const size_t len = _mi_strlen(name); - if (len == 0) return false; + if (len == 0) return -1; char** env = mi_get_environ(); - if (env == NULL) return false; + if (env == NULL) return -1; // compare up to 10000 entries for (int i = 0; i < 10000 && env[i] != NULL; i++) { const char* s = env[i]; if (_mi_strnicmp(name, s, len) == 0 && s[len] == '=') { // case insensitive // found it - _mi_strlcpy(result, s + len + 1, result_size); - return true; + if (!_mi_strlcpy(result, s + len + 1, result_size)) return -1; + return 1; // success } } - return false; + return 0; // not found } #else // fallback: use standard C `getenv` but this cannot be used while initializing the C runtime -bool _mi_prim_getenv(const char* name, char* result, size_t result_size) { +int _mi_prim_getenv(const char* name, char* result, size_t result_size) { // cannot call getenv() when still initializing the C runtime. - if (_mi_preloading()) return false; + if (_mi_preloading()) return -1; // error, try again later const char* s = getenv(name); if (s == NULL) { // we check the upper case name too. @@ -719,9 +935,9 @@ bool _mi_prim_getenv(const char* name, char* result, size_t result_size) { buf[len] = 0; s = getenv(buf); } - if (s == NULL || _mi_strnlen(s,result_size) >= result_size) return false; - _mi_strlcpy(result, s, result_size); - return true; + if (s == NULL || _mi_strnlen(s,result_size) >= result_size) return 0; // not found + if (!_mi_strlcpy(result, s, result_size)) return -1; + return 1; // success } #endif // !MI_USE_ENVIRON @@ -730,40 +946,30 @@ bool _mi_prim_getenv(const char* name, char* result, size_t result_size) { // Random //---------------------------------------------------------------- -#if defined(__APPLE__) - -#include -#if defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_10 +#if defined(__APPLE__) && defined(MAC_OS_X_VERSION_10_15) && (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_15) #include #include -#endif + bool _mi_prim_random_buf(void* buf, size_t buf_len) { - #if defined(MAC_OS_X_VERSION_10_15) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_15 - // We prefer CCRandomGenerateBytes as it returns an error code while arc4random_buf - // may fail silently on macOS. See PR #390, and - return (CCRandomGenerateBytes(buf, buf_len) == kCCSuccess); - #else - // fall back on older macOS - arc4random_buf(buf, buf_len); - return true; - #endif + // We prefer CCRandomGenerateBytes as it returns an error code while arc4random_buf + // may fail silently on macOS. See PR #390, and + return (CCRandomGenerateBytes(buf, buf_len) == kCCSuccess); } #elif defined(__ANDROID__) || defined(__DragonFly__) || \ defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || \ - defined(__sun) || defined(__CYGWIN__) + defined(__sun) || \ + (defined(__APPLE__) && (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_7)) -#include bool _mi_prim_random_buf(void* buf, size_t buf_len) { arc4random_buf(buf, buf_len); return true; } -#elif defined(__linux__) || defined(__HAIKU__) +#elif defined(__APPLE__) || defined(__linux__) || defined(__HAIKU__) || defined(__CYGWIN__) // also for old apple versions < 10.7 (issue #829) #include #include -#include #include bool _mi_prim_random_buf(void* buf, size_t buf_len) { @@ -778,6 +984,7 @@ bool _mi_prim_random_buf(void* buf, size_t buf_len) { static _Atomic(uintptr_t) no_getrandom; // = 0 if (mi_atomic_load_acquire(&no_getrandom)==0) { ssize_t ret = syscall(SYS_getrandom, buf, buf_len, GRND_NONBLOCK); + if (ret > 0) { mi_msan_unpoison(buf, (size_t)ret); } if (ret >= 0) return (buf_len == (size_t)ret); if (errno != ENOSYS) return false; mi_atomic_store_release(&no_getrandom, (uintptr_t)1); // don't call again, and fall back to /dev/urandom @@ -792,7 +999,10 @@ bool _mi_prim_random_buf(void* buf, size_t buf_len) { size_t count = 0; while(count < buf_len) { ssize_t ret = mi_prim_read(fd, (char*)buf + count, buf_len - count); - if (ret<=0) { + if (ret==0) { + break; + } + else if (ret<0) { if (errno!=EAGAIN && errno!=EINTR) break; } else { @@ -819,30 +1029,35 @@ bool _mi_prim_random_buf(void* buf, size_t buf_len) { #if defined(MI_USE_PTHREADS) // use pthread local storage keys to detect thread ending -// (and used with MI_TLS_PTHREADS for the default heap) -pthread_key_t _mi_heap_default_key = (pthread_key_t)(-1); +// (and used with MI_TLS_PTHREADS for the default theap) +pthread_key_t _mi_heap_default_key = MI_PTHREAD_KEY_INVALID; static void mi_pthread_done(void* value) { if (value!=NULL) { - _mi_thread_done((mi_heap_t*)value); + _mi_thread_done((mi_theap_t*)value); } } void _mi_prim_thread_init_auto_done(void) { - mi_assert_internal(_mi_heap_default_key == (pthread_key_t)(-1)); + mi_assert_internal(_mi_heap_default_key == MI_PTHREAD_KEY_INVALID); pthread_key_create(&_mi_heap_default_key, &mi_pthread_done); } void _mi_prim_thread_done_auto_done(void) { - // nothing to do + pthread_key_t key = _mi_heap_default_key; + if (key != MI_PTHREAD_KEY_INVALID) { // do not leak the key, see issue #809 + _mi_heap_default_key = MI_PTHREAD_KEY_INVALID; + pthread_key_delete(key); + } } -void _mi_prim_thread_associate_default_heap(mi_heap_t* heap) { - if (_mi_heap_default_key != (pthread_key_t)(-1)) { // can happen during recursive invocation on freeBSD - pthread_setspecific(_mi_heap_default_key, heap); +void _mi_prim_thread_associate_default_theap(mi_theap_t* theap) { + if (_mi_heap_default_key != MI_PTHREAD_KEY_INVALID) { // can happen during recursive invocation on freeBSD + pthread_setspecific(_mi_heap_default_key, theap); } } + #else void _mi_prim_thread_init_auto_done(void) { @@ -853,8 +1068,20 @@ void _mi_prim_thread_done_auto_done(void) { // nothing } -void _mi_prim_thread_associate_default_heap(mi_heap_t* heap) { - MI_UNUSED(heap); +void _mi_prim_thread_associate_default_theap(mi_theap_t* theap) { + MI_UNUSED(theap); } #endif + +bool _mi_prim_thread_is_in_threadpool(void) { + return false; +} + +void _mi_prim_thread_yield(void) { + sleep(0); +} + +bool _mi_prim_process_is_killed(void) { + return false; // todo: can we detect when exit_group has been called and threads have been killed (potentially holding locks?) +} diff --git a/Objects/mimalloc/prim/wasi/prim.c b/Objects/mimalloc/prim/wasi/prim.c index 640dac0493f5732..1946cfc43409f9f 100644 --- a/Objects/mimalloc/prim/wasi/prim.c +++ b/Objects/mimalloc/prim/wasi/prim.c @@ -8,10 +8,14 @@ terms of the MIT license. A copy of the license can be found in the file // This file is included in `src/prim/prim.c` #include "mimalloc.h" -#include "mimalloc/internal.h" -#include "mimalloc/atomic.h" #include "mimalloc/prim.h" -#include // sbrk() +#include "mimalloc/prim/os.h" +#include "mimalloc/util/libc.h" +#include "mimalloc/heap/init.h" + +#include // fputs +#include // getenv +#include // sbrk, sleep //--------------------------------------------- // Initialize @@ -21,7 +25,7 @@ void _mi_prim_mem_init( mi_os_mem_config_t* config ) { config->page_size = 64*MI_KiB; // WebAssembly has a fixed page size: 64KiB config->alloc_granularity = 16; config->has_overcommit = false; - config->must_free_whole = true; + config->has_partial_free = false; config->has_virtual_reserve = false; } @@ -31,7 +35,7 @@ void _mi_prim_mem_init( mi_os_mem_config_t* config ) { int _mi_prim_free(void* addr, size_t size ) { MI_UNUSED(addr); MI_UNUSED(size); - // wasi heap cannot be shrunk + // wasi theap cannot be shrunk return 0; } @@ -51,7 +55,7 @@ int _mi_prim_free(void* addr, size_t size ) { } #elif defined(__wasi__) static void* mi_memory_grow( size_t size ) { - size_t base = (size > 0 ? __builtin_wasm_memory_grow(0,_mi_divide_up(size, _mi_os_page_size())) + size_t base = (size > 0 ? __builtin_wasm_memory_grow(0,mi_divide_up(size, _mi_os_page_size())) : __builtin_wasm_memory_size(0)); if (base == SIZE_MAX) return NULL; return (void*)(base * _mi_os_page_size()); @@ -59,7 +63,7 @@ int _mi_prim_free(void* addr, size_t size ) { #endif #if defined(MI_USE_PTHREADS) -static pthread_mutex_t mi_heap_grow_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t mi_theap_grow_mutex = PTHREAD_MUTEX_INITIALIZER; #endif static void* mi_prim_mem_grow(size_t size, size_t try_alignment) { @@ -67,11 +71,11 @@ static void* mi_prim_mem_grow(size_t size, size_t try_alignment) { if (try_alignment <= 1) { // `sbrk` is not thread safe in general so try to protect it (we could skip this on WASM but leave it in for now) #if defined(MI_USE_PTHREADS) - pthread_mutex_lock(&mi_heap_grow_mutex); + pthread_mutex_lock(&mi_theap_grow_mutex); #endif p = mi_memory_grow(size); #if defined(MI_USE_PTHREADS) - pthread_mutex_unlock(&mi_heap_grow_mutex); + pthread_mutex_unlock(&mi_theap_grow_mutex); #endif } else { @@ -81,18 +85,18 @@ static void* mi_prim_mem_grow(size_t size, size_t try_alignment) { // between getting the current size and actual allocation // (also, `sbrk` is not thread safe in general) #if defined(MI_USE_PTHREADS) - pthread_mutex_lock(&mi_heap_grow_mutex); + pthread_mutex_lock(&mi_theap_grow_mutex); #endif { void* current = mi_memory_grow(0); // get current size if (current != NULL) { void* aligned_current = mi_align_up_ptr(current, try_alignment); // and align from there to minimize wasted space - alloc_size = _mi_align_up( ((uint8_t*)aligned_current - (uint8_t*)current) + size, _mi_os_page_size()); + alloc_size = mi_align_up( ((uint8_t*)aligned_current - (uint8_t*)current) + size, _mi_os_page_size()); base = mi_memory_grow(alloc_size); } } #if defined(MI_USE_PTHREADS) - pthread_mutex_unlock(&mi_heap_grow_mutex); + pthread_mutex_unlock(&mi_theap_grow_mutex); #endif if (base != NULL) { p = mi_align_up_ptr(base, try_alignment); @@ -116,8 +120,8 @@ static void* mi_prim_mem_grow(size_t size, size_t try_alignment) { } // Note: the `try_alignment` is just a hint and the returned pointer is not guaranteed to be aligned. -int _mi_prim_alloc(size_t size, size_t try_alignment, bool commit, bool allow_large, bool* is_large, bool* is_zero, void** addr) { - MI_UNUSED(allow_large); MI_UNUSED(commit); +int _mi_prim_alloc(void* hint_addr, size_t size, size_t try_alignment, bool commit, bool allow_large, bool* is_large, bool* is_zero, void** addr) { + MI_UNUSED(allow_large); MI_UNUSED(commit); MI_UNUSED(hint_addr); *is_large = false; *is_zero = false; *addr = mi_prim_mem_grow(size, try_alignment); @@ -146,6 +150,11 @@ int _mi_prim_reset(void* addr, size_t size) { return 0; } +int _mi_prim_reuse(void* addr, size_t size) { + MI_UNUSED(addr); MI_UNUSED(size); + return 0; +} + int _mi_prim_protect(void* addr, size_t size, bool protect) { MI_UNUSED(addr); MI_UNUSED(size); MI_UNUSED(protect); return 0; @@ -178,33 +187,39 @@ size_t _mi_prim_numa_node_count(void) { #include -#if defined(CLOCK_REALTIME) || defined(CLOCK_MONOTONIC) - -mi_msecs_t _mi_prim_clock_now(void) { - struct timespec t; - #ifdef CLOCK_MONOTONIC - clock_gettime(CLOCK_MONOTONIC, &t); +// low resolution timer +static mi_msecs_t mi_prim_clock_now_lowres(void) { + const int64_t ticks = (int64_t)clock(); + #if !defined(CLOCKS_PER_SEC) + return ticks; #else - clock_gettime(CLOCK_REALTIME, &t); + if (CLOCKS_PER_SEC <= 0 || CLOCKS_PER_SEC == 1000) { + return ticks; + } + else if (CLOCKS_PER_SEC > 0 && CLOCKS_PER_SEC < 1000) { + return ticks * (1000 / (mi_msecs_t)CLOCKS_PER_SEC); + } + else { + return ticks / ((mi_msecs_t)CLOCKS_PER_SEC / 1000); + } #endif - return ((mi_msecs_t)t.tv_sec * 1000) + ((mi_msecs_t)t.tv_nsec / 1000000); } -#else - -// low resolution timer mi_msecs_t _mi_prim_clock_now(void) { - #if !defined(CLOCKS_PER_SEC) || (CLOCKS_PER_SEC == 1000) || (CLOCKS_PER_SEC == 0) - return (mi_msecs_t)clock(); - #elif (CLOCKS_PER_SEC < 1000) - return (mi_msecs_t)clock() * (1000 / (mi_msecs_t)CLOCKS_PER_SEC); - #else - return (mi_msecs_t)clock() / ((mi_msecs_t)CLOCKS_PER_SEC / 1000); + #if defined(CLOCK_REALTIME) || defined(CLOCK_MONOTONIC) + #ifdef CLOCK_MONOTONIC + const clockid_t clockid = CLOCK_MONOTONIC; + #else + const clockid_t clockid = CLOCK_REALTIME; + #endif + struct timespec t; + if (clock_gettime(clockid,&t) == 0) { + return ((mi_msecs_t)t.tv_sec * 1000) + ((mi_msecs_t)t.tv_nsec / 1000000L); + } #endif + return mi_prim_clock_now_lowres(); } -#endif - //---------------------------------------------------------------- // Process info @@ -230,9 +245,9 @@ void _mi_prim_out_stderr( const char* msg ) { // Environment //---------------------------------------------------------------- -bool _mi_prim_getenv(const char* name, char* result, size_t result_size) { +int _mi_prim_getenv(const char* name, char* result, size_t result_size) { // cannot call getenv() when still initializing the C runtime. - if (_mi_preloading()) return false; + if (_mi_preloading()) return -1; // error, try again later const char* s = getenv(name); if (s == NULL) { // we check the upper case name too. @@ -244,9 +259,9 @@ bool _mi_prim_getenv(const char* name, char* result, size_t result_size) { buf[len] = 0; s = getenv(buf); } - if (s == NULL || _mi_strnlen(s,result_size) >= result_size) return false; + if (s == NULL || _mi_strnlen(s,result_size) >= result_size) return 0; // not found _mi_strlcpy(result, s, result_size); - return true; + return 1; // found } @@ -271,6 +286,18 @@ void _mi_prim_thread_done_auto_done(void) { // nothing } -void _mi_prim_thread_associate_default_heap(mi_heap_t* heap) { - MI_UNUSED(heap); +void _mi_prim_thread_associate_default_theap(mi_theap_t* theap) { + MI_UNUSED(theap); +} + +bool _mi_prim_thread_is_in_threadpool(void) { + return false; +} + +void _mi_prim_thread_yield(void) { + sleep(0); +} + +bool _mi_prim_process_is_killed(void) { + return false; } diff --git a/Objects/mimalloc/prim/windows/etw.h b/Objects/mimalloc/prim/windows/etw.h index e9ec35fc9ed7ebc..4e0a092a10f4ba9 100644 --- a/Objects/mimalloc/prim/windows/etw.h +++ b/Objects/mimalloc/prim/windows/etw.h @@ -136,7 +136,7 @@ extern "C" { // - MCGEN_EVENTSETINFORMATION // - MCGEN_EVENTWRITETRANSFER // -// If the macro is undefined, the MC implementation will default to the +// If the the macro is undefined, the MC implementation will default to the // corresponding ETW APIs. For example, if the MCGEN_EVENTREGISTER macro is // undefined, the EventRegister[MyProviderName] macro will use EventRegister // in user mode and will use EtwRegister in kernel mode. diff --git a/Objects/mimalloc/prim/windows/prim.c b/Objects/mimalloc/prim/windows/prim.c index 49652f65712abbc..92596fd206828b6 100644 --- a/Objects/mimalloc/prim/windows/prim.c +++ b/Objects/mimalloc/prim/windows/prim.c @@ -8,17 +8,30 @@ terms of the MIT license. A copy of the license can be found in the file // This file is included in `src/prim/prim.c` #include "mimalloc.h" -#include "mimalloc/internal.h" -#include "mimalloc/atomic.h" #include "mimalloc/prim.h" +#include "mimalloc/prim/os.h" +#include "mimalloc/util/libc.h" +#include "mimalloc/util/options.h" +#include "mimalloc/heap/init.h" #include // fputs, stderr -#include // NTSTATUS +#include // atexit +// xbox has no console IO and cannot use LoadLibrary or GetModuleHandle +#if !defined(WINAPI_FAMILY_PARTITION) || WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP | WINAPI_PARTITION_SYSTEM) +#define MI_WIN_DESKTOP 1 +#endif //--------------------------------------------- // Dynamically bind Windows API points for portability //--------------------------------------------- +#if defined(_MSC_VER) +#pragma warning(disable:4996) // don't use GetVersionExW +#endif + +static DWORD win_major_version = 6; +static DWORD win_minor_version = 0; + // We use VirtualAlloc2 for aligned allocation, but it is only supported on Windows 10 and Windows Server 2016. // So, we need to look it up dynamically to run on older systems. (use __stdcall for 32-bit compatibility) // NtAllocateVirtualAllocEx is used for huge OS page allocation (1GiB) @@ -47,32 +60,74 @@ typedef struct MI_MEM_ADDRESS_REQUIREMENTS_S { #define MI_MEM_EXTENDED_PARAMETER_NONPAGED_HUGE 0x00000010 #include -typedef PVOID (__stdcall *PVirtualAlloc2)(HANDLE, PVOID, SIZE_T, ULONG, ULONG, MI_MEM_EXTENDED_PARAMETER*, ULONG); -typedef NTSTATUS (__stdcall *PNtAllocateVirtualMemoryEx)(HANDLE, PVOID*, SIZE_T*, ULONG, ULONG, MI_MEM_EXTENDED_PARAMETER*, ULONG); +typedef PVOID (__stdcall *PVirtualAlloc2)(HANDLE, PVOID, SIZE_T, ULONG, ULONG, MI_MEM_EXTENDED_PARAMETER*, ULONG); +typedef LONG (__stdcall *PNtAllocateVirtualMemoryEx)(HANDLE, PVOID*, SIZE_T*, ULONG, ULONG, MI_MEM_EXTENDED_PARAMETER*, ULONG); // avoid NTSTATUS as it is not defined on xbox (pr #1084) static PVirtualAlloc2 pVirtualAlloc2 = NULL; static PNtAllocateVirtualMemoryEx pNtAllocateVirtualMemoryEx = NULL; -// Similarly, GetNumaProcesorNodeEx is only supported since Windows 7 +// Similarly, GetNumaProcessorNodeEx is only supported since Windows 7 (and GetNumaNodeProcessorMask is not supported on xbox) typedef struct MI_PROCESSOR_NUMBER_S { WORD Group; BYTE Number; BYTE Reserved; } MI_PROCESSOR_NUMBER; typedef VOID (__stdcall *PGetCurrentProcessorNumberEx)(MI_PROCESSOR_NUMBER* ProcNumber); typedef BOOL (__stdcall *PGetNumaProcessorNodeEx)(MI_PROCESSOR_NUMBER* Processor, PUSHORT NodeNumber); typedef BOOL (__stdcall* PGetNumaNodeProcessorMaskEx)(USHORT Node, PGROUP_AFFINITY ProcessorMask); typedef BOOL (__stdcall *PGetNumaProcessorNode)(UCHAR Processor, PUCHAR NodeNumber); +typedef BOOL (__stdcall* PGetNumaNodeProcessorMask)(UCHAR Node, PULONGLONG ProcessorMask); +typedef BOOL (__stdcall* PGetNumaHighestNodeNumber)(PULONG Node); +typedef BOOL (__stdcall* PRtlDllShutdownInProgress)(VOID); static PGetCurrentProcessorNumberEx pGetCurrentProcessorNumberEx = NULL; static PGetNumaProcessorNodeEx pGetNumaProcessorNodeEx = NULL; static PGetNumaNodeProcessorMaskEx pGetNumaNodeProcessorMaskEx = NULL; static PGetNumaProcessorNode pGetNumaProcessorNode = NULL; +static PGetNumaNodeProcessorMask pGetNumaNodeProcessorMask = NULL; +static PGetNumaHighestNodeNumber pGetNumaHighestNodeNumber = NULL; +static PRtlDllShutdownInProgress pRtlDllShutdownInProgress = NULL; + +// Not available on xbox +typedef SIZE_T(__stdcall* PGetLargePageMinimum)(VOID); +static PGetLargePageMinimum pGetLargePageMinimum = NULL; + +// Available after Windows XP +typedef BOOL (__stdcall *PGetPhysicallyInstalledSystemMemory)( PULONGLONG TotalMemoryInKilobytes ); +typedef BOOL (__stdcall* PGetVersionExW)(LPOSVERSIONINFOW lpVersionInformation); + + +// Load a library +static HMODULE mi_win_loadlibrary(const wchar_t* library) { + #if MI_WIN_DESKTOP + return LoadLibraryW(library); + #else + return LoadPackagedLibrary(library, 0); + #endif +} + +// Get a library handle (and possibly load it) +static HMODULE mi_win_getlibrary(const wchar_t* library, bool* should_free) { + #if MI_WIN_DESKTOP + // avoid calling LoadLibrary for "kernel32", "ntdll", and "kernelbase" (also to avoid hitting the loader lock) + HMODULE mod = GetModuleHandleW(library); + if (mod!=NULL) { + *should_free = false; + return mod; + } + #endif + *should_free = true; + return mi_win_loadlibrary(library); +} + +static void mi_win_freelibrary(HMODULE mod, bool should_free) { + if (should_free) { + FreeLibrary(mod); + } +} //--------------------------------------------- // Enable large page support dynamically (if possible) //--------------------------------------------- -static bool win_enable_large_os_pages(size_t* large_page_size) +static bool win_enable_large_os_pages_once(size_t* large_page_size) { - static bool large_initialized = false; - if (large_initialized) return (_mi_os_large_page_size() > 0); - large_initialized = true; + if (pGetLargePageMinimum==NULL) return false; // no large page support (xbox etc.) // Try to see if large OS pages are supported // To use large pages on Windows, we first need access permission @@ -81,69 +136,117 @@ static bool win_enable_large_os_pages(size_t* large_page_size) unsigned long err = 0; HANDLE token = NULL; BOOL ok = OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token); + err = GetLastError(); if (ok) { TOKEN_PRIVILEGES tp; ok = LookupPrivilegeValue(NULL, TEXT("SeLockMemoryPrivilege"), &tp.Privileges[0].Luid); + err = GetLastError(); if (ok) { tp.PrivilegeCount = 1; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; ok = AdjustTokenPrivileges(token, FALSE, &tp, 0, (PTOKEN_PRIVILEGES)NULL, 0); + err = GetLastError(); if (ok) { - err = GetLastError(); ok = (err == ERROR_SUCCESS); - if (ok && large_page_size != NULL) { - *large_page_size = GetLargePageMinimum(); + if (ok && large_page_size != NULL && pGetLargePageMinimum != NULL) { + *large_page_size = (*pGetLargePageMinimum)(); } } } CloseHandle(token); } if (!ok) { - if (err == 0) err = GetLastError(); + if (err == 0) { err = GetLastError(); } _mi_warning_message("cannot enable large OS page support, error %lu\n", err); } return (ok!=0); } +static bool win_enable_large_os_pages(size_t* large_page_size) { + static size_t win_large_page_size = 0; + mi_atomic_do_once { + win_enable_large_os_pages_once(&win_large_page_size); + } + if (large_page_size != NULL) { *large_page_size = win_large_page_size; } + return (win_large_page_size > 0); +} + //--------------------------------------------- // Initialize //--------------------------------------------- +static DWORD win_allocation_granularity = 64*MI_KiB; + void _mi_prim_mem_init( mi_os_mem_config_t* config ) { config->has_overcommit = false; - config->must_free_whole = true; + config->has_partial_free = false; config->has_virtual_reserve = true; + // get the page size - SYSTEM_INFO si; + SYSTEM_INFO si; _mi_memzero_var(si); GetSystemInfo(&si); if (si.dwPageSize > 0) { config->page_size = si.dwPageSize; } - if (si.dwAllocationGranularity > 0) { config->alloc_granularity = si.dwAllocationGranularity; } + if (si.dwAllocationGranularity > 0) { + config->alloc_granularity = si.dwAllocationGranularity; + win_allocation_granularity = si.dwAllocationGranularity; + } + // get virtual address bits + if ((uintptr_t)si.lpMaximumApplicationAddress > 0) { + const size_t vbits = MI_SIZE_BITS - mi_clz((uintptr_t)si.lpMaximumApplicationAddress); + config->virtual_address_bits = vbits; + } + // get the VirtualAlloc2 function - HINSTANCE hDll; - hDll = LoadLibrary(TEXT("kernelbase.dll")); + bool hDllFree; + HINSTANCE hDll = mi_win_getlibrary(L"kernelbase.dll", &hDllFree); if (hDll != NULL) { // use VirtualAlloc2FromApp if possible as it is available to Windows store apps pVirtualAlloc2 = (PVirtualAlloc2)(void (*)(void))GetProcAddress(hDll, "VirtualAlloc2FromApp"); if (pVirtualAlloc2==NULL) pVirtualAlloc2 = (PVirtualAlloc2)(void (*)(void))GetProcAddress(hDll, "VirtualAlloc2"); - FreeLibrary(hDll); + mi_win_freelibrary(hDll, hDllFree); } // NtAllocateVirtualMemoryEx is used for huge page allocation - hDll = LoadLibrary(TEXT("ntdll.dll")); + hDll = mi_win_getlibrary(L"ntdll.dll", &hDllFree); if (hDll != NULL) { pNtAllocateVirtualMemoryEx = (PNtAllocateVirtualMemoryEx)(void (*)(void))GetProcAddress(hDll, "NtAllocateVirtualMemoryEx"); - FreeLibrary(hDll); + pRtlDllShutdownInProgress = (PRtlDllShutdownInProgress)(void (*)(void))GetProcAddress(hDll, "RtlDllShutdownInProgress"); + mi_win_freelibrary(hDll, hDllFree); } // Try to use Win7+ numa API - hDll = LoadLibrary(TEXT("kernel32.dll")); + hDll = mi_win_getlibrary(L"kernel32.dll", &hDllFree); if (hDll != NULL) { pGetCurrentProcessorNumberEx = (PGetCurrentProcessorNumberEx)(void (*)(void))GetProcAddress(hDll, "GetCurrentProcessorNumberEx"); pGetNumaProcessorNodeEx = (PGetNumaProcessorNodeEx)(void (*)(void))GetProcAddress(hDll, "GetNumaProcessorNodeEx"); pGetNumaNodeProcessorMaskEx = (PGetNumaNodeProcessorMaskEx)(void (*)(void))GetProcAddress(hDll, "GetNumaNodeProcessorMaskEx"); pGetNumaProcessorNode = (PGetNumaProcessorNode)(void (*)(void))GetProcAddress(hDll, "GetNumaProcessorNode"); - FreeLibrary(hDll); + pGetNumaNodeProcessorMask = (PGetNumaNodeProcessorMask)(void (*)(void))GetProcAddress(hDll, "GetNumaNodeProcessorMask"); + pGetNumaHighestNodeNumber = (PGetNumaHighestNodeNumber)(void (*)(void))GetProcAddress(hDll, "GetNumaHighestNodeNumber"); + pGetLargePageMinimum = (PGetLargePageMinimum)(void (*)(void))GetProcAddress(hDll, "GetLargePageMinimum"); + // Get physical memory (not available on XP, so check dynamically) + PGetPhysicallyInstalledSystemMemory pGetPhysicallyInstalledSystemMemory = (PGetPhysicallyInstalledSystemMemory)(void (*)(void))GetProcAddress(hDll,"GetPhysicallyInstalledSystemMemory"); + if (pGetPhysicallyInstalledSystemMemory != NULL) { + ULONGLONG memInKiB = 0; + if ((*pGetPhysicallyInstalledSystemMemory)(&memInKiB)) { + if (memInKiB > 0 && memInKiB <= SIZE_MAX) { + config->physical_memory_in_kib = (size_t)memInKiB; + } + } + } + // Get Windows version + PGetVersionExW pGetVersionExW = (PGetVersionExW)(void (*)(void))GetProcAddress(hDll, "GetVersionExW"); + if (pGetVersionExW != NULL) { + OSVERSIONINFOW version; _mi_memzero_var(version); + version.dwOSVersionInfoSize = sizeof(version); + if ((*pGetVersionExW)(&version)) { + win_major_version = version.dwMajorVersion; + win_minor_version = version.dwMinorVersion; + } + } + mi_win_freelibrary(hDll, hDllFree); } + // Enable large/huge OS page support? if (mi_option_is_enabled(mi_option_allow_large_os_pages) || mi_option_is_enabled(mi_option_reserve_huge_os_pages)) { win_enable_large_os_pages(&config->large_page_size); } @@ -163,9 +266,10 @@ int _mi_prim_free(void* addr, size_t size ) { // In mi_os_mem_alloc_aligned the fallback path may have returned a pointer inside // the memory region returned by VirtualAlloc; in that case we need to free using // the start of the region. - MEMORY_BASIC_INFORMATION info = { 0 }; - VirtualQuery(addr, &info, sizeof(info)); - if (info.AllocationBase < addr && ((uint8_t*)addr - (uint8_t*)info.AllocationBase) < (ptrdiff_t)MI_SEGMENT_SIZE) { + MEMORY_BASIC_INFORMATION info; _mi_memzero_var(info); + err = (VirtualQuery(addr, &info, sizeof(info)) == 0); + if (err) { errcode = GetLastError(); } + if (!err && info.AllocationBase < addr && ((uint8_t*)addr - (uint8_t*)info.AllocationBase) < (ptrdiff_t)(4*MI_MiB)) { errcode = 0; err = (VirtualFree(info.AllocationBase, 0, MEM_RELEASE) == 0); if (err) { errcode = GetLastError(); } @@ -179,8 +283,8 @@ int _mi_prim_free(void* addr, size_t size ) { // VirtualAlloc //--------------------------------------------- -static void* win_virtual_alloc_prim(void* addr, size_t size, size_t try_alignment, DWORD flags) { - #if (MI_INTPTR_SIZE >= 8) +static void* win_virtual_alloc_prim_once(void* addr, size_t size, size_t try_alignment, DWORD flags) { + #if (MI_SIZE_SIZE >= 8) // on 64-bit systems, try to use the virtual address area after 2TiB for 4MiB aligned allocations if (addr == NULL) { void* hint = _mi_os_get_aligned_hint(try_alignment,size); @@ -193,7 +297,7 @@ static void* win_virtual_alloc_prim(void* addr, size_t size, size_t try_alignmen } #endif // on modern Windows try use VirtualAlloc2 for aligned allocation - if (try_alignment > 1 && (try_alignment % _mi_os_page_size()) == 0 && pVirtualAlloc2 != NULL) { + if (addr == NULL && try_alignment > win_allocation_granularity && (try_alignment % _mi_os_page_size()) == 0 && pVirtualAlloc2 != NULL) { MI_MEM_ADDRESS_REQUIREMENTS reqs = { 0, 0, 0 }; reqs.Alignment = try_alignment; MI_MEM_EXTENDED_PARAMETER param = { {0, 0}, {0} }; @@ -201,20 +305,61 @@ static void* win_virtual_alloc_prim(void* addr, size_t size, size_t try_alignmen param.Arg.Pointer = &reqs; void* p = (*pVirtualAlloc2)(GetCurrentProcess(), addr, size, flags, PAGE_READWRITE, ¶m, 1); if (p != NULL) return p; - _mi_warning_message("unable to allocate aligned OS memory (%zu bytes, error code: 0x%x, address: %p, alignment: %zu, flags: 0x%x)\n", size, GetLastError(), addr, try_alignment, flags); + _mi_warning_message("unable to allocate aligned OS memory (0x%zx bytes, error code: 0x%x, address: %p, alignment: 0x%zx, flags: 0x%x)\n", size, GetLastError(), addr, try_alignment, flags); // fall through on error } // last resort return VirtualAlloc(addr, size, flags, PAGE_READWRITE); } +static bool win_is_out_of_memory_error(DWORD err) { + switch (err) { + case ERROR_COMMITMENT_MINIMUM: + case ERROR_COMMITMENT_LIMIT: + case ERROR_PAGEFILE_QUOTA: + case ERROR_NOT_ENOUGH_MEMORY: + return true; + default: + return false; + } +} + +static void* win_virtual_alloc_prim(void* addr, size_t size, size_t try_alignment, DWORD flags) { + long max_retry_msecs = mi_option_get_clamp(mi_option_retry_on_oom, 0, 2000); // at most 2 seconds + if (max_retry_msecs == 1) { max_retry_msecs = 100; } // if one sets the option to "true" + for (long tries = 1; tries <= 10; tries++) { // try at most 10 times (=2200ms) + void* p = win_virtual_alloc_prim_once(addr, size, try_alignment, flags); + if (p != NULL) { + // success, return the address + return p; + } + else if (max_retry_msecs > 0 && (try_alignment <= 8*MI_MiB) && + (flags&MEM_COMMIT) != 0 && (flags&MEM_LARGE_PAGES) == 0 && + win_is_out_of_memory_error(GetLastError())) { + // if committing regular memory and being out-of-memory, + // keep trying for a bit in case memory frees up after all. See issue #894 + _mi_warning_message("out-of-memory on OS allocation, try again... (attempt %lu, 0x%zx bytes, error code: 0x%x, address: %p, alignment: 0x%zx, flags: 0x%x)\n", tries, size, GetLastError(), addr, try_alignment, flags); + long sleep_msecs = tries*40; // increasing waits + if (sleep_msecs > max_retry_msecs) { sleep_msecs = max_retry_msecs; } + max_retry_msecs -= sleep_msecs; + Sleep(sleep_msecs); + } + else { + // otherwise return with an error + break; + } + } + return NULL; +} + static void* win_virtual_alloc(void* addr, size_t size, size_t try_alignment, DWORD flags, bool large_only, bool allow_large, bool* is_large) { mi_assert_internal(!(large_only && !allow_large)); static _Atomic(size_t) large_page_try_ok; // = 0; void* p = NULL; // Try to allocate large OS pages (2MiB) if allowed or required. - if ((large_only || _mi_os_use_large_page(size, try_alignment)) - && allow_large && (flags&MEM_COMMIT)!=0 && (flags&MEM_RESERVE)!=0) { + if ((large_only || (_mi_os_canuse_large_page(size, try_alignment) && mi_option_is_enabled(mi_option_allow_large_os_pages))) + && allow_large && (flags&MEM_COMMIT)!=0 && (flags&MEM_RESERVE)!=0) + { size_t try_ok = mi_atomic_load_acquire(&large_page_try_ok); if (!large_only && try_ok > 0) { // if a large page allocation fails, it seems the calls to VirtualAlloc get very expensive. @@ -241,14 +386,14 @@ static void* win_virtual_alloc(void* addr, size_t size, size_t try_alignment, DW return p; } -int _mi_prim_alloc(size_t size, size_t try_alignment, bool commit, bool allow_large, bool* is_large, bool* is_zero, void** addr) { +int _mi_prim_alloc(void* hint_addr, size_t size, size_t try_alignment, bool commit, bool allow_large, bool* is_large, bool* is_zero, void** addr) { mi_assert_internal(size > 0 && (size % _mi_os_page_size()) == 0); mi_assert_internal(commit || !allow_large); mi_assert_internal(try_alignment > 0); *is_zero = true; int flags = MEM_RESERVE; if (commit) { flags |= MEM_COMMIT; } - *addr = win_virtual_alloc(NULL, size, try_alignment, flags, false, allow_large, is_large); + *addr = win_virtual_alloc(hint_addr, size, try_alignment, flags, false, allow_large, is_large); return (*addr != NULL ? 0 : (int)GetLastError()); } @@ -294,9 +439,19 @@ int _mi_prim_reset(void* addr, size_t size) { return (p != NULL ? 0 : (int)GetLastError()); } +int _mi_prim_reuse(void* addr, size_t size) { + MI_UNUSED(addr); MI_UNUSED(size); + return 0; +} + int _mi_prim_protect(void* addr, size_t size, bool protect) { + #if !MI_WIN_DESKTOP && WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP) + ULONG oldprotect = 0; + BOOL ok = VirtualProtectFromApp(addr, size, protect ? PAGE_NOACCESS : PAGE_READWRITE, &oldprotect); + #else DWORD oldprotect = 0; BOOL ok = VirtualProtect(addr, size, protect ? PAGE_NOACCESS : PAGE_READWRITE, &oldprotect); + #endif return (ok ? 0 : (int)GetLastError()); } @@ -313,8 +468,8 @@ static void* _mi_prim_alloc_huge_os_pagesx(void* hint_addr, size_t size, int num MI_MEM_EXTENDED_PARAMETER params[3] = { {{0,0},{0}},{{0,0},{0}},{{0,0},{0}} }; // on modern Windows try use NtAllocateVirtualMemoryEx for 1GiB huge pages - static bool mi_huge_pages_available = true; - if (pNtAllocateVirtualMemoryEx != NULL && mi_huge_pages_available) { + static _Atomic(size_t) mi_huge_pages_available = MI_ATOMIC_VAR_INIT(1); + if (pNtAllocateVirtualMemoryEx != NULL && mi_atomic_load_acquire(&mi_huge_pages_available) != 0) { params[0].Type.Type = MiMemExtendedParameterAttributeFlags; params[0].Arg.ULong64 = MI_MEM_EXTENDED_PARAMETER_NONPAGED_HUGE; ULONG param_count = 1; @@ -325,13 +480,13 @@ static void* _mi_prim_alloc_huge_os_pagesx(void* hint_addr, size_t size, int num } SIZE_T psize = size; void* base = hint_addr; - NTSTATUS err = (*pNtAllocateVirtualMemoryEx)(GetCurrentProcess(), &base, &psize, flags, PAGE_READWRITE, params, param_count); + LONG err = (*pNtAllocateVirtualMemoryEx)(GetCurrentProcess(), &base, &psize, flags, PAGE_READWRITE, params, param_count); if (err == 0 && base != NULL) { return base; } else { // fall back to regular large pages - mi_huge_pages_available = false; // don't try further huge pages + mi_atomic_store_release(&mi_huge_pages_available,0); // don't try further huge pages _mi_warning_message("unable to allocate using huge (1GiB) pages, trying large (2MiB) pages instead (status 0x%lx)\n", err); } } @@ -378,11 +533,12 @@ size_t _mi_prim_numa_node(void) { } size_t _mi_prim_numa_node_count(void) { -#ifdef MS_WINDOWS_DESKTOP ULONG numa_max = 0; - GetNumaHighestNodeNumber(&numa_max); + if (pGetNumaHighestNodeNumber!=NULL) { + (*pGetNumaHighestNodeNumber)(&numa_max); + } // find the highest node number that has actual processors assigned to it. Issue #282 - while(numa_max > 0) { + while (numa_max > 0) { if (pGetNumaNodeProcessorMaskEx != NULL) { // Extended API is supported GROUP_AFFINITY affinity; @@ -393,17 +549,16 @@ size_t _mi_prim_numa_node_count(void) { else { // Vista or earlier, use older API that is limited to 64 processors. ULONGLONG mask; - if (GetNumaNodeProcessorMask((UCHAR)numa_max, &mask)) { - if (mask != 0) break; // found the maximum non-empty node + if (pGetNumaNodeProcessorMask != NULL) { + if ((*pGetNumaNodeProcessorMask)((UCHAR)numa_max, &mask)) { + if (mask != 0) break; // found the maximum non-empty node + } }; } // max node was invalid or had no processor assigned, try again numa_max--; } return ((size_t)numa_max + 1); -#else - return ((size_t)1); -#endif } @@ -415,8 +570,9 @@ static mi_msecs_t mi_to_msecs(LARGE_INTEGER t) { static LARGE_INTEGER mfreq; // = 0 if (mfreq.QuadPart == 0LL) { LARGE_INTEGER f; - QueryPerformanceFrequency(&f); - mfreq.QuadPart = f.QuadPart/1000LL; + if (QueryPerformanceFrequency(&f)) { + mfreq.QuadPart = f.QuadPart/1000LL; + } if (mfreq.QuadPart == 0) mfreq.QuadPart = 1; } return (mi_msecs_t)(t.QuadPart / mfreq.QuadPart); @@ -424,8 +580,12 @@ static mi_msecs_t mi_to_msecs(LARGE_INTEGER t) { mi_msecs_t _mi_prim_clock_now(void) { LARGE_INTEGER t; - QueryPerformanceCounter(&t); - return mi_to_msecs(t); + if (QueryPerformanceCounter(&t)) { + return mi_to_msecs(t); + } + else { + return 0; + } } @@ -433,7 +593,6 @@ mi_msecs_t _mi_prim_clock_now(void) { // Process Info //---------------------------------------------------------------- -#include #include static mi_msecs_t filetime_msecs(const FILETIME* ftime) { @@ -453,29 +612,31 @@ void _mi_prim_process_info(mi_process_info_t* pinfo) FILETIME ut; FILETIME st; FILETIME et; - GetProcessTimes(GetCurrentProcess(), &ct, &et, &st, &ut); - pinfo->utime = filetime_msecs(&ut); - pinfo->stime = filetime_msecs(&st); + if (GetProcessTimes(GetCurrentProcess(), &ct, &et, &st, &ut)) { + pinfo->utime = filetime_msecs(&ut); + pinfo->stime = filetime_msecs(&st); + } // load psapi on demand - if (pGetProcessMemoryInfo == NULL) { - HINSTANCE hDll = LoadLibrary(TEXT("psapi.dll")); + mi_atomic_do_once{ + HINSTANCE hDll = mi_win_loadlibrary(L"psapi.dll"); if (hDll != NULL) { pGetProcessMemoryInfo = (PGetProcessMemoryInfo)(void (*)(void))GetProcAddress(hDll, "GetProcessMemoryInfo"); + // mi_win_freelibrary(hDll, true); // don't free } } // get process info - PROCESS_MEMORY_COUNTERS info; - memset(&info, 0, sizeof(info)); if (pGetProcessMemoryInfo != NULL) { - pGetProcessMemoryInfo(GetCurrentProcess(), &info, sizeof(info)); + PROCESS_MEMORY_COUNTERS info; _mi_memzero_var(info); + if (pGetProcessMemoryInfo(GetCurrentProcess(), &info, sizeof(info))) { + pinfo->current_rss = (size_t)info.WorkingSetSize; + pinfo->peak_rss = (size_t)info.PeakWorkingSetSize; + pinfo->current_commit = (size_t)info.PagefileUsage; + pinfo->peak_commit = (size_t)info.PeakPagefileUsage; + pinfo->page_faults = (size_t)info.PageFaultCount; + } } - pinfo->current_rss = (size_t)info.WorkingSetSize; - pinfo->peak_rss = (size_t)info.PeakWorkingSetSize; - pinfo->current_commit = (size_t)info.PagefileUsage; - pinfo->peak_commit = (size_t)info.PeakPagefileUsage; - pinfo->page_faults = (size_t)info.PageFaultCount; } //---------------------------------------------------------------- @@ -487,28 +648,40 @@ void _mi_prim_out_stderr( const char* msg ) // on windows with redirection, the C runtime cannot handle locale dependent output // after the main thread closes so we use direct console output. if (!_mi_preloading()) { - // _cputs(msg); // _cputs cannot be used at is aborts if it fails to lock the console static HANDLE hcon = INVALID_HANDLE_VALUE; - static bool hconIsConsole; + #if MI_WIN_DESKTOP + static bool hconIsConsole = false; + #endif if (hcon == INVALID_HANDLE_VALUE) { + hcon = GetStdHandle(STD_ERROR_HANDLE); // returns NULL if no stderr is available + #if MI_WIN_DESKTOP CONSOLE_SCREEN_BUFFER_INFO sbi; - hcon = GetStdHandle(STD_ERROR_HANDLE); - hconIsConsole = ((hcon != INVALID_HANDLE_VALUE) && GetConsoleScreenBufferInfo(hcon, &sbi)); + hconIsConsole = ((hcon != NULL && hcon != INVALID_HANDLE_VALUE) && GetConsoleScreenBufferInfo(hcon, &sbi)); + #endif } const size_t len = _mi_strlen(msg); if (len > 0 && len < UINT32_MAX) { DWORD written = 0; - if (hconIsConsole) { - WriteConsoleA(hcon, msg, (DWORD)len, &written, NULL); - } - else if (hcon != INVALID_HANDLE_VALUE) { - // use direct write if stderr was redirected - WriteFile(hcon, msg, (DWORD)len, &written, NULL); + if (hcon != NULL && hcon != INVALID_HANDLE_VALUE) { + #if MI_WIN_DESKTOP + if (hconIsConsole) { + WriteConsoleA(hcon, msg, (DWORD)len, &written, NULL); + } + else + #endif + { + // use direct write in case stderr was redirected + WriteFile(hcon, msg, (DWORD)len, &written, NULL); + } } - else { - // finally fall back to fputs after all - fputs(msg, stderr); + #if MI_WIN_DESKTOP + else if (hcon == NULL) { + // no stderr handle: this is usually a GUI or detached process. Send the + // output to the debugger instead (see issue #1354 and #1429) + OutputDebugStringA(msg); } + #endif + // don't fall back to fputs or _cputs as the crt can have it locked } } } @@ -522,106 +695,557 @@ void _mi_prim_out_stderr( const char* msg ) // reliably even when this is invoked before the C runtime is initialized. // i.e. when `_mi_preloading() == true`. // Note: on windows, environment names are not case sensitive. -bool _mi_prim_getenv(const char* name, char* result, size_t result_size) { +int _mi_prim_getenv(const char* name, char* result, size_t result_size) { result[0] = 0; - size_t len = GetEnvironmentVariableA(name, result, (DWORD)result_size); - return (len > 0 && len < result_size); + const size_t len = GetEnvironmentVariableA(name, result, (DWORD)result_size); + return (len < result_size ? (len > 0 ? 1 /* success */ : 0 /* not found */) : -1 /* error */); } - //---------------------------------------------------------------- // Random //---------------------------------------------------------------- -#if defined(MI_USE_RTLGENRANDOM) // || defined(__cplusplus) -// We prefer to use BCryptGenRandom instead of (the unofficial) RtlGenRandom but when using -// dynamic overriding, we observed it can raise an exception when compiled with C++, and -// sometimes deadlocks when also running under the VS debugger. -// In contrast, issue #623 implies that on Windows Server 2019 we need to use BCryptGenRandom. -// To be continued.. +#if defined(MI_USE_RTLGENRANDOM) || MI_WIN_DESKTOP +#if defined(_MSC_VER) #pragma comment (lib,"advapi32.lib") +#endif #define RtlGenRandom SystemFunction036 mi_decl_externc BOOLEAN NTAPI RtlGenRandom(PVOID RandomBuffer, ULONG RandomBufferLength); -bool _mi_prim_random_buf(void* buf, size_t buf_len) { +static bool mi_win_rtl_gen_random(void* buf, size_t buf_len) { + mi_assert(buf_len <= ULONG_MAX); + if (buf_len > ULONG_MAX) return false; return (RtlGenRandom(buf, (ULONG)buf_len) != 0); } +#endif -#else +#if defined(MI_USE_RTLGENRANDOM) +bool _mi_prim_random_buf(void* buf, size_t buf_len) { + return mi_win_rtl_gen_random(buf, buf_len); +} +#elif !MI_WIN_DESKTOP +// UWP/Xbox cannot dynamically load system libraries, so we link to BCryptGenRandom directly. +#include +#if defined(_MSC_VER) +#pragma comment (lib,"bcrypt.lib") +#endif #ifndef BCRYPT_USE_SYSTEM_PREFERRED_RNG #define BCRYPT_USE_SYSTEM_PREFERRED_RNG 0x00000002 #endif -typedef LONG (NTAPI *PBCryptGenRandom)(HANDLE, PUCHAR, ULONG, ULONG); -static PBCryptGenRandom pBCryptGenRandom = NULL; +bool _mi_prim_random_buf(void* buf, size_t buf_len) { + mi_assert(buf_len <= ULONG_MAX); + if (buf_len > ULONG_MAX) return false; + return (BCryptGenRandom(NULL, (PUCHAR)buf, (ULONG)buf_len, BCRYPT_USE_SYSTEM_PREFERRED_RNG) >= 0); +} + +#else +// We use the documented ProcessPrng (Windows 8+) which always succeeds, and fall back to RtlGenRandom +// if it is not available. We avoid BCryptGenRandom as on Windows 7 / Server 2008 R2 it seems it can deadlock +// when called while the loader lock is held (as during process attach) (issues #1395 and #1426). +typedef BOOL (WINAPI *PProcessPrng)(PBYTE pbData, SIZE_T cbData); +static PProcessPrng pProcessPrng = NULL; bool _mi_prim_random_buf(void* buf, size_t buf_len) { - if (pBCryptGenRandom == NULL) { - HINSTANCE hDll = LoadLibrary(TEXT("bcrypt.dll")); - if (hDll != NULL) { - pBCryptGenRandom = (PBCryptGenRandom)(void (*)(void))GetProcAddress(hDll, "BCryptGenRandom"); + mi_atomic_do_once { + if (win_major_version > 6 || (win_major_version == 6 && win_minor_version >= 2)) { + bool should_free = false; + HMODULE hDll = mi_win_getlibrary(L"bcryptprimitives.dll", &should_free); + if (hDll != NULL) { + pProcessPrng = (PProcessPrng)(void (*)(void))GetProcAddress(hDll, "ProcessPrng"); + if (pProcessPrng == NULL) { mi_win_freelibrary(hDll, should_free); } // otherwise don't free + } } - if (pBCryptGenRandom == NULL) return false; } - return (pBCryptGenRandom(NULL, (PUCHAR)buf, (ULONG)buf_len, BCRYPT_USE_SYSTEM_PREFERRED_RNG) >= 0); + if (pProcessPrng != NULL) { + return (pProcessPrng((PBYTE)buf, buf_len) != FALSE); + } + return mi_win_rtl_gen_random(buf, buf_len); } #endif // MI_USE_RTLGENRANDOM + //---------------------------------------------------------------- -// Thread init/done +// Thread pool? //---------------------------------------------------------------- -#if !defined(MI_SHARED_LIB) - -// use thread local storage keys to detect thread ending -#include -#if (_WIN32_WINNT < 0x600) // before Windows Vista -WINBASEAPI DWORD WINAPI FlsAlloc( _In_opt_ PFLS_CALLBACK_FUNCTION lpCallback ); -WINBASEAPI PVOID WINAPI FlsGetValue( _In_ DWORD dwFlsIndex ); -WINBASEAPI BOOL WINAPI FlsSetValue( _In_ DWORD dwFlsIndex, _In_opt_ PVOID lpFlsData ); -WINBASEAPI BOOL WINAPI FlsFree(_In_ DWORD dwFlsIndex); +bool _mi_prim_thread_is_in_threadpool(void) { +#if (MI_ARCH_X64 || MI_ARCH_X86 || MI_ARCH_ARM64) + if (win_major_version >= 6) { + // check if this thread belongs to a windows threadpool + // see: + struct _TEB* const teb = NtCurrentTeb(); + void* const pool_data = *((void**)((uint8_t*)teb + (MI_SIZE_BITS == 32 ? 0x0F90 : 0x1778))); + return (pool_data != NULL); + } #endif + return false; +} -static DWORD mi_fls_key = (DWORD)(-1); +void _mi_prim_thread_yield(void) { + SwitchToThread(); +} -static void NTAPI mi_fls_done(PVOID value) { - mi_heap_t* heap = (mi_heap_t*)value; - if (heap != NULL) { - _mi_thread_done(heap); - FlsSetValue(mi_fls_key, NULL); // prevent recursion as _mi_thread_done may set it back to the main heap, issue #672 +bool _mi_prim_process_is_killed(void) { + if (pRtlDllShutdownInProgress != NULL) { + return (*pRtlDllShutdownInProgress)(); + } + else { + return false; } } -void _mi_prim_thread_init_auto_done(void) { - mi_fls_key = FlsAlloc(&mi_fls_done); -} +//---------------------------------------------------------------- +// Process & Thread Init/Done +//---------------------------------------------------------------- -void _mi_prim_thread_done_auto_done(void) { - // call thread-done on all threads (except the main thread) to prevent - // dangling callback pointer if statically linked with a DLL; Issue #208 - FlsFree(mi_fls_key); -} +//static void mi_debug_out(const char* s) { +// HANDLE h = GetStdHandle(STD_ERROR_HANDLE); +// WriteConsole(h, s, (DWORD)_mi_strlen(s), NULL, NULL); +//} -void _mi_prim_thread_associate_default_heap(mi_heap_t* heap) { - mi_assert_internal(mi_fls_key != (DWORD)(-1)); - FlsSetValue(mi_fls_key, heap); +static void NTAPI mi_win_main(PVOID module, DWORD reason, LPVOID reserved) { + MI_UNUSED(reserved); + MI_UNUSED(module); + if (reason==DLL_PROCESS_ATTACH) { + _mi_auto_process_init(); + } + else if (reason==DLL_PROCESS_DETACH) { + _mi_auto_process_done(); + } + else if (reason==DLL_THREAD_DETACH && !_mi_is_redirected()) { + _mi_thread_done(NULL); + } } -#else -// Dll; nothing to do as in that case thread_done is handled through the DLL_THREAD_DETACH event. +/* ----------------------------------------------------------------------- + Auto initialize and finalize mimalloc on process and thread start/end. + By default we use a combination of CRT init and TLS sections for + both static and dynamic linkage (`MI_WIN_INIT_USE_CRT_TLS`). +------------------------------------------------------------------------- */ +#if !defined(MI_WIN_INIT_USE_CRT_TLS) && !defined(MI_WIN_INIT_USE_RAW_DLLMAIN) && !defined(MI_WIN_INIT_USE_TLS_DLLMAIN) && !defined(MI_WIN_INIT_USE_FLS) + #if defined(__INTEL_LLVM_COMPILER) || defined(__INTEL_COMPILER) + #define MI_WIN_INIT_USE_TLS_DLLMAIN 1 /* needed for Intel ICX, see issue #1268 */ + #else + #define MI_WIN_INIT_USE_CRT_TLS 1 /* default */ + #endif +#endif -void _mi_prim_thread_init_auto_done(void) { -} +#if defined(MI_WIN_INIT_USE_CRT_TLS) + #if !defined(__MINGW32__) || defined(MI_MINGW_UCRT64) // on mingw without UCRT use the constructor attribute (in `src/prim/prim.c`) + #define MI_PRIM_HAS_PROCESS_ATTACH 1 + #endif -void _mi_prim_thread_done_auto_done(void) { -} + // nothing to do since `_mi_thread_done` is handled through the DLL_THREAD_DETACH event. + void _mi_prim_thread_init_auto_done(void) {} + void _mi_prim_thread_done_auto_done(void) {} + void _mi_prim_thread_associate_default_theap(mi_theap_t* theap) { + MI_UNUSED(theap); + } -void _mi_prim_thread_associate_default_heap(mi_heap_t* heap) { - MI_UNUSED(heap); -} + static bool mi_module_is_dll(PVOID mod) { + if (mod==NULL) return false; + PIMAGE_DOS_HEADER imageDosHeader = (PIMAGE_DOS_HEADER)mod; + PIMAGE_NT_HEADERS imageNtHeaders = (PIMAGE_NT_HEADERS)((unsigned char*)imageDosHeader + imageDosHeader->e_lfanew); + return ((imageNtHeaders->FileHeader.Characteristics & IMAGE_FILE_DLL) == IMAGE_FILE_DLL); + } + + mi_decl_externc IMAGE_DOS_HEADER __ImageBase; // supplied by the linker + + static bool mi_current_module_is_dll(void) { + // HMODULE mod = NULL; + // const BOOL ok = GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCSTR)mi_current_module_is_dll, &mod); + // return (ok && mi_module_is_dll(mod)); + const HMODULE mod = (HMODULE)(&__ImageBase); + return mi_module_is_dll(mod); + } + // Hook into CRT initialization and finalization. + static void mi_cdecl mi_crt_done(void) { + // mi_debug_out(mi_current_module_is_dll() ? "crt dll done\n" : "crt exe done\n"); + mi_win_main(NULL, DLL_PROCESS_DETACH, 0); + } + + static int mi_cdecl mi_crt_init(void) { + // mi_debug_out(mi_current_module_is_dll() ? "crt dll init\n" : "crt exe init\n"); + if (mi_current_module_is_dll()) { + // in a dll, atexit (crt_done) is called after tls process detach + atexit(&mi_crt_done); + } + return 0; + } + + // We also hook into the Windows loader TLS initialization and finalization. + static void NTAPI mi_tls_attach(PVOID module, DWORD reason, LPVOID reserved) { + if (reason == DLL_THREAD_ATTACH) { + //mi_debug_out("tls thread attach\n"); + mi_win_main(module, reason, reserved); + } + else if (reason == DLL_PROCESS_ATTACH) { + // mi_debug_out(mi_module_is_dll(module) ? "tls dll process attach\n" : "tls exe process attach\n"); + // tls process attach is always called before crt init + mi_win_main(module, reason, reserved); + } + } + + static void NTAPI mi_tls_detach(PVOID module, DWORD reason, LPVOID reserved) { + if (reason == DLL_THREAD_DETACH) { + //mi_debug_out("tls thread detach\n"); + mi_win_main(module, reason, reserved); + } + else if (reason == DLL_PROCESS_DETACH) { + // mi_debug_out(mi_module_is_dll(module) ? "tls dll process detach\n" : "tls exe process detach\n"); + if (!mi_module_is_dll(module)) { + // in an exe, tls process detach is called after atexit (mi_crt_done) + mi_win_main(module, reason, reserved); + } + } + } + + // Set up TLS callbacks in a statically linked library by using special data sections. + // See + // We use 2 entries to ensure we call attach events before constructors + // are called, and detach events after destructors are called. + // We also set up a CRT initialization callback. + #if defined(__cplusplus) + extern "C" { + #endif + + typedef int (mi_cdecl* mi_crt_callback_t)(void); + + #if (defined(_WIN64) || defined(_M_ARM)) && defined(_MSC_VER) // no underscore + #pragma comment(linker, "/INCLUDE:_tls_used") + #pragma comment(linker, "/INCLUDE:_mi_tls_callback_pre") + #pragma comment(linker, "/INCLUDE:_mi_tls_callback_post") + #pragma comment(linker, "/INCLUDE:_mi_crt_callback_init") + #pragma const_seg(".CRT$XLB") + extern const PIMAGE_TLS_CALLBACK _mi_tls_callback_pre[]; + const PIMAGE_TLS_CALLBACK _mi_tls_callback_pre[] = { &mi_tls_attach }; + #pragma const_seg() + #pragma const_seg(".CRT$XLY") + extern const PIMAGE_TLS_CALLBACK _mi_tls_callback_post[]; + const PIMAGE_TLS_CALLBACK _mi_tls_callback_post[] = { &mi_tls_detach }; + #pragma const_seg() + #pragma const_seg(".CRT$XIB") + extern const mi_crt_callback_t _mi_crt_callback_init[]; + const mi_crt_callback_t _mi_crt_callback_init[] = { &mi_crt_init }; + #pragma const_seg() + #elif defined(_MSC_VER) // x86 32-bit, add underscore + #pragma comment(linker, "/INCLUDE:__tls_used") + #pragma comment(linker, "/INCLUDE:__mi_tls_callback_pre") + #pragma comment(linker, "/INCLUDE:__mi_tls_callback_post") + #pragma comment(linker, "/INCLUDE:__mi_crt_callback_init") + #pragma data_seg(".CRT$XLB") + PIMAGE_TLS_CALLBACK _mi_tls_callback_pre[] = { &mi_tls_attach }; + #pragma data_seg() + #pragma data_seg(".CRT$XLY") + PIMAGE_TLS_CALLBACK _mi_tls_callback_post[] = { &mi_tls_detach }; + #pragma data_seg() + #pragma data_seg(".CRT$XIB") + mi_crt_callback_t _mi_crt_callback_init[] = { &mi_crt_init }; + #pragma data_seg() + #elif defined(__MINGW32__) + extern const IMAGE_TLS_DIRECTORY _tls_used; + __attribute__((used)) static const void* const mi_tls_used_ref = &_tls_used; // pull in the CRT tls + __attribute__((used, section(".CRT$XLB"))) PIMAGE_TLS_CALLBACK _mi_tls_callback_pre = &mi_tls_attach; + __attribute__((used, section(".CRT$XLY"))) PIMAGE_TLS_CALLBACK _mi_tls_callback_post = &mi_tls_detach; + __attribute__((used, section(".CRT$XIB"))) mi_crt_callback_t _mi_crt_callback_init = &mi_crt_init; + #endif + + #if defined(__cplusplus) + } + #endif + +/* ---------------------------------------------------------------- + Here we use a combination of the raw DLL main and TLS sections for + both static and dynamic linkage. This is perhaps the most robust as + the raw DLL main ensure we can close mimalloc latest possible. + However, it can lead to link errors with on `_pRawDllMain` if other + libraries also try to hook into this (like Boost). +*/ +#elif defined(MI_WIN_INIT_USE_RAW_DLLMAIN) + #define MI_PRIM_HAS_PROCESS_ATTACH 1 + // nothing to do since `_mi_thread_done` is handled through the DLL_THREAD_DETACH event. + void _mi_prim_thread_init_auto_done(void) {} + void _mi_prim_thread_done_auto_done(void) {} + void _mi_prim_thread_associate_default_theap(mi_theap_t* theap) { + MI_UNUSED(theap); + } + + // If linked into a DLL module, this raw entry is called before the CRT attach and + // after the CRT detach through the CRT _pRawDllMain pointer. + static BOOL NTAPI mi_dll_main_raw(PVOID module, DWORD reason, LPVOID reserved) { + mi_win_main(module, reason, reserved); + return TRUE; + } + + // Set the value of the CRT _pRawDllMain pointer + #if defined(__cplusplus) + extern "C" + #endif + PVOID _pRawDllMain = (PVOID)&mi_dll_main_raw; + + // We also hook into the Windows loader TLS initialization and finalization. + // If we are linked into an EXE module we rely on these as `mi_dll_main_raw` + // is not called (and otherwise we ignore the TLS callbacks by checking if we are in a DLL). + static bool mi_module_is_dll(PVOID mod) { + if (mod==NULL) return false; + PIMAGE_DOS_HEADER imageDosHeader = (PIMAGE_DOS_HEADER)mod; + PIMAGE_NT_HEADERS imageNtHeaders = (PIMAGE_NT_HEADERS)((unsigned char*)imageDosHeader + imageDosHeader->e_lfanew); + return ((imageNtHeaders->FileHeader.Characteristics & IMAGE_FILE_DLL) == IMAGE_FILE_DLL); + } + + static void NTAPI mi_tls_attach(PVOID module, DWORD reason, LPVOID reserved) { + if (reason == DLL_PROCESS_ATTACH || reason == DLL_THREAD_ATTACH) { + if (!mi_module_is_dll(module)) { + mi_win_main(module, reason, reserved); + } + } + } + + static void NTAPI mi_tls_detach(PVOID module, DWORD reason, LPVOID reserved) { + if (reason == DLL_PROCESS_DETACH || reason == DLL_THREAD_DETACH) { + if (!mi_module_is_dll(module)) { + mi_win_main(module, reason, reserved); + } + } + } + + // Set up TLS callbacks in a statically linked library by using special data sections. + // See + // We use 2 entries to ensure we call attach events before constructors + // are called, and detach events after destructors are called. + #if defined(__cplusplus) + extern "C" { + #endif + + #if (defined(_WIN64) || defined(_M_ARM)) && defined(_MSC_VER) // no underscore + #pragma comment(linker, "/INCLUDE:_tls_used") + #pragma comment(linker, "/INCLUDE:_mi_tls_callback_pre") + #pragma comment(linker, "/INCLUDE:_mi_tls_callback_post") + #pragma const_seg(".CRT$XLB") + extern const PIMAGE_TLS_CALLBACK _mi_tls_callback_pre[]; + const PIMAGE_TLS_CALLBACK _mi_tls_callback_pre[] = { &mi_tls_attach }; + #pragma const_seg() + #pragma const_seg(".CRT$XLY") + extern const PIMAGE_TLS_CALLBACK _mi_tls_callback_post[]; + const PIMAGE_TLS_CALLBACK _mi_tls_callback_post[] = { &mi_tls_detach }; + #pragma const_seg() + #elif defined(_MSC_VER) // x86 32-bit + #pragma comment(linker, "/INCLUDE:__tls_used") + #pragma comment(linker, "/INCLUDE:__mi_tls_callback_pre") + #pragma comment(linker, "/INCLUDE:__mi_tls_callback_post") + #pragma data_seg(".CRT$XLB") + PIMAGE_TLS_CALLBACK _mi_tls_callback_pre[] = { &mi_tls_attach }; + #pragma data_seg() + #pragma data_seg(".CRT$XLY") + PIMAGE_TLS_CALLBACK _mi_tls_callback_post[] = { &mi_tls_detach }; + #pragma data_seg() + #elif defined(__MINGW32__) + extern const IMAGE_TLS_DIRECTORY _tls_used; + __attribute__((used)) static const void* const mi_tls_used_ref = &_tls_used; // pull in the CRT tls + __attribute__((used, section(".CRT$XLB"))) PIMAGE_TLS_CALLBACK _mi_tls_callback_pre = &mi_tls_attach; + __attribute__((used, section(".CRT$XLY"))) PIMAGE_TLS_CALLBACK _mi_tls_callback_post = &mi_tls_detach; + #endif + + #if defined(__cplusplus) + } + #endif + +/* -------------------------------------------------------------------- + Legacy options: DllMain, TLS, and FLS + --------------------------------------------------------------------*/ +#elif defined(MI_WIN_INIT_USE_TLS_DLLMAIN) && defined(MI_SHARED_LIB) + #define MI_PRIM_HAS_PROCESS_ATTACH 1 + + // Windows DLL: easy to hook into process_init and thread_done + BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID reserved) { + mi_win_main((PVOID)inst,reason,reserved); + return TRUE; + } + + // nothing to do since `_mi_thread_done` is handled through the DLL_THREAD_DETACH event. + void _mi_prim_thread_init_auto_done(void) { } + void _mi_prim_thread_done_auto_done(void) { } + void _mi_prim_thread_associate_default_theap(mi_theap_t* theap) { + MI_UNUSED(theap); + } + +#elif defined(MI_WIN_INIT_USE_TLS_DLLMAIN) + #define MI_PRIM_HAS_PROCESS_ATTACH 1 + + static void NTAPI mi_win_main_attach(PVOID module, DWORD reason, LPVOID reserved) { + if (reason == DLL_PROCESS_ATTACH || reason == DLL_THREAD_ATTACH) { + mi_win_main(module, reason, reserved); + } + } + static void NTAPI mi_win_main_detach(PVOID module, DWORD reason, LPVOID reserved) { + if (reason == DLL_PROCESS_DETACH || reason == DLL_THREAD_DETACH) { + mi_win_main(module, reason, reserved); + } + } + + // Set up TLS callbacks in a statically linked library by using special data sections. + // See + // We use 2 entries to ensure we call attach events before constructors + // are called, and detach events after destructors are called. + #if defined(__cplusplus) + extern "C" { + #endif + + #if (defined(_WIN64) || defined(_M_ARM)) && defined(_MSC_VER) // no underscore + #pragma comment(linker, "/INCLUDE:_tls_used") + #pragma comment(linker, "/INCLUDE:_mi_tls_callback_pre") + #pragma comment(linker, "/INCLUDE:_mi_tls_callback_post") + #pragma const_seg(".CRT$XLB") + extern const PIMAGE_TLS_CALLBACK _mi_tls_callback_pre[]; + const PIMAGE_TLS_CALLBACK _mi_tls_callback_pre[] = { &mi_win_main_attach }; + #pragma const_seg() + #pragma const_seg(".CRT$XLY") + extern const PIMAGE_TLS_CALLBACK _mi_tls_callback_post[]; + const PIMAGE_TLS_CALLBACK _mi_tls_callback_post[] = { &mi_win_main_detach }; + #pragma const_seg() + #elif defined(_MSC_VER) // x86 32-bit + #pragma comment(linker, "/INCLUDE:__tls_used") + #pragma comment(linker, "/INCLUDE:__mi_tls_callback_pre") + #pragma comment(linker, "/INCLUDE:__mi_tls_callback_post") + #pragma data_seg(".CRT$XLB") + PIMAGE_TLS_CALLBACK _mi_tls_callback_pre[] = { &mi_win_main_attach }; + #pragma data_seg() + #pragma data_seg(".CRT$XLY") + PIMAGE_TLS_CALLBACK _mi_tls_callback_post[] = { &mi_win_main_detach }; + #pragma data_seg() + #elif defined(__MINGW32__) + extern const IMAGE_TLS_DIRECTORY _tls_used; + __attribute__((used)) static const void* const mi_tls_used_ref = &_tls_used; // pull in the CRT tls + __attribute__((used, section(".CRT$XLB"))) PIMAGE_TLS_CALLBACK _mi_tls_callback_pre = &mi_win_main_attach; + __attribute__((used, section(".CRT$XLY"))) PIMAGE_TLS_CALLBACK _mi_tls_callback_post = &mi_win_main_detach; + #endif + + #if defined(__cplusplus) + } + #endif + + // nothing to do since `_mi_thread_done` is handled through the DLL_THREAD_DETACH event. + void _mi_prim_thread_init_auto_done(void) { } + void _mi_prim_thread_done_auto_done(void) { } + void _mi_prim_thread_associate_default_theap(mi_theap_t* theap) { + MI_UNUSED(theap); + } + +#elif defined(MI_WIN_INIT_USE_FLS) // deprecated: statically linked, use fiber api + + #if defined(_MSC_VER) // on clang/gcc use the constructor attribute (in `src/prim/prim.c`) + // MSVC: use data section magic for static libraries + // See + #define MI_PRIM_HAS_PROCESS_ATTACH 1 + + static int mi_cdecl mi_crt_init(void) { + mi_win_main(NULL,DLL_PROCESS_ATTACH,NULL); + atexit(&_mi_auto_process_done); + return 0; + } + + #if defined(__cplusplus) + extern "C" { + #endif + typedef int (mi_cdecl* mi_crt_callback_t)(void); + #if defined(_WIN64) // 64-bit + #pragma comment(linker, "/INCLUDE:_mi_crt_callback_init") + #pragma const_seg(".CRT$XIU") + extern const mi_crt_callback_t _mi_crt_callback_init[]; + const mi_crt_callback_t _mi_crt_callback_init[] = { &mi_crt_init }; + #pragma const_seg() + #else // 32-bit + #pragma comment(linker, "/INCLUDE:__mi_crt_callback_init") + #pragma data_seg(".CRT$XIU") + mi_crt_callback_t _mi_crt_callback_init[] = { &mi_crt_init }; + #pragma data_seg() + #endif + #if defined(__cplusplus) + } + #endif + #endif + + // use the fiber api for calling `_mi_thread_done`. + #include + #if (_WIN32_WINNT < 0x600) // before Windows Vista + WINBASEAPI DWORD WINAPI FlsAlloc( _In_opt_ PFLS_CALLBACK_FUNCTION lpCallback ); + WINBASEAPI PVOID WINAPI FlsGetValue( _In_ DWORD dwFlsIndex ); + WINBASEAPI BOOL WINAPI FlsSetValue( _In_ DWORD dwFlsIndex, _In_opt_ PVOID lpFlsData ); + WINBASEAPI BOOL WINAPI FlsFree(_In_ DWORD dwFlsIndex); + #endif + + static DWORD mi_fls_key = (DWORD)(-1); + + static void NTAPI mi_fls_done(PVOID value) { + mi_theap_t* theap = (mi_theap_t*)value; + if (theap != NULL) { + _mi_thread_done(theap); + FlsSetValue(mi_fls_key, NULL); // prevent recursion as _mi_thread_done may set it back to the main theap, issue #672 + } + } + + void _mi_prim_thread_init_auto_done(void) { + mi_fls_key = FlsAlloc(&mi_fls_done); + } + + void _mi_prim_thread_done_auto_done(void) { + // call thread-done on all threads (except the main thread) to prevent + // dangling callback pointer if statically linked with a DLL; Issue #208 + FlsFree(mi_fls_key); + } + + void _mi_prim_thread_associate_default_theap(mi_theap_t* theap) { + // mi_thread_init runs before _mi_prim_thread_init_auto_done during process init + if (mi_fls_key == (DWORD)(-1)) return; + FlsSetValue(mi_fls_key, theap); + } +#else +#error "define windows process and thread auto initialization" +#endif + +// ---------------------------------------------------- +// Communicate with the redirection module on Windows +// ---------------------------------------------------- +#if defined(MI_SHARED_LIB) && !defined(MI_WIN_NOREDIRECT) + #define MI_PRIM_HAS_ALLOCATOR_INIT 1 + + static bool mi_redirected = false; // true if malloc redirects to mi_malloc + + bool _mi_is_redirected(void) { + return mi_redirected; + } + + #ifdef __cplusplus + extern "C" { + #endif + mi_decl_export void _mi_redirect_entry(DWORD reason) { + // called on redirection; careful as this may be called before DllMain + if (reason == DLL_PROCESS_ATTACH) { + mi_redirected = true; + } + else if (reason == DLL_PROCESS_DETACH) { + mi_redirected = false; + } + else if (reason == DLL_THREAD_DETACH) { + // mi_debug_out("redirect thread detach\n"); + _mi_thread_done(NULL); + } + } + __declspec(dllimport) bool mi_cdecl mi_allocator_init(const char** message); + __declspec(dllimport) void mi_cdecl mi_allocator_done(void); + #ifdef __cplusplus + } + #endif + bool _mi_allocator_init(const char** message) { + return mi_allocator_init(message); + } + void _mi_allocator_done(void) { + mi_allocator_done(); + } #endif diff --git a/Objects/mimalloc/profile/pprof.c b/Objects/mimalloc/profile/pprof.c new file mode 100644 index 000000000000000..ea0ae528298e542 --- /dev/null +++ b/Objects/mimalloc/profile/pprof.c @@ -0,0 +1,1441 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. + +Disclaimer: Unlike the rest of the considerate hand-crafted mimalloc sources, +this file was written with AI assistance (for the process image and protobuf +format generation). +-----------------------------------------------------------------------------*/ + +// --------------------------------------------------------------------------- +// This implements a basic memory profiler that outputs pprof-compatible heap +// snapshots (text or protobuf). It is basic, but it does scale with a lock-free +// hash table for locations, and uses poisson distributed sampling. +// It has no impact on runtime performance when profiling is not active. +// +// It can be used programmatically (using `mi_pprof_profiler_new` etc.) +// or be invoked with environment variables. For example: +// MIMALLOC_PROFILE=profile ./my_program +// and then analyze the generated profile using pprof tools: +// pprof -http=:8080 ./myprogram profile.* +// --------------------------------------------------------------------------- + +#include "mimalloc.h" +#include "mimalloc/prim/prim-tls.h" // _mi_theap_default +#include "mimalloc/util/libc.h" +#include "mimalloc/util/options.h" +#include "mimalloc/util/stats.h" +#include "mimalloc/profile/pprof.h" + +// --------------------------------------------------------------------------- +// Internal API +// --------------------------------------------------------------------------- + +typedef struct mi_location_s mi_location_t; +typedef struct mi_locations_s mi_locations_t; +typedef struct mi_callstack_s mi_callstack_t; + +static bool mi_locations_init(mi_heap_t* heap, mi_locations_t* locations); +static void mi_locations_done(mi_heap_t* heap, mi_locations_t* locations); +static mi_location_t* mi_locations_find_or_insert(mi_heap_t* heap, mi_locations_t* locations, mi_threadid_t thread_id, const mi_callstack_t* callstack, size_t callstack_hash); + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- +#define MI_MAX_BACKTRACE_DEPTH (100) +#define MI_BACKTRACE_SKIP_FRAMES (3) + +// Threshold (in bytes) for the thread-local interval countdown batching in `on_alloc` +#ifndef MI_PROFILE_COUNTDOWN_THRESHOLD +#define MI_PROFILE_COUNTDOWN_THRESHOLD (8*MI_KiB) +#endif +#define MI_PROFILE_COUNTDOWN_MIN_INTERVAL (8*MI_PROFILE_COUNTDOWN_THRESHOLD) + +// A callstack represents a sequence of function call frames captured at a specific point in time. +struct mi_callstack_s { + size_t count; + void** frames; +}; + +// A location is a unique combination of a thread and runtime callstack for +// which we track allocation and deallocation statistics. +// +// Locations are only ever inserted, or mutated in-place after publication, and a location is only ever looked up using its own +// `hash`/`thread_id`/`callstack` which allows a lock-free lookup of locations in the hash table. +struct mi_location_s { + _Atomic(struct mi_location_s*) next; // next location in the same hash bucket (for chaining on collision); + mi_threadid_t thread_id; + mi_callstack_t callstack; + size_t hash; // hash of the thread_id and callstack + int64_t alloc_count; // only ever written by the single (allocating) owning thread: not atomic + int64_t alloc_bytes; + _Atomic(size_t) inuse_count; // currently in-use (allocated but not yet freed) objects/bytes for this location; + _Atomic(size_t) inuse_bytes; // incremented in `on_alloc` and decremented in `on_free`, both possibly on any thread +}; + +// A basic hash table of locations. The bucket array is fixed-size (allocated +// once in `mi_locations_init`) and each bucket is a lock-free singly-linked +// list that only ever grows by prepending (see `mi_locations_find_or_insert`). +struct mi_locations_s { + size_t bucket_count; // number of buckets (fixed size) + _Atomic(mi_location_t*)* buckets; // array of `bucket_count` chain heads + _Atomic(size_t) count; // number of locations currently stored (just a statistic) +}; + +// Our profiler. +typedef struct { + mi_profiler_t profiler; // mimalloc profiler info + mi_heap_t* profile_heap; // heap used for all profiler allocations (so these don't interfere) + mi_locations_t locations; // hash table of call locations + size_t sample_threshold; // current sample threshold + size_t snapshot_count; // number of times `mi_profiler_snapshot` was called (used to number the snapshot files) + char* base_file_name; // base file name for snapshot files, e.g. "..heap" + bool format_text; // text or protobuf snapshot format + size_t alloc_interval_size; // if >0, automatically snapshot every `alloc_interval_size` allocated bytes (capped at `MI_SSIZE_MAX`) + _Atomic(mi_ssize_t) alloc_interval_countdown; // bytes remaining until the next automatic snapshot (can go negative; decremented concurrently by `on_alloc` on any thread) + size_t inuse_interval_size; // if >0, automatically snapshot every time (sampled) in-use bytes grow by `inuse_interval_size` bytes + _Atomic(size_t) inuse_bytes; // running total of (sampled) in-use bytes across all locations (can never exceed the address space); incremented in `on_alloc`, decremented in `on_free`, both possibly on any thread + _Atomic(size_t) inuse_interval_threshold; // next `inuse_bytes` total that triggers a snapshot; advanced (monotonically increasing) whenever reached in `on_alloc` + size_t time_interval_secs; // if >0, automatically snapshot every `time_interval_secs` seconds (checked from `on_alloc`) + _Atomic(mi_msecs_t) time_interval_deadline; // next `_mi_clock_now()` value that triggers a snapshot; advanced (monotonically increasing) whenever reached in `on_alloc` +} mi_pprof_profiler_t; + +static inline mi_pprof_profiler_t* downcast( mi_profiler_t* prof ) { + return (mi_pprof_profiler_t*)prof; +} + +static void mi_pprof_profiler_snapshot(mi_pprof_profiler_t* prof); +static size_t mi_prim_backtrace(void** buffer, size_t max_depth, size_t* hash); // `buffer` must have room for `max_depth + MI_PPROF_SKIP_FRAMES + 1` entries (see definition) + + +// --------------------------------------------------------------------------- +// Automatic profiler driven by the `MIMALLOC_PROFILE` environment variable. +// This allows profiling an arbitrary program (with no code changes) by +// overriding its allocator with a mimalloc shared library build and setting +// `MIMALLOC_PROFILE=` +// (and optionally `MIMALLOC_PROFILE_ALLOC_INTERVAL=` to periodically snapshot +// while the program is running). +// --------------------------------------------------------------------------- +#ifndef MI_PROFILE_USE_BUILTIN +#define MI_PROFILE_USE_BUILTIN (MI_PROFILE) +#endif + +#if MI_PROFILE_USE_BUILTIN // todo: also disable profiling in secure mode? + +static bool mi_is_elevated_process(void); +static mi_profiler_t* mi_profile_env_profiler; // NULL if `MIMALLOC_PROFILE` was not set (or initialization failed) + +// Called once at process initialization (see `mi_process_init` in `heap/init.c`). +void _mi_pprof_profiler_init(void) { + #if MI_PROFILE + if (mi_is_elevated_process()) return; // don't let an untrusted environment influence a privileged process + char fname[1024]; + if (_mi_getenv("mimalloc_profile", fname, sizeof(fname)) != 0 || fname[0] == 0) return; // `MIMALLOC_PROFILE` not set + const size_t sample_rate = mi_option_get_size(mi_option_profile_sample_rate); // 16 KiB by default + const size_t alloc_interval_size = mi_option_get_size(mi_option_profile_alloc_interval); // 0 by default (no automatic interval snapshots) + const size_t inuse_interval_size = mi_option_get_size(mi_option_profile_inuse_interval); // 0 by default (no automatic interval snapshots) + const size_t time_interval_secs = mi_option_get_clamp(mi_option_profile_time_interval,0,24*60*60L); // 0 by default (no automatic interval snapshots) + mi_profiler_t* profiler = mi_pprof_profiler_new(sample_rate, fname, alloc_interval_size, inuse_interval_size, time_interval_secs); + if (profiler == NULL) return; + mi_profile_env_profiler = profiler; + _mi_verbose_message("pprof profiler initialized with base file name: %s\n", fname); + mi_profile(profiler); // attach to the main sub-process (and any current/future heaps in it) + mi_profiler_start(profiler); + #endif +} + +// Called once at process termination (see `mi_process_done` in `heap/init.c`). +void _mi_pprof_profiler_done(void) { + #if MI_PROFILE + mi_profiler_t* profiler = mi_profile_env_profiler; + if (profiler == NULL) return; + mi_profile_env_profiler = NULL; + mi_profiler_stop(profiler); + _mi_verbose_message("pprof profiler done\n"); + mi_profiler_snapshot(profiler); // final snapshot so short-lived processes still get a complete profile + mi_profile(NULL); // detach from the main sub-process + mi_pprof_profiler_delete(profiler); + #endif +} + +#else + +void _mi_pprof_profiler_init(void) { +} + +void _mi_pprof_profiler_done(void) { +} + +#endif + +// --------------------------------------------------------------------------- +// Get a location +// --------------------------------------------------------------------------- + +static mi_decl_noinline mi_location_t* mi_location_get(mi_pprof_profiler_t* prof) { + void* frames[MI_MAX_BACKTRACE_DEPTH + 1 + MI_BACKTRACE_SKIP_FRAMES]; // extra room: `mi_prim_backtrace` captures+shifts out its own frame and `MI_BACKTRACE_SKIP_FRAMES` wrapper frames in-place + size_t callstack_hash = 0; + const size_t depth = mi_prim_backtrace(frames, MI_MAX_BACKTRACE_DEPTH, &callstack_hash); + if (depth==0) return NULL; + mi_callstack_t callstack = { depth, frames }; + const mi_threadid_t thread_id = _mi_thread_id(); + mi_location_t* loc = mi_locations_find_or_insert(prof->profile_heap, &prof->locations, thread_id, &callstack, callstack_hash); + return loc; +} + +// --------------------------------------------------------------------------- +// Profiler callbacks and initialization +// --------------------------------------------------------------------------- + +// Draws an integer approximating Exp(scale) to give a poisson distribution, +// instead of a fixed period, which avoids sampling bias from allocation +// patterns that happen to align with a constant period +static size_t mi_exp_sample(mi_theap_t* theap, size_t scale) { + if (scale == 0) return 0; // keep 0 + #if MI_PROFILE==2 + if (scale == 1) return 1; // keep 1 as is (to allow sampling every allocation) + #endif + const size_t r = _mi_theap_random_next(theap) | 1; + const size_t lz = mi_clz(r); // coarse -log2(U): Geometric(1/2) + const size_t frac_q8 = (r << (lz + 1)) >> (MI_SIZE_BITS - 8); // next 8 bits: fractional refinement, in [0,256) + const size_t bits_q8 = (lz << 8) | frac_q8; // Q8 fixed-point estimate of -log2(U) + const size_t ln_q8 = (bits_q8 * 177) >> 8; // -log2(U) -> -ln(U): *ln(2) (~177/256) + size_t next = ((ln_q8 * scale) >> 8) + 1; // scale by the mean; +1 avoids a zero threshold + const size_t cap = scale * 32; // clamp the (rare) long tail + return (next > cap ? cap : next); +} + +// Check if the sampled bytes accumulated so far (batched per-thread) should trigger a shared +// countdown decrement, and if so, whether that decrement crosses zero (triggering a snapshot and +// resetting the countdown to `alloc_interval_size`). +static bool mi_interval_update(uint64_t ubytes_since_last_sample, size_t alloc_interval_size, _Atomic(mi_ssize_t)* alloc_interval_countdown, mi_ssize_t* interval_pending) { + mi_ssize_t sampled_bytes = 0; + mi_ssize_t bytes_since_last_sample= (ubytes_since_last_sample > MI_SSIZE_MAX ? MI_SSIZE_MAX : (mi_ssize_t)ubytes_since_last_sample); + if (alloc_interval_size < MI_PROFILE_COUNTDOWN_MIN_INTERVAL || MI_SSIZE_MAX - *interval_pending < bytes_since_last_sample /* overflow? */) { + // interval too small relative to the batching; always adjust + sampled_bytes = bytes_since_last_sample; + } + else { + // batch locally per-thread and only touch the shared (contended) countdown + // once the local pending amount reaches `MI_PROFILE_COUNTDOWN_THRESHOLD`. + *interval_pending += bytes_since_last_sample; + if (*interval_pending >= (mi_ssize_t)MI_PROFILE_COUNTDOWN_THRESHOLD) { + sampled_bytes = *interval_pending; + *interval_pending = 0; + } + } + // update profile interval countdown if we have sampled bytes + if (sampled_bytes > 0) { + const mi_ssize_t new_countdown = mi_atomic_addi_relaxed(alloc_interval_countdown, -sampled_bytes) - sampled_bytes; + if (new_countdown <= 0 && new_countdown + sampled_bytes > 0) { + mi_atomic_storess_relaxed(alloc_interval_countdown, (mi_ssize_t)alloc_interval_size); + return true; + } + } + return false; +} + +// Check if the (global) in-use byte total has reached `inuse_interval_threshold` +static bool mi_inuse_threshold_update(size_t new_total, size_t inuse_interval_size, _Atomic(size_t)* inuse_interval_threshold) { + size_t threshold = mi_atomic_load_relaxed(inuse_interval_threshold); + while (new_total >= threshold) { + size_t next_threshold = threshold + inuse_interval_size; + while (next_threshold <= new_total) { next_threshold += inuse_interval_size; } // skip ahead if we grew by more than one interval + if (mi_atomic_cas_weak_relaxed(inuse_interval_threshold, &threshold, next_threshold)) { + return true; + } + // CAS failed: `threshold` now holds the current value; retry (another thread may have already advanced it far enough) + } + return false; +} + +// Check if `now` has reached `time_interval_deadline`, and if so, atomically advance the +// deadline to the next multiple of `time_interval_secs` beyond `now`, returning `true` (to the +// single thread that won the race) so a snapshot is triggered exactly once. +static bool mi_time_threshold_update(mi_msecs_t now, size_t time_interval_secs, _Atomic(mi_msecs_t)* time_interval_deadline) { + const mi_msecs_t interval_msecs = (mi_msecs_t)time_interval_secs * 1000; + mi_msecs_t deadline = mi_atomic_loadi64_relaxed(time_interval_deadline); + while (now >= deadline) { + mi_msecs_t next_deadline = deadline + interval_msecs; + while (next_deadline <= now) { next_deadline += interval_msecs; } // skip ahead if more than one interval has passed + if (mi_atomic_casi64_strong_acq_rel(time_interval_deadline, &deadline, next_deadline)) { + return true; + } + // CAS failed: `deadline` now holds the current value; retry (another thread may have already advanced it far enough) + } + return false; +} + +// Sample allocation event and update profiling data accordingly +static size_t mi_cdecl on_alloc(mi_profiler_t* profiler, mi_profiler_sample_data_t* data, void* ptr, size_t requested_size, size_t threshold, uint64_t bytes_since_last_sample, mi_heap_t* heap) +{ + MI_UNUSED(threshold); MI_UNUSED(heap); MI_UNUSED(ptr); + mi_pprof_profiler_t* prof = downcast(profiler); + mi_location_t* loc = mi_location_get(prof); + mi_theap_t* theap = _mi_heap_theap(heap); + const size_t new_threshold = mi_exp_sample(theap, prof->sample_threshold); // randomized (Poisson) next sample threshold + const size_t alloc_size = requested_size; // or mi_heap_usable_size(heap,ptr) ? + + if (data!=NULL) { + // pass the location to the corresponding mi_free (maybe called from another thread) + mi_assert(data->user_data_size >= 2*sizeof(void*)); + data->user_data[0] = loc; + data->user_data[1] = (void*)((uintptr_t)alloc_size); + } + if (loc!=NULL) { + // `on_alloc` for a given location only ever runs on its one owning (allocating) + // thread, so plain increments are safe here (no concurrent writers possible). + loc->alloc_bytes += (int64_t)alloc_size; + loc->alloc_count += 1; + // `inuse_count`/`inuse_bytes` can be decremented concurrently by `on_free` on any + // thread, so these updates must be atomic. + mi_atomic_add_relaxed(&loc->inuse_bytes, alloc_size); + mi_atomic_increment_relaxed(&loc->inuse_count); + } + + if (prof->alloc_interval_size > 0) { // interval-based profiling enabled + mi_ssize_t* pinterval_pending = (mi_ssize_t*)&theap->profiler_reserved1; + if (mi_interval_update(bytes_since_last_sample, prof->alloc_interval_size, &prof->alloc_interval_countdown, pinterval_pending)) { + mi_profiler_snapshot(profiler); + } + } + if (prof->inuse_interval_size > 0) { // snapshot whenever (sampled) in-use memory has grown by `inuse_interval_size` bytes + const size_t total_inuse_bytes = mi_atomic_add_relaxed(&prof->inuse_bytes, alloc_size) + alloc_size; + if (mi_inuse_threshold_update(total_inuse_bytes, prof->inuse_interval_size, &prof->inuse_interval_threshold)) { + mi_profiler_snapshot(profiler); + } + } + if (prof->time_interval_secs > 0) { // snapshot whenever `time_interval_secs` seconds have passed + if (mi_time_threshold_update(_mi_clock_now(), prof->time_interval_secs, &prof->time_interval_deadline)) { + mi_profiler_snapshot(profiler); + } + } + return new_threshold; +} + +// Sample free event and update profiling data accordingly +static void mi_cdecl on_free(mi_profiler_t* profiler, mi_profiler_sample_data_t* data, void* ptr, mi_heap_t* heap) +{ + MI_UNUSED(heap); MI_UNUSED(ptr); + mi_pprof_profiler_t* prof = downcast(profiler); + if (data!=NULL) { + mi_assert(data->user_data_size >= 2*sizeof(void*)); + mi_location_t* loc = (mi_location_t*)data->user_data[0]; + const size_t freed_bytes = (size_t)((uintptr_t)data->user_data[1]); + if (loc!=NULL) { + // different threads can free allocations that share the same (allocating-thread, + // callstack) location concurrently, so these updates must be atomic. + mi_atomic_sub_relaxed(&loc->inuse_bytes, freed_bytes); + mi_atomic_decrement_relaxed(&loc->inuse_count); + } + if (prof->inuse_interval_size > 0) { // keep the (global) in-use total in sync while interval-based snapshotting is enabled + mi_atomic_sub_relaxed(&prof->inuse_bytes, freed_bytes); + } + } +} + +static void mi_cdecl on_snapshot(mi_profiler_t* profiler) { + mi_pprof_profiler_t* prof = downcast(profiler); + mi_pprof_profiler_snapshot(prof); +} + +// Create a new profiler. If `base_file_name` ends with a recognized extension (`.heap` or `.text`), +// that extension selects the text snapshot format and is stripped from the stored base file name +// (the actual snapshot files always get their own `..heap`/`..pb` extension, see `mi_pprof_profiler_snapshot`). +// Any other extension (or none) keeps the default (protobuf) snapshot format and is left untouched. +mi_profiler_t* mi_pprof_profiler_new(size_t initial_threshold, const char* base_file_name, size_t alloc_interval_size, size_t inuse_interval_size, size_t time_interval_secs) { + // heap just for the profiler itself + mi_heap_t* heap = mi_heap_new(); + mi_heap_profile_disable(heap); // don't sample allocations in this heap + + // allocate and initialize the profiler structure from this heap + mi_pprof_profiler_t* prof = mi_heap_zalloc_tp(mi_pprof_profiler_t,heap); + if (prof == NULL) return NULL; + prof->sample_threshold = initial_threshold; + prof->profile_heap = heap; + prof->base_file_name = (base_file_name != NULL ? mi_heap_strndup(heap, base_file_name, 1024) : NULL); + prof->format_text = false; // default: protobuf format, unless overridden by a recognized extension below + if (prof->base_file_name != NULL) { + // only look for a '.' extension after the last path separator (if any) + const char* start = prof->base_file_name; + const char* sep = _mi_strrchr(start, '/'); + if (sep != NULL) start = sep + 1; + sep = _mi_strrchr(start, '\\'); + if (sep != NULL) start = sep + 1; + const char* dot = _mi_strrchr(start, '.'); + if (dot != NULL) { + if (_mi_streq(dot + 1, "heap") || _mi_streq(dot + 1, "text")) { + prof->format_text = true; + } + prof->base_file_name[dot - prof->base_file_name] = 0; // always strip the extension, whether recognized or not + } + } + prof->alloc_interval_size = (alloc_interval_size > (size_t)MI_SSIZE_MAX ? (size_t)MI_SSIZE_MAX : alloc_interval_size); // cap so it always fits in a `mi_ssize_t` + mi_atomic_storess_relaxed(&prof->alloc_interval_countdown, (mi_ssize_t)prof->alloc_interval_size); // 0 if disabled: `on_alloc` never decrements/checks in that case + prof->inuse_interval_size = inuse_interval_size; + mi_atomic_store_relaxed(&prof->inuse_interval_threshold, prof->inuse_interval_size); // 0 if disabled (`inuse_interval_size` defaults to 0): `on_alloc` never checks in that case + prof->time_interval_secs = time_interval_secs; + mi_atomic_storei64_relaxed(&prof->time_interval_deadline, _mi_clock_now() + (mi_msecs_t)prof->time_interval_secs * 1000); // deadline stays unreachable (now, i.e. always due) if disabled, but `on_alloc` never checks in that case + if (!mi_locations_init(heap, &prof->locations)) { + mi_free(prof); + return NULL; + } + prof->profiler.initial_sample_rate = initial_threshold; + prof->profiler.sample_data_size = 2*sizeof(void*); // we store the location and the allocation size in the sample data + prof->profiler.on_alloc = &on_alloc; + prof->profiler.on_free = &on_free; + prof->profiler.on_snapshot = &on_snapshot; + return &prof->profiler; +} + +void mi_pprof_profiler_delete(mi_profiler_t* profiler) { + if (profiler == NULL) return; + mi_pprof_profiler_t* prof = downcast(profiler); + mi_heap_t* heap = prof->profile_heap; + mi_locations_done(heap, &prof->locations); + mi_free(prof->base_file_name); + mi_free(prof); + mi_heap_delete(heap); +} + +// --------------------------------------------------------------------------- +// A basic hash table of locations. All memory (the bucket array, the +// locations, and their callstack frames) is allocated from the profiler's +// own heap so it does not interfere with the profiled allocations. +// Collisions are resolved with simple chaining. +// +// `on_alloc` and `on_free` (and thus `mi_locations_find_or_insert`) can run +// concurrently on multiple threads and are made thread-safe without a lock: +// a bucket is a singly-linked list that only ever grows by prepending a new, +// fully initialized location; once published, a location's identifying +// fields (`hash`, `thread_id`, `callstack`) never change again. Moreover, +// since the hash (and thus the location itself) is derived from the calling +// thread's own id, a location is only ever looked up by the same thread +// that creates it, so there is never a concurrent insert race for the +// same location. +// --------------------------------------------------------------------------- + +// A reasonably large prime bucket count to keep collision chains short +#define MI_LOCATIONS_BUCKET_COUNT (16 * 1024 - 3) /* 16381, prime */ + +// FNV-1a hash, mixed over the thread id and the (precomputed) callstack hash; +#if SIZE_MAX > UINT32_MAX +#define MI_FNV_OFFSET_BASIS ((size_t)0xcbf29ce484222325ULL) // FNV-1a 64-bit offset basis +#define MI_FNV_PRIME ((size_t)0x100000001b3ULL) // FNV-1a 64-bit prime +#else +#define MI_FNV_OFFSET_BASIS ((size_t)0x811c9dc5UL) // FNV-1a 32-bit offset basis +#define MI_FNV_PRIME ((size_t)0x01000193UL) // FNV-1a 32-bit prime +#endif + +static size_t mi_location_hash(mi_threadid_t thread_id, size_t callstack_hash) { + size_t hash = MI_FNV_OFFSET_BASIS; + const size_t prime = MI_FNV_PRIME; + hash = (hash ^ (size_t)thread_id) * prime; + hash = (hash ^ callstack_hash) * prime; + return hash; +} + +// Initialize a location hash table +static bool mi_locations_init(mi_heap_t* heap, mi_locations_t* locations) { + locations->bucket_count = MI_LOCATIONS_BUCKET_COUNT; + locations->buckets = (_Atomic(mi_location_t*)*)mi_heap_zalloc(heap, locations->bucket_count * sizeof(*locations->buckets)); + mi_atomic_store_relaxed(&locations->count, (size_t)0); + return (locations->buckets != NULL); +} + +// Free all memory associated with the location hash table. +// Not thread-safe: assumes no concurrent `on_alloc`/`on_free` (see the note above). +static void mi_locations_done(mi_heap_t* heap, mi_locations_t* locations) { + MI_UNUSED(heap); + if (locations->buckets == NULL) return; + for (size_t i = 0; i < locations->bucket_count; i++) { + mi_location_t* loc = mi_atomic_load_ptr_relaxed(mi_location_t, &locations->buckets[i]); + while (loc != NULL) { + mi_location_t* next = mi_atomic_load_ptr_relaxed(mi_location_t, &loc->next); + if (loc->callstack.frames != NULL) { + mi_free(loc->callstack.frames); + } + mi_free(loc); + loc = next; + } + } + mi_free(locations->buckets); + locations->buckets = NULL; + mi_atomic_store_relaxed(&locations->count, (size_t)0); +} + +// Match a location's identifying fields; these never change after publication. +static bool mi_location_matches(const mi_location_t* loc, size_t hash, mi_threadid_t thread_id, const mi_callstack_t* callstack) { + return (loc->hash == hash && loc->thread_id == thread_id && + loc->callstack.count == callstack->count && + (callstack->count == 0 || mi_memcmp(loc->callstack.frames, callstack->frames, callstack->count * sizeof(void*)) == 0)); +} + +// Find an existing location matching `thread_id` and `callstack`, or insert +// a fresh, zero-initialized one (with `hash` and identifying fields filled in) +// if none exists yet. Returns NULL only on allocation failure. +// Thread-safe (see the note above): the common case (an existing location) +// is a lock-free, read-only traversal using relaxed loads; only inserting a +// genuinely new location uses a CAS (and even then, only ever races against +// other threads inserting *different* locations into the same bucket). +static mi_location_t* mi_locations_find_or_insert(mi_heap_t* heap, mi_locations_t* locations, mi_threadid_t thread_id, const mi_callstack_t* callstack, size_t callstack_hash) { + if (locations->buckets == NULL) return NULL; + const size_t hash = mi_location_hash(thread_id, callstack_hash); + const size_t idx = hash % locations->bucket_count; + _Atomic(mi_location_t*)* bucket = &locations->buckets[idx]; + + // fast path: lock-free, read-only lookup in the (possibly concurrently growing) chain + for (mi_location_t* loc = mi_atomic_load_ptr_relaxed(mi_location_t, bucket); loc != NULL; + loc = mi_atomic_load_ptr_relaxed(mi_location_t, &loc->next)) + { + if (mi_location_matches(loc, hash, thread_id, callstack)) return loc; + } + + // not found: allocate and fully initialize a new location; it is not yet + // visible to any other thread so plain (non-atomic) stores are fine here. + mi_location_t* loc = mi_heap_zalloc_tp(mi_location_t,heap); + if (loc == NULL) return NULL; + if (callstack->count > 0) { + loc->callstack.frames = (void**)mi_heap_mallocn(heap, callstack->count, sizeof(void*)); + if (loc->callstack.frames == NULL) return NULL; + mi_memcpy(loc->callstack.frames, callstack->frames, callstack->count * sizeof(void*)); + } + loc->callstack.count = callstack->count; + loc->thread_id = thread_id; + loc->hash = hash; + + // publish the new location at the head of the bucket with a CAS loop. Since + // a location's key includes the calling thread's own id, no other thread + // can ever race to insert this *same* key -- a CAS can only fail because + // some other thread prepended an unrelated location to this bucket in the + // meantime, so we simply retry with the updated head (no duplicate check + // needed, unlike a typical lock-free insert). + mi_location_t* head = mi_atomic_load_ptr_relaxed(mi_location_t, bucket); + do { + mi_atomic_store_ptr_relaxed(mi_location_t, &loc->next, head); // `loc` not yet published: plain store is fine + } while (!mi_atomic_cas_ptr_weak_release(mi_location_t, bucket, &head, loc)); // `head` is updated to the current value on failure + mi_atomic_increment_relaxed(&locations->count); // just a statistic + return loc; +} + + + +// --------------------------------------------------------------------------- +// Avoid OS specific code when MI_PROFILE_USE_BUILTIN is not enabled +// --------------------------------------------------------------------------- + +#if !MI_PROFILE_USE_BUILTIN + +static size_t mi_prim_backtrace(void** buffer, size_t max_depth, size_t* hash) { + MI_UNUSED(buffer); + MI_UNUSED(max_depth); + *hash = 0; + return 0; +} + +void mi_pprof_profiler_snapshot(mi_pprof_profiler_t* profiler) { + MI_UNUSED(profiler); +} + +#else + +#include // FILE, fopen, fprintf, fclose +#if defined(_WIN32) +#include // _O_CREAT, _O_TRUNC, _O_WRONLY, _O_BINARY, _O_TEXT +#include // _sopen_s, _fdopen, _close +#include // _SH_DENYNO +#include // _S_IREAD, _S_IWRITE +#elif MI_HAS_UNISTDH +#include // open, O_CREAT, O_EXCL, O_NOFOLLOW +#include // close, getuid, geteuid, getgid, getegid, issetugid +#include // S_IRUSR, S_IWUSR +#endif + +// forward declarations; implemented further below. +static void mi_pprof_write_mapped_libraries(FILE* f); +static void mi_pprof_snapshot_text(mi_pprof_profiler_t* prof, const char* fname); +static void mi_pprof_snapshot_proto(mi_pprof_profiler_t* prof, const char* fname); + + +// Detect if we are running with elevated privileges (e.g. a setuid/setgid executable or similar), +// in which case we must not trust `MIMALLOC_PROFILE` environment. +#if defined(_WIN32) +static bool mi_is_elevated_process(void) { + return false; // no direct setuid/setgid equivalent; a UAC elevation does not inherit the parent's environment +} +#elif MI_HAS_UNISTDH && (defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__)) +static bool mi_is_elevated_process(void) { + return (issetugid() != 0); +} +#elif MI_HAS_UNISTDH +static bool mi_is_elevated_process(void) { + return (getuid() != geteuid() || getgid() != getegid()); +} +#else +static bool mi_is_elevated_process(void) { + return false; // no portable way to check without `unistd.h`; assume not elevated +} +#endif + + +// Open a snapshot file for writing, refusing to follow a symlink at `fname` (to avoid an +// attacker redirecting the write to an arbitrary target) and restricting the file's +// permissions to the owner only (snapshots can contain sensitive address/callstack data). +// On platforms without these POSIX/Win32 facilities (e.g. wasm/Emscripten), we fall +// back to a fully portable plain `fopen` (without the symlink/permission hardening). +static FILE* mi_pprof_fopen(const char* fname, bool binary) { + #if defined(_WIN32) + int oflag = _O_CREAT | _O_TRUNC | _O_WRONLY | (binary ? _O_BINARY : _O_TEXT); + int fd = -1; + if (_sopen_s(&fd, fname, oflag, _SH_DENYNO, _S_IREAD | _S_IWRITE) != 0 || fd < 0) return NULL; + FILE* f = _fdopen(fd, binary ? "wb" : "w"); + if (f == NULL) { _close(fd); } + return f; + #elif MI_HAS_UNISTDH + int oflag = O_CREAT | O_TRUNC | O_WRONLY; + #if defined(O_NOFOLLOW) + oflag |= O_NOFOLLOW; // fail (ELOOP) if `fname` is a symlink + #endif + const int fd = open(fname, oflag, S_IRUSR | S_IWUSR); // restrict to owner read/write only + if (fd < 0) return NULL; + FILE* f = fdopen(fd, binary ? "wb" : "w"); + if (f == NULL) { close(fd); } + return f; + #else + return fopen(fname, binary ? "wb" : "w"); // fully portable fallback: no symlink/permission hardening available + #endif +} + +// Write out the current profiler data to a new snapshot file named +// `..` with an incrementing sequence number +// (`.heap` for the text format, mimicking the naming used by the original +// (gperftools) pprof heap profiler; `.pb` for the (uncompressed) protobuf +// format). The format is chosen when the profiler is created (see +// `mi_pprof_profiler_new`). The base file name is also set at creation time; +// if it is NULL, no snapshot is written and this function does nothing. +static void mi_pprof_profiler_snapshot(mi_pprof_profiler_t* prof) { + if (prof == NULL) return; + bool was_running = mi_profiler_stop(&prof->profiler); + mi_locations_t* locations = &prof->locations; + if (locations->buckets != NULL && prof->base_file_name != NULL) { + const size_t seq = ++prof->snapshot_count; + char fname[1024]; + if (!prof->format_text) { + _mi_snprintf(fname, sizeof(fname), "%s.%04zu.pb", prof->base_file_name, seq); + mi_pprof_snapshot_proto(prof, fname); + } + else { + _mi_snprintf(fname, sizeof(fname), "%s.%04zu.heap", prof->base_file_name, seq); + mi_pprof_snapshot_text(prof, fname); + } + _mi_verbose_message("pprof profiler snapshot: %s\n", fname); + } + if (was_running) { mi_profiler_start(&prof->profiler); } +} + +// --------------------------------------------------------------------------- +// Basic backtraces +// --------------------------------------------------------------------------- + +#if MI_HAS_EXECINFOH || MI_HAS_LIBUNWINDH +static size_t mi_backtrace_hash(void** frames, size_t depth) { + size_t hash = MI_FNV_OFFSET_BASIS; + for (size_t i = 0; i < depth; i++) { + hash = (hash ^ (size_t)((uintptr_t)frames[i])) * MI_FNV_PRIME; + } + return hash; +} +#endif + +#if _WIN32 +#include // CaptureStackBackTrace +static mi_decl_noinline size_t mi_prim_backtrace(void** buffer, size_t max_depth, size_t* hash) { + if (max_depth > ULONG_MAX) { max_depth = ULONG_MAX; } + // On Windows XP/Server 2003, `FramesToSkip + FramesToCapture` must be less than 63; + // clamp defensively so we never exceed this, regardless of the actual OS version. + const size_t skip = 1 + MI_BACKTRACE_SKIP_FRAMES; // +1 for `mi_prim_backtrace`'s own frame + const size_t win_limit = 62; // strictly less than 63 + if (skip + max_depth >= win_limit) { max_depth = (skip <= win_limit ? win_limit - skip : 0); } + if (max_depth == 0) { *hash = 0; return 0; } + DWORD win_hash = 0; + const size_t depth = (size_t)CaptureStackBackTrace((ULONG)skip, (ULONG)max_depth, buffer, &win_hash); + *hash = (size_t)win_hash; // use the hash computed by `CaptureStackBackTrace` itself + return depth; +} +#elif MI_HAS_EXECINFOH +#include // backtrace +static mi_decl_noinline size_t mi_prim_backtrace(void** buffer, size_t max_depth, size_t* hash) { + const size_t skip = 1 + MI_BACKTRACE_SKIP_FRAMES; // +1 for `mi_prim_backtrace`'s own frame + if (max_depth <= skip) { *hash = 0; return 0; } + if (max_depth > INT_MAX) { max_depth = INT_MAX; } + const size_t raw_depth = (size_t)backtrace(buffer, (int)(max_depth - skip)); + if (raw_depth <= skip) { *hash = 0; return 0; } + const size_t depth = raw_depth - skip; + memmove(buffer, buffer + skip, depth * sizeof(void*)); + *hash = mi_backtrace_hash(buffer, depth); // no platform-provided hash, compute our own + return depth; +} +#elif MI_HAS_LIBUNWINDH +#define UNW_LOCAL_ONLY +#include // unw_backtrace +static mi_decl_noinline size_t mi_prim_backtrace(void** buffer, size_t max_depth, size_t* hash) { + // used as a fallback on platforms without `execinfo.h` (e.g. musl-based Linux like Alpine) + const size_t skip = 1 + MI_BACKTRACE_SKIP_FRAMES; // +1 for `mi_prim_backtrace`'s own frame + if (max_depth <= skip) { *hash = 0; return 0; } + if (max_depth > INT_MAX) { max_depth = INT_MAX; } + const size_t raw_depth = (size_t)unw_backtrace(buffer, (int)(max_depth - skip)); + if (raw_depth <= skip) { *hash = 0; return 0; } + const size_t depth = raw_depth - skip; + memmove(buffer, buffer + skip, depth * sizeof(void*)); + *hash = mi_backtrace_hash(buffer, depth); // no platform-provided hash, compute our own + return depth; +} +#else +static size_t mi_prim_backtrace(void** buffer, size_t max_depth, size_t* hash) { + MI_UNUSED(buffer); + MI_UNUSED(max_depth); + *hash = 0; + return 0; +} +#endif + + +// --------------------------------------------------------------------------- +// Write out mapped libraries +// --------------------------------------------------------------------------- + +// A loaded module's address range and (if known) its file path; used both to +// print the `MAPPED_LIBRARIES` trailer of the text format and to build the +// `Mapping` entries of the protobuf format (see `mi_pprof_snapshot_proto`). +typedef struct { + uintptr_t start; + uintptr_t end; + char* path; // heap-allocated (owned), or NULL if unknown +} mi_pprof_module_t; + +typedef struct { + mi_pprof_module_t* modules; + size_t count; + size_t capacity; +} mi_pprof_modules_t; + +static void mi_pprof_modules_add(mi_heap_t* heap, mi_pprof_modules_t* mods, uintptr_t start, uintptr_t end, const char* path) { + if (mods->count == mods->capacity) { + const size_t new_capacity = (mods->capacity == 0 ? 16 : mods->capacity * 2); + mods->modules = (mi_pprof_module_t*)mi_heap_realloc(heap, mods->modules, new_capacity * sizeof(*mods->modules)); + mods->capacity = new_capacity; + } + mi_pprof_module_t* m = &mods->modules[mods->count++]; + m->start = start; + m->end = end; + m->path = (path != NULL ? mi_heap_strndup(heap, path, 1024) : NULL); +} + +static void mi_pprof_modules_done(mi_pprof_modules_t* mods) { + for (size_t i = 0; i < mods->count; i++) { mi_free(mods->modules[i].path); } + mi_free(mods->modules); + mods->modules = NULL; + mods->count = 0; + mods->capacity = 0; +} + +// On Linux, `pprof` can symbolize addresses using the `MAPPED_LIBRARIES` trailer +// which is just a copy of `/proc/self/maps`. On macOS there is no equivalent +// file so we reconstruct a similarly formatted line (`start-end perm ... path`) +// per loaded Mach-O image using the dyld APIs: the range is taken from the +// image's `__TEXT` segment, which is also the "load address" that tools like +// `atos` use to symbolize addresses (`atos -o -l `). On +// Windows we walk the process address space with `VirtualQuery` +// On other platforms we leave this section empty (`pprof` can still work +// without it if not using PIE, or when combined with tools like `atos` / +// `addr2line` / a symbol server). + +// A small wrapper that formats with our own minimal `_mi_vsnprintf` (so call +// sites can use its `%l8x`/`%l8d`/`%tx` extensions instead of 's +// `PRIx64`/`PRIu64`/`PRIxPTR` macros) and writes the result directly to a `FILE*`. +// (Distinct from the existing `_mi_fprintf(mi_output_fun*, void*, ...)` in +// `util/options.c`, which targets mimalloc's redirectable output callback instead +// of an arbitrary `FILE*`.) +static void mi_fprintf(FILE* f, const char* fmt, ...) { + char buf[512]; + va_list args; + va_start(args, fmt); + _mi_vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + fputs(buf, f); +} + +#if defined(_WIN32) +#include + +// Walk the process address space, calling `on_module(ctx, base, end, path)` +// once for each loaded module. Consecutive `MEM_IMAGE` regions that belong to +// the same module (same allocation base) are merged into a single range. +static void mi_walk_modules_win32(void (*on_module)(void* ctx, uintptr_t base, uintptr_t end, const char* path), void* ctx) { + SYSTEM_INFO si; + GetSystemInfo(&si); + uint8_t* addr = (uint8_t*)si.lpMinimumApplicationAddress; + uint8_t* const addr_max = (uint8_t*)si.lpMaximumApplicationAddress; + void* cur_base = NULL; + uintptr_t cur_end = 0; + while (addr < addr_max) { + MEMORY_BASIC_INFORMATION mbi; + const SIZE_T n = VirtualQuery(addr, &mbi, sizeof(mbi)); + if (n == 0) break; // no more (queryable) regions + if (mbi.Type == MEM_IMAGE && mbi.State != MEM_FREE) { + if (mbi.AllocationBase == cur_base) { + // extend the current module's range + cur_end = (uintptr_t)mbi.BaseAddress + mbi.RegionSize; + } + else { + // a new module starts: flush the previous one first + if (cur_base != NULL) { + char path[MAX_PATH]; + const DWORD len = GetModuleFileNameA((HMODULE)cur_base, path, (DWORD)sizeof(path)); + on_module(ctx, (uintptr_t)cur_base, cur_end, (len > 0 ? path : "")); + } + cur_base = mbi.AllocationBase; + cur_end = (uintptr_t)mbi.BaseAddress + mbi.RegionSize; + } + } + else if (cur_base != NULL) { + // the module's regions ended: flush it + char path[MAX_PATH]; + const DWORD len = GetModuleFileNameA((HMODULE)cur_base, path, (DWORD)sizeof(path)); + on_module(ctx, (uintptr_t)cur_base, cur_end, (len > 0 ? path : "")); + cur_base = NULL; + } + const uint8_t* next = (const uint8_t*)mbi.BaseAddress + mbi.RegionSize; + if (next <= addr) break; // guard against a non-advancing region + addr = (uint8_t*)next; + } + if (cur_base != NULL) { + char path[MAX_PATH]; + const DWORD len = GetModuleFileNameA((HMODULE)cur_base, path, (DWORD)sizeof(path)); + on_module(ctx, (uintptr_t)cur_base, cur_end, (len > 0 ? path : "")); + } +} + +static void mi_write_mapped_libraries_on_module_win32(void* ctx, uintptr_t base, uintptr_t end, const char* path) { + FILE* f = (FILE*)ctx; + mi_fprintf(f, "%tx-%tx r-xp 00000000 00:00 0 %s\n", base, end, path); +} + +static void mi_write_mapped_libraries_win32(FILE* f) { + mi_walk_modules_win32(&mi_write_mapped_libraries_on_module_win32, f); +} + +typedef struct { mi_heap_t* heap; mi_pprof_modules_t* mods; } mi_pprof_collect_modules_win32_ctx_t; + +static void mi_pprof_collect_modules_win32_on_module(void* ctx, uintptr_t base, uintptr_t end, const char* path) { + mi_pprof_collect_modules_win32_ctx_t* c = (mi_pprof_collect_modules_win32_ctx_t*)ctx; + mi_pprof_modules_add(c->heap, c->mods, base, end, (path[0] != 0 ? path : NULL)); +} + +static void mi_pprof_collect_modules_win32(mi_heap_t* heap, mi_pprof_modules_t* mods) { + mi_pprof_collect_modules_win32_ctx_t ctx = { heap, mods }; + mi_walk_modules_win32(&mi_pprof_collect_modules_win32_on_module, &ctx); +} + +#elif defined(__APPLE__) +#include +#include + +// Find the (slid) virtual address range of the `__TEXT` segment of a loaded +// Mach-O image; this is the image's load address as used for symbolication. +static bool mi_macho_image_range(const struct mach_header* mh, intptr_t slide, uintptr_t* start, uintptr_t* end) { + const bool is64 = (mh->magic == MH_MAGIC_64 || mh->magic == MH_CIGAM_64); + const uint8_t* cmd_ptr = (const uint8_t*)mh + (is64 ? sizeof(struct mach_header_64) : sizeof(struct mach_header)); + for (uint32_t c = 0; c < mh->ncmds; c++) { + const struct load_command* lc = (const struct load_command*)cmd_ptr; + if (is64 && lc->cmd == LC_SEGMENT_64) { + const struct segment_command_64* seg = (const struct segment_command_64*)lc; + if (_mi_streq(seg->segname, SEG_TEXT)) { + *start = (uintptr_t)seg->vmaddr + (uintptr_t)slide; + *end = *start + (uintptr_t)seg->vmsize; + return true; + } + } + else if (!is64 && lc->cmd == LC_SEGMENT) { + const struct segment_command* seg = (const struct segment_command*)lc; + if (_mi_streq(seg->segname, SEG_TEXT)) { + *start = (uintptr_t)seg->vmaddr + (uintptr_t)slide; + *end = *start + (uintptr_t)seg->vmsize; + return true; + } + } + cmd_ptr += lc->cmdsize; + } + return false; // no __TEXT segment found +} + +static void mi_write_mapped_libraries_macos(FILE* f) { + const uint32_t count = _dyld_image_count(); + for (uint32_t i = 0; i < count; i++) { + const struct mach_header* mh = _dyld_get_image_header(i); + const char* name = _dyld_get_image_name(i); + if (mh == NULL || name == NULL) continue; + const intptr_t slide = _dyld_get_image_vmaddr_slide(i); + uintptr_t start = 0; + uintptr_t end = 0; + if (!mi_macho_image_range(mh, slide, &start, &end)) continue; + mi_fprintf(f, "%tx-%tx r-xp 00000000 00:00 0 %s\n", start, end, name); + } +} + +static void mi_pprof_collect_modules_macos(mi_heap_t* heap, mi_pprof_modules_t* mods) { + const uint32_t count = _dyld_image_count(); + for (uint32_t i = 0; i < count; i++) { + const struct mach_header* mh = _dyld_get_image_header(i); + const char* name = _dyld_get_image_name(i); + if (mh == NULL || name == NULL) continue; + const intptr_t slide = _dyld_get_image_vmaddr_slide(i); + uintptr_t start = 0; + uintptr_t end = 0; + if (!mi_macho_image_range(mh, slide, &start, &end)) continue; + mi_pprof_modules_add(heap, mods, start, end, name); + } +} + +#elif defined(__linux__) +#include // strtoull + +// Copy `/proc/self/maps` verbatim; this is exactly the format `pprof` expects. +static void mi_write_mapped_libraries_linux(FILE* f) { + FILE* maps = fopen("/proc/self/maps", "r"); + if (maps != NULL) { + char line[512]; + while (fgets(line, sizeof(line), maps) != NULL) { + fputs(line, f); + } + fclose(maps); + } +} + +// Parse `/proc/self/maps` lines of the form +// `- ` (path may be absent +// for anonymous mappings, which are skipped here since they are not useful +// for symbolization). +static void mi_pprof_collect_modules_linux(mi_heap_t* heap, mi_pprof_modules_t* mods) { + FILE* maps = fopen("/proc/self/maps", "r"); + if (maps == NULL) return; + char line[512]; + while (fgets(line, sizeof(line), maps) != NULL) { + char* end_ptr = NULL; + const uintptr_t start = (uintptr_t)strtoull(line, &end_ptr, 16); + if (end_ptr == NULL || *end_ptr != '-') continue; + const uintptr_t end = (uintptr_t)strtoull(end_ptr + 1, &end_ptr, 16); + // the path (if present) is the last whitespace-separated field on the line + const char* path = strrchr(line, ' '); + if (path == NULL) continue; + while (*path == ' ') path++; + size_t len = strlen(path); + while (len > 0 && (path[len-1] == '\n' || path[len-1] == '\r')) { len--; } + if (len == 0 || path[0] == '[') continue; // skip anonymous mappings like `[heap]`, `[stack]` + char pathbuf[512]; + if (len >= sizeof(pathbuf)) { len = sizeof(pathbuf) - 1; } + memcpy(pathbuf, path, len); + pathbuf[len] = 0; + mi_pprof_modules_add(heap, mods, start, end, pathbuf); + } + fclose(maps); +} + +#elif defined(__FreeBSD__) || defined(__DragonFly__) +#include // struct kinfo_vmentry +#include // kinfo_getvmmap +#include // getpid +#include // free + +// FreeBSD (and DragonFly, which forked from FreeBSD) do not reliably provide Linux's +// `/proc/self/maps` (procfs is optional and rarely mounted by default); use the native +// `kinfo_getvmmap` API instead (declared in , requires linking `-lutil`) to +// enumerate the process's virtual memory mappings. +static void mi_write_mapped_libraries_bsd(FILE* f) { + int cnt = 0; + struct kinfo_vmentry* vmmap = kinfo_getvmmap(getpid(), &cnt); + if (vmmap == NULL) return; + for (int i = 0; i < cnt; i++) { + const struct kinfo_vmentry* kve = &vmmap[i]; + if (kve->kve_path[0] == 0) continue; // skip anonymous mappings + mi_fprintf(f, "%l8x-%l8x r-xp 00000000 00:00 0 %s\n", (uint64_t)kve->kve_start, (uint64_t)kve->kve_end, kve->kve_path); + } + free(vmmap); +} + +static void mi_pprof_collect_modules_bsd(mi_heap_t* heap, mi_pprof_modules_t* mods) { + int cnt = 0; + struct kinfo_vmentry* vmmap = kinfo_getvmmap(getpid(), &cnt); + if (vmmap == NULL) return; + for (int i = 0; i < cnt; i++) { + const struct kinfo_vmentry* kve = &vmmap[i]; + if (kve->kve_path[0] == 0) continue; // skip anonymous mappings + mi_pprof_modules_add(heap, mods, (uintptr_t)kve->kve_start, (uintptr_t)kve->kve_end, kve->kve_path); + } + free(vmmap); +} +#endif + +static void mi_pprof_collect_modules(mi_heap_t* heap, mi_pprof_modules_t* mods) { + mods->modules = NULL; + mods->count = 0; + mods->capacity = 0; + #if defined(__linux__) + mi_pprof_collect_modules_linux(heap, mods); + #elif defined(_WIN32) + mi_pprof_collect_modules_win32(heap, mods); + #elif defined(__APPLE__) + mi_pprof_collect_modules_macos(heap, mods); + #elif defined(__FreeBSD__) || defined(__DragonFly__) + mi_pprof_collect_modules_bsd(heap, mods); + #else + MI_UNUSED(heap); + #endif +} + +static void mi_pprof_write_mapped_libraries(FILE* f) { + fprintf(f, "MAPPED_LIBRARIES:\n"); + #if defined(__linux__) + mi_write_mapped_libraries_linux(f); + #elif defined(_WIN32) + mi_write_mapped_libraries_win32(f); + #elif defined(__APPLE__) + mi_write_mapped_libraries_macos(f); + #elif defined(__FreeBSD__) || defined(__DragonFly__) + mi_write_mapped_libraries_bsd(f); + #endif +} + + +// --------------------------------------------------------------------------- +// Writing the profile in the (original, textual) pprof heap profile format +// --------------------------------------------------------------------------- + +// Write out the current profiler data in the pprof heap profile text format: +// a header line with the totals, followed by one line per unique call +// location, and a trailer with the mapped libraries (used by `pprof` to +// symbolize the addresses). +static void mi_pprof_snapshot_text(mi_pprof_profiler_t* prof, const char* fname) { + mi_locations_t* locations = &prof->locations; + FILE* f = mi_pprof_fopen(fname, false); + if (f == NULL) return; + + // first pass: compute the totals over all locations + // (snapshot is assumed to run single-threaded while sampling is stopped, so + // `alloc_count`/`alloc_bytes` can be read directly; `inuse_count`/`inuse_bytes` + // are still `_Atomic` fields so we use a relaxed load for those) + uint64_t total_inuse_objects = 0; + uint64_t total_inuse_bytes = 0; + uint64_t total_alloc_objects = 0; + uint64_t total_alloc_bytes = 0; + for (size_t i = 0; i < locations->bucket_count; i++) { + for (mi_location_t* loc = mi_atomic_load_ptr_relaxed(mi_location_t, &locations->buckets[i]); loc != NULL; loc = mi_atomic_load_ptr_relaxed(mi_location_t, &loc->next)) { + total_inuse_objects += (uint64_t)mi_atomic_load_relaxed(&loc->inuse_count); + total_inuse_bytes += (uint64_t)mi_atomic_load_relaxed(&loc->inuse_bytes); + total_alloc_objects += (uint64_t)loc->alloc_count; + total_alloc_bytes += (uint64_t)loc->alloc_bytes; + } + } + + mi_fprintf(f, "heap profile: %6l8d: %8l8d [%6l8d: %8l8d] @ heapprofile\n", + total_inuse_objects, total_inuse_bytes, total_alloc_objects, total_alloc_bytes); + + // second pass: write one line per unique call location + for (size_t i = 0; i < locations->bucket_count; i++) { + for (mi_location_t* loc = mi_atomic_load_ptr_relaxed(mi_location_t, &locations->buckets[i]); loc != NULL; loc = mi_atomic_load_ptr_relaxed(mi_location_t, &loc->next)) { + const size_t inuse_count = mi_atomic_load_relaxed(&loc->inuse_count); + const size_t inuse_bytes = mi_atomic_load_relaxed(&loc->inuse_bytes); + mi_fprintf(f, "%6l8d: %8l8d [%6l8d: %8l8d] @", + (uint64_t)inuse_count, (uint64_t)inuse_bytes, + (uint64_t)loc->alloc_count, (uint64_t)loc->alloc_bytes); + for (size_t j = 0; j < loc->callstack.count; j++) { + mi_fprintf(f, " 0x%tx", (uintptr_t)loc->callstack.frames[j]); + } + fprintf(f, "\n"); + } + } + + mi_pprof_write_mapped_libraries(f); + fclose(f); +} + + +// --------------------------------------------------------------------------- +// Writing the profile in the modern `pprof` protobuf format +// (`perftools.profiles.Profile`, see +// https://github.com/google/pprof/blob/main/proto/profile.proto) +// +// On-disk, `pprof` profiles are normally gzip-compressed, but `pprof` also +// accepts a *raw* (uncompressed) serialized `Profile` message: it only tries +// to gunzip the input if it starts with the gzip magic bytes, and otherwise +// parses it directly as a protobuf message. We take advantage of that so we +// don't need to implement a deflate/gzip writer. +// +// We still need the locations hash table for this format: samples are +// aggregated per call site (thread + callstack), exactly as for the text +// format, rather than emitting one `Sample` per individual allocation. What +// changes is the *representation* of a callstack: instead of a flat list of +// hex addresses embedded directly in a text line, each frame address becomes +// its own (deduplicated) `Location` entry, referenced by id from `Sample. +// location_id`. The originating thread id -- which the text format doesn't +// record explicitly (it is only implicit in which addresses got sampled) -- +// is attached to each `Sample` via a numeric `Label` (pprof has no dedicated +// thread-id field, but labels are the generic mechanism for this). +// --------------------------------------------------------------------------- + +// A simple growable byte buffer, allocated from the profiler's own heap. +typedef struct { + mi_heap_t* heap; + uint8_t* data; + size_t size; + size_t capacity; +} mi_pb_buf_t; + +static void mi_pb_init(mi_pb_buf_t* b, mi_heap_t* heap) { + b->heap = heap; + b->data = NULL; + b->size = 0; + b->capacity = 0; +} + +static void mi_pb_done(mi_pb_buf_t* b) { + mi_free(b->data); + b->data = NULL; + b->size = 0; + b->capacity = 0; +} + +static void mi_pb_reserve(mi_pb_buf_t* b, size_t extra) { + if (b->size + extra <= b->capacity) return; + size_t new_capacity = (b->capacity == 0 ? 4096 : b->capacity * 2); + while (new_capacity < b->size + extra) { new_capacity *= 2; } + b->data = (uint8_t*)mi_heap_realloc(b->heap, b->data, new_capacity); + b->capacity = new_capacity; +} + +static void mi_pb_bytes(mi_pb_buf_t* b, const void* p, size_t n) { + mi_pb_reserve(b, n); + memcpy(b->data + b->size, p, n); + b->size += n; +} + +static void mi_pb_byte(mi_pb_buf_t* b, uint8_t byte) { + mi_pb_bytes(b, &byte, 1); +} + +// Unsigned LEB128 (protobuf "varint") encoding. +static void mi_pb_varint(mi_pb_buf_t* b, uint64_t v) { + do { + uint8_t byte = (uint8_t)(v & 0x7F); + v >>= 7; + if (v != 0) { byte |= 0x80; } + mi_pb_byte(b, byte); + } while (v != 0); +} + +static void mi_pb_tag(mi_pb_buf_t* b, uint32_t field, uint32_t wiretype) { + mi_pb_varint(b, ((uint64_t)field << 3) | wiretype); +} + +// Scalar varint fields; proto3 encoders may omit fields set to their default +// (0/false) value, which we do here to keep the output compact. +static void mi_pb_uint64_field(mi_pb_buf_t* b, uint32_t field, uint64_t v) { + if (v == 0) return; + mi_pb_tag(b, field, 0); + mi_pb_varint(b, v); +} + +static void mi_pb_int64_field(mi_pb_buf_t* b, uint32_t field, int64_t v) { + if (v == 0) return; + mi_pb_tag(b, field, 0); + mi_pb_varint(b, (uint64_t)v); // proto3 plain int64 is encoded as a varint of its 2's complement bit pattern +} + +// Length-delimited fields (string/bytes/embedded message) are always emitted, +// even when empty: this matters for `string_table[0]` (which must be the +// empty string) and for repeated message fields (an empty message is still a +// valid, distinct list entry). +static void mi_pb_len_field(mi_pb_buf_t* b, uint32_t field, const void* data, size_t len) { + mi_pb_tag(b, field, 2); + mi_pb_varint(b, len); + mi_pb_bytes(b, data, len); +} + +static void mi_pb_message_field(mi_pb_buf_t* b, uint32_t field, const mi_pb_buf_t* msg) { + mi_pb_len_field(b, field, msg->data, msg->size); +} + +// A packed repeated scalar field (used for `Sample.location_id`/`Sample.value`): +// all elements are varint-encoded and concatenated into one length-delimited blob. +static void mi_pb_packed_uint64_field(mi_pb_buf_t* b, uint32_t field, const uint64_t* values, size_t count) { + if (count == 0) return; + mi_pb_buf_t tmp; + mi_pb_init(&tmp, b->heap); + for (size_t i = 0; i < count; i++) { mi_pb_varint(&tmp, values[i]); } + mi_pb_message_field(b, field, &tmp); + mi_pb_done(&tmp); +} + +static void mi_pb_packed_int64_field(mi_pb_buf_t* b, uint32_t field, const int64_t* values, size_t count) { + mi_pb_packed_uint64_field(b, field, (const uint64_t*)values, count); // same varint bit pattern +} + +// --------------------------------------------------------------------------- +// String table: dedups strings into `Profile.string_table` (field 6) and +// hands back the (1-based; index 0 is reserved for "") index of each string. +// A linear search is fine here: a profile typically only has a handful to a +// few hundred distinct strings (sample type names, mapped file paths). +// --------------------------------------------------------------------------- + +typedef struct { + char* str; + int64_t index; +} mi_pb_string_entry_t; + +typedef struct { + mi_heap_t* heap; + mi_pb_string_entry_t* entries; + size_t count; + size_t capacity; + mi_pb_buf_t table; // the serialized `string_table` entries (Profile field 6) +} mi_pb_strings_t; + +static void mi_pb_strings_init(mi_pb_strings_t* strings, mi_heap_t* heap) { + strings->heap = heap; + strings->entries = NULL; + strings->count = 0; + strings->capacity = 0; + mi_pb_init(&strings->table, heap); + mi_pb_len_field(&strings->table, 6, "", 0); // index 0 must always be "" +} + +static void mi_pb_strings_done(mi_pb_strings_t* strings) { + for (size_t i = 0; i < strings->count; i++) { mi_free(strings->entries[i].str); } + mi_free(strings->entries); + mi_pb_done(&strings->table); +} + +static int64_t mi_pb_strings_intern(mi_pb_strings_t* strings, const char* s) { + if (s == NULL) { s = ""; } + for (size_t i = 0; i < strings->count; i++) { + if (_mi_streq(strings->entries[i].str, s)) { return strings->entries[i].index; } + } + if (strings->count == strings->capacity) { + const size_t new_capacity = (strings->capacity == 0 ? 32 : strings->capacity * 2); + strings->entries = (mi_pb_string_entry_t*)mi_heap_realloc(strings->heap, strings->entries, new_capacity * sizeof(*strings->entries)); + strings->capacity = new_capacity; + } + const int64_t index = (int64_t)(strings->count + 1); // index 0 is reserved for "" + strings->entries[strings->count].str = mi_heap_strndup(strings->heap, s, 1024); + strings->entries[strings->count].index = index; + strings->count++; + mi_pb_len_field(&strings->table, 6, s, strlen(s)); + return index; +} + +// --------------------------------------------------------------------------- +// A small single-threaded hash map from a raw frame address to the id of its +// (deduplicated) `Location` entry. Only ever built and consulted from within +// `mi_pprof_snapshot_proto`, so (unlike the locations hash table) it needs no +// atomics at all. +// --------------------------------------------------------------------------- + +typedef struct mi_pb_addr_entry_s { + struct mi_pb_addr_entry_s* next; + uintptr_t addr; + uint64_t id; +} mi_pb_addr_entry_t; + +typedef struct { + mi_heap_t* heap; + size_t bucket_count; + mi_pb_addr_entry_t** buckets; +} mi_pb_addr_map_t; + +static void mi_pb_addr_map_init(mi_pb_addr_map_t* map, mi_heap_t* heap, size_t bucket_count) { + map->heap = heap; + map->bucket_count = (bucket_count == 0 ? 1 : bucket_count); + map->buckets = (mi_pb_addr_entry_t**)mi_heap_zalloc(heap, map->bucket_count * sizeof(*map->buckets)); +} + +static void mi_pb_addr_map_done(mi_pb_addr_map_t* map) { + if (map->buckets == NULL) return; + for (size_t i = 0; i < map->bucket_count; i++) { + mi_pb_addr_entry_t* e = map->buckets[i]; + while (e != NULL) { + mi_pb_addr_entry_t* const next = e->next; + mi_free(e); + e = next; + } + } + mi_free(map->buckets); + map->buckets = NULL; +} + +// Returns the existing location id for `addr`, or assigns it `next_id` and +// returns that (setting `*is_new` to true) if this is the first time `addr` +// is seen. +static uint64_t mi_pb_addr_map_find_or_insert(mi_pb_addr_map_t* map, uintptr_t addr, uint64_t next_id, bool* is_new) { + if (map->buckets == NULL) { *is_new = false; return 0; } + const size_t idx = (size_t)(addr ^ (addr >> 16)) % map->bucket_count; + for (mi_pb_addr_entry_t* e = map->buckets[idx]; e != NULL; e = e->next) { + if (e->addr == addr) { *is_new = false; return e->id; } + } + mi_pb_addr_entry_t* e = mi_heap_malloc_tp(mi_pb_addr_entry_t, map->heap); + e->addr = addr; + e->id = next_id; + e->next = map->buckets[idx]; + map->buckets[idx] = e; + *is_new = true; + return next_id; +} + +// Find the (1-based) `Mapping` id of the module containing `addr`, or 0 if +// `addr` doesn't fall within any collected module range. +static uint64_t mi_pprof_find_mapping_id(const mi_pprof_modules_t* mods, uintptr_t addr) { + for (size_t i = 0; i < mods->count; i++) { + if (addr >= mods->modules[i].start && addr < mods->modules[i].end) { return (uint64_t)(i + 1); } + } + return 0; +} + +// Write out the current profiler data as a raw (uncompressed) `perftools. +// profiles.Profile` protobuf message (see the file-level comment above). +static void mi_pprof_snapshot_proto(mi_pprof_profiler_t* prof, const char* fname) { + mi_heap_t* heap = prof->profile_heap; + mi_locations_t* locations = &prof->locations; + + mi_pb_buf_t profile; + mi_pb_init(&profile, heap); + mi_pb_strings_t strings; + mi_pb_strings_init(&strings, heap); + + const int64_t str_count = mi_pb_strings_intern(&strings, "count"); + const int64_t str_bytes = mi_pb_strings_intern(&strings, "bytes"); + const int64_t str_alloc_objects = mi_pb_strings_intern(&strings, "alloc_objects"); + const int64_t str_alloc_bytes = mi_pb_strings_intern(&strings, "alloc_bytes"); + const int64_t str_inuse_objects = mi_pb_strings_intern(&strings, "inuse_objects"); + const int64_t str_inuse_bytes = mi_pb_strings_intern(&strings, "inuse_bytes"); + const int64_t str_space = mi_pb_strings_intern(&strings, "space"); + const int64_t str_thread = mi_pb_strings_intern(&strings, "thread"); + + // Profile.sample_type = 1: [alloc_objects:count, alloc_bytes:bytes, inuse_objects:count, inuse_bytes:bytes] + { + const int64_t types[4] = { str_alloc_objects, str_alloc_bytes, str_inuse_objects, str_inuse_bytes }; + const int64_t units[4] = { str_count, str_bytes, str_count, str_bytes }; + for (size_t i = 0; i < 4; i++) { + mi_pb_buf_t vt; + mi_pb_init(&vt, heap); + mi_pb_int64_field(&vt, 1, types[i]); + mi_pb_int64_field(&vt, 2, units[i]); + mi_pb_message_field(&profile, 1, &vt); + mi_pb_done(&vt); + } + } + + // Profile.mapping = 3: the modules loaded into this process, so `pprof` can + // symbolize the (raw) addresses recorded in each `Location` below. + mi_pprof_modules_t mods; + mi_pprof_collect_modules(heap, &mods); + for (size_t i = 0; i < mods.count; i++) { + const mi_pprof_module_t* m = &mods.modules[i]; + mi_pb_buf_t mapping; + mi_pb_init(&mapping, heap); + mi_pb_uint64_field(&mapping, 1, (uint64_t)(i + 1)); // id + mi_pb_uint64_field(&mapping, 2, (uint64_t)m->start); // memory_start + mi_pb_uint64_field(&mapping, 3, (uint64_t)m->end); // memory_limit + if (m->path != NULL) { + mi_pb_int64_field(&mapping, 5, mi_pb_strings_intern(&strings, m->path)); // filename + } + mi_pb_message_field(&profile, 3, &mapping); + mi_pb_done(&mapping); + } + + // Profile.location = 4 and Profile.sample = 2: one (deduplicated) `Location` + // per unique frame address, and one `Sample` per unique call site (i.e. one + // per entry in our own locations hash table), exactly mirroring the text + // format's aggregation. + mi_pb_addr_map_t addr_map; + mi_pb_addr_map_init(&addr_map, heap, locations->bucket_count); + uint64_t next_location_id = 1; + + for (size_t i = 0; i < locations->bucket_count; i++) { + for (mi_location_t* loc = mi_atomic_load_ptr_relaxed(mi_location_t, &locations->buckets[i]); loc != NULL; loc = mi_atomic_load_ptr_relaxed(mi_location_t, &loc->next)) { + const size_t inuse_count = mi_atomic_load_relaxed(&loc->inuse_count); + const size_t inuse_bytes = mi_atomic_load_relaxed(&loc->inuse_bytes); + + const size_t frame_count = (loc->callstack.count > MI_MAX_BACKTRACE_DEPTH ? MI_MAX_BACKTRACE_DEPTH : loc->callstack.count); + uint64_t location_ids[MI_MAX_BACKTRACE_DEPTH]; + for (size_t j = 0; j < frame_count; j++) { + const uintptr_t addr = (uintptr_t)loc->callstack.frames[j]; + bool is_new = false; + const uint64_t id = mi_pb_addr_map_find_or_insert(&addr_map, addr, next_location_id, &is_new); + location_ids[j] = id; + if (is_new) { + next_location_id++; + mi_pb_buf_t location; + mi_pb_init(&location, heap); + mi_pb_uint64_field(&location, 1, id); // id + mi_pb_uint64_field(&location, 2, mi_pprof_find_mapping_id(&mods, addr)); // mapping_id + mi_pb_uint64_field(&location, 3, (uint64_t)addr); // address + mi_pb_message_field(&profile, 4, &location); + mi_pb_done(&location); + } + } + + mi_pb_buf_t sample; + mi_pb_init(&sample, heap); + mi_pb_packed_uint64_field(&sample, 1, location_ids, frame_count); // location_id (leaf-first, matching our capture order) + const int64_t values[4] = { + loc->alloc_count, loc->alloc_bytes, + (int64_t)inuse_count, (int64_t)inuse_bytes + }; + mi_pb_packed_int64_field(&sample, 2, values, 4); // value + { + // attach the originating thread id as a numeric label: pprof has no + // dedicated thread-id field, but `Sample.label` is the generic + // mechanism for this kind of per-sample metadata. + mi_pb_buf_t label; + mi_pb_init(&label, heap); + mi_pb_int64_field(&label, 1, str_thread); // key + mi_pb_int64_field(&label, 3, (int64_t)loc->thread_id); // num + mi_pb_message_field(&sample, 3, &label); + mi_pb_done(&label); + } + mi_pb_message_field(&profile, 2, &sample); + mi_pb_done(&sample); + } + } + + mi_pb_addr_map_done(&addr_map); + mi_pprof_modules_done(&mods); + + // Profile.period_type = 11 / Profile.period = 12: matches the convention + // used by Go's heap profiles ("space"/"bytes", sampling period in bytes). + { + mi_pb_buf_t period_type; + mi_pb_init(&period_type, heap); + mi_pb_int64_field(&period_type, 1, str_space); + mi_pb_int64_field(&period_type, 2, str_bytes); + mi_pb_message_field(&profile, 11, &period_type); + mi_pb_done(&period_type); + } + mi_pb_int64_field(&profile, 12, (int64_t)prof->sample_threshold); // period + mi_pb_int64_field(&profile, 9, (int64_t)_mi_clock_now() * 1000); // clock_now is in milli-secs + + // the string table (Profile.string_table = 6) can only be finalized now, + // since strings were interned into it while building the message above. + mi_pb_bytes(&profile, strings.table.data, strings.table.size); + mi_pb_strings_done(&strings); + + FILE* f = mi_pprof_fopen(fname, true); + if (f != NULL) { + fwrite(profile.data, 1, profile.size, f); + fclose(f); + } + mi_pb_done(&profile); +} + +#endif // MI_PROFILE diff --git a/Objects/mimalloc/profile/readme.md b/Objects/mimalloc/profile/readme.md new file mode 100644 index 000000000000000..581a5f58ab1a891 --- /dev/null +++ b/Objects/mimalloc/profile/readme.md @@ -0,0 +1,5 @@ +# Allocation Sampling and Profiling + +- [`sample-profile.c`](sample-profile.c): Allocation sampling, profiler callbacks. +- [`sample-guarded.c`](sample-guarded.c): Sampled allocations protected by guard pages. +- [`pprof.c`](pprof.c): Default heap profiler implementation with stack traces and pprof-compatible text or protobuf snapshots. diff --git a/Objects/mimalloc/profile/sample-guarded.c b/Objects/mimalloc/profile/sample-guarded.c new file mode 100644 index 000000000000000..1f7af8ba6196a38 --- /dev/null +++ b/Objects/mimalloc/profile/sample-guarded.c @@ -0,0 +1,176 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2019-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#include "mimalloc.h" +#include "mimalloc/prim/prim-tls.h" +#include "mimalloc/util/stats.h" +#include "mimalloc/alloc/page.h" +#include "mimalloc/profile/sample-guarded.h" + +mi_decl_export void mi_theap_guarded_set_sample_rate(mi_theap_t* theap, size_t sample_rate, size_t seed) { + theap->guarded_sample_rate = (sample_rate > MI_SAMPLE_RATE_MAX ? MI_SAMPLE_RATE_MAX : sample_rate); + if (theap->guarded_sample_rate > 1) { + if (seed == 0) { + seed = _mi_theap_random_next(theap); + } + theap->guarded_sample_countdown = (seed % theap->guarded_sample_rate) + 1; // start at random count between 1 and `sample_rate` + } + else if (theap->guarded_sample_countdown > theap->guarded_sample_rate) { + theap->guarded_sample_countdown = theap->guarded_sample_rate; + } + _mi_theap_update_sample_rate(theap); +} + +mi_decl_export void mi_theap_guarded_set_size_bound(mi_theap_t* theap, size_t min, size_t max) { + theap->guarded_size_min = min; + theap->guarded_size_max = (min > max ? min : max); +} + +void _mi_theap_guarded_init(mi_theap_t* theap) { + #if MI_GUARDED + mi_theap_guarded_set_sample_rate(theap, + (size_t)mi_option_get_clamp(mi_option_guarded_sample_rate, 0, LONG_MAX), + (size_t)mi_option_get(mi_option_guarded_sample_seed)); + mi_theap_guarded_set_size_bound(theap, + (size_t)mi_option_get_clamp(mi_option_guarded_min, 0, LONG_MAX), + (size_t)mi_option_get_clamp(mi_option_guarded_max, 0, LONG_MAX) ); + #else + mi_theap_guarded_set_sample_rate(theap,0,0); + #endif +} + + +// We always allocate a guarded allocation at an offset (`mi_page_has_interior_pointers` will be true). +// We then set the first word of the block to `0` for regular offset aligned allocations (in `alloc/alloc-aligned.c`) +// and the first word to `~0` for guarded allocations to have a correct `mi_usable_size` +static void* mi_block_ptr_set_guarded(mi_block_t* block, size_t obj_size, size_t* usable_size) { + // todo: we can still make padding work by moving it out of the guard page area + mi_page_t* const page = mi_ptr_page(block); + mi_page_set_has_interior_pointers(page, true); + block->next = MI_BLOCK_TAG_GUARDED; + + // set guard page at the end of the block + const size_t block_size = mi_page_block_size(page); // must use `block_size` to match `mi_free_local` + const size_t os_page_size = _mi_os_page_size(); + mi_assert_internal(block_size >= obj_size + os_page_size + sizeof(mi_block_t)); + if (block_size < obj_size + os_page_size + sizeof(mi_block_t)) { + // should never happen + mi_free(block); + return NULL; + } + uint8_t* guard_page = (uint8_t*)block + block_size - os_page_size; + // note: the alignment of the guard page relies on blocks being os_page_size aligned which + // is ensured in `mi_arena_page_alloc_fresh`. + mi_assert_internal(mi_is_aligned(block, os_page_size)); + mi_assert_internal(mi_is_aligned(guard_page, os_page_size)); + if (!page->memid.is_pinned && mi_is_aligned(guard_page, os_page_size)) { + const bool ok = _mi_os_protect(guard_page, os_page_size); + if mi_unlikely(!ok) { + _mi_warning_message("failed to set a guard page behind an object (object %p of size %zu)\n", block, block_size); + } + } + else { + _mi_warning_message("unable to set a guard page behind an object due to pinned memory (large OS pages?) (object %p of size %zu)\n", block, block_size); + } + + // align pointer just in front of the guard page + size_t offset = block_size - os_page_size - obj_size; + mi_assert_internal(offset > sizeof(mi_block_t)); + if (offset > MI_PAGE_MAX_OVERALLOC_ALIGN) { + // give up to place it right in front of the guard page if the offset is too large for unalignment + offset = MI_PAGE_MAX_OVERALLOC_ALIGN; + } + uint8_t* const p = (uint8_t*)block + offset; + mi_assert_internal(p == guard_page - obj_size || offset >= MI_PAGE_MAX_OVERALLOC_ALIGN); + if (usable_size != NULL) { *usable_size = (guard_page - p); mi_assert_internal(mi_usable_size(p)==*usable_size); } + mi_track_align(block, p, offset, obj_size); + mi_track_mem_defined(block, sizeof(mi_block_t)); + return p; +} + +// Allocate a block with a guard page behind it. +mi_decl_restrict void* _mi_theap_malloc_guarded(mi_theap_t* theap, size_t size, bool zero, mi_page_t** ppage) mi_attr_noexcept +{ + // allocate multiple of page size ending in a guard page + // ensure minimal alignment requirement? + if mi_unlikely(size >= MI_MAX_ALLOC_SIZE - MI_PADDING_SIZE) { // check up front so the `req_size` won't overflow + _mi_error_message(EOVERFLOW, "(guarded) allocation request is too large (%zu bytes)\n", size); + return NULL; + } + const size_t os_page_size = _mi_os_page_size(); + const size_t obj_size = (mi_option_is_enabled(mi_option_guarded_precise) ? size : mi_align_up(size, MI_MAX_ALIGN_SIZE)); + const size_t bsize = mi_align_up(mi_align_up(obj_size, MI_MAX_ALIGN_SIZE) + sizeof(mi_block_t), MI_MAX_ALIGN_SIZE); + const size_t req_size = mi_align_up(bsize + os_page_size, os_page_size); + // const size_t threshold = mi_theap_disable_profiler(theap); + mi_block_t* const block = (mi_block_t*)_mi_malloc_generic_no_sample(theap, req_size, false /* don't zero */, ppage); + // mi_theap_enable_profiler(theap,threshold); + if (block==NULL) return NULL; + size_t usable_size = 0; + void* const p = mi_block_ptr_set_guarded(block, obj_size, &usable_size); + if (p == NULL) return NULL; + if (zero) { + mi_memzero(p,obj_size); // we have to zero afterwards as padding might have written inside the block (if the `blocksize > reqsize + os_page_size`) + } + + // stats + mi_track_malloc(p, usable_size, zero); + if (!mi_theap_is_initialized(theap)) { theap = _mi_theap_default(); } + mi_theap_stat_counter_increase(theap, malloc_guarded_count, 1); + #if MI_STATS + // adjust request stats to only count the allocated size of the block (and not the guard page) + mi_theap_stat_counter_decrease(theap, malloc_requested, req_size); + mi_theap_stat_counter_increase(theap, malloc_requested, size); + #endif + #if MI_DEBUG>3 + if (zero) { + mi_assert_expensive(mi_mem_is_zero(p, size)); + } + #endif + #if MI_PAGE_META_SMALL_IS_ALIGNED && MI_DEBUG>=2 + // we should never allocate something allocated as small in a non-small page or otherwise aligned mi_free_small may fail. + if (size <= MI_SMALL_SIZE_MAX) { + mi_page_t* const page = mi_ptr_page(p); + mi_assert_internal(mi_page_block_size(page) <= MI_SMALL_MAX_OBJ_SIZE); + } + #endif + return p; +} + + +// Remove guard page when building with MI_GUARDED +#if MI_GUARDED +void _mi_page_block_unguard(mi_page_t* page, mi_block_t* block, void* p) { + // if (!mi_block_ptr_is_guarded(block,p)) return; + MI_UNUSED_RELEASE(p); + mi_assert_internal(mi_block_ptr_is_guarded(block, p)); + mi_assert_internal(mi_page_has_interior_pointers(page)); + mi_assert_internal((uint8_t*)p - (uint8_t*)block >= (ptrdiff_t)sizeof(mi_block_t)); + mi_assert_internal(block->next == MI_BLOCK_TAG_GUARDED); + + const size_t bsize = mi_page_block_size(page); + const size_t psize = _mi_os_page_size(); + mi_assert_internal(bsize > psize); + mi_assert_internal(!page->memid.is_pinned); + void* gpage = (uint8_t*)block + bsize - psize; + mi_assert_internal(mi_is_aligned(gpage, psize)); + _mi_os_unprotect(gpage, psize); +} + +// unguard a whole page (called from `mi_heap_destroy`) +void _mi_page_unguard_all(mi_page_t* page) { + if mi_likely(!mi_page_has_interior_pointers(page)) return; + uint8_t* const start = mi_page_start(page); + const size_t psize = mi_page_committed(page); + _mi_os_unprotect(start,psize); // unprotect all at once as we cannot know which blocks are guarded +} +#else +void _mi_page_block_unguard(mi_page_t* page, mi_block_t* block, void* p) { + MI_UNUSED(page); MI_UNUSED(block); MI_UNUSED(p); +} +void _mi_page_unguard_all(mi_page_t* page) { + MI_UNUSED(page); +} +#endif diff --git a/Objects/mimalloc/profile/sample-profile.c b/Objects/mimalloc/profile/sample-profile.c new file mode 100644 index 000000000000000..3dfdfc773bf9087 --- /dev/null +++ b/Objects/mimalloc/profile/sample-profile.c @@ -0,0 +1,276 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2019-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#include "mimalloc.h" +#include "mimalloc/prim/prim-tls.h" +#include "mimalloc/util/stats.h" +#include "mimalloc/alloc/page.h" +#include "mimalloc/profile/sample-guarded.h" +#include "mimalloc/profile/sample-profile.h" + +//---------------------------------------------------------------------------- +// General sampled allocation (called from `alloc/alloc-generic.c:mi_malloc_generic_fallback`) +// This is for both guarded and profiled sampling +//---------------------------------------------------------------------------- + +size_t _mi_theap_update_sample_rate(mi_theap_t* theap) { + const size_t old_sample_rate = theap->sample_rate; + theap->sample_rate = theap->profile_sample_rate; + if (theap->sample_rate == 0 || (theap->guarded_sample_rate!=0 && theap->sample_rate > theap->guarded_sample_rate)) { + theap->sample_rate = theap->guarded_sample_rate; + } + if (theap->sample_countdown > theap->sample_rate) { + theap->sample_countdown = theap->sample_rate; // todo: adjust difference? + } + return old_sample_rate; +} + +// Called when the theap sample countdown <= req_size; should call guarded or profiled allocation. +mi_decl_noinline mi_decl_restrict void* _mi_theap_malloc_sampled(mi_theap_t* theap, size_t req_size, bool zero, mi_page_t** ppage) mi_attr_noexcept +{ + // the size has not yet been counted against the countdown (and does not include MI_PADDING_SIZE) + mi_assert_internal(req_size <= MI_MAX_ALLOC_SIZE); + mi_assert_internal((theap->sample_countdown==SIZE_MAX && _mi_is_empty_theap(theap)) || + (theap->sample_countdown <= MI_SAMPLE_COUNTDOWN_MAX && theap->sample_countdown < req_size)); + const size_t size = req_size + MI_PADDING_SIZE; + + // handle empty theap and disabled sampling + if (theap->sample_rate==0) { + if (!_mi_is_empty_theap(theap)) { // avoid writing to the initial empty theap + theap->sample_countdown = MI_SAMPLE_COUNTDOWN_MAX; // avoid the sampling path for a long time + } + return _mi_malloc_generic_no_sample(theap,size,zero,ppage); + } + + // update countdown and total accummulated requested bytes since the last sample + mi_assert_internal(!_mi_is_empty_theap(theap)); + mi_assert_internal(req_size <= SIZE_MAX/2); + mi_assert_internal(theap->sample_rate > 0); + mi_assert_internal(theap->sample_rate <= SIZE_MAX/2); + mi_assert_internal(theap->sample_rate >= theap->sample_countdown); + + const uint64_t requested = theap->sample_requested = (uint64_t)theap->sample_rate + (uint64_t)(req_size - theap->sample_countdown) + theap->sample_requested; + mi_assert_internal(requested > 0); + mi_assert_internal(requested >= req_size); + theap->sample_countdown = theap->sample_rate; // reset sampling + + // update derived countdowns + mi_assert_internal(theap->profile_sample_rate!=0 || theap->profile_sample_countdown==0); + mi_assert_internal(theap->guarded_sample_rate!=0 || theap->guarded_sample_countdown==0); + bool sample_profile = false; + bool sample_guarded = false; + if (theap->profile_sample_rate!=0) { + if (theap->profile_sample_countdown >= requested) { theap->profile_sample_countdown -= (size_t)requested; } else { sample_profile = true; } + } + if (theap->guarded_sample_rate!=0) { + if (theap->guarded_sample_countdown >= requested) { theap->guarded_sample_countdown -= (size_t)requested; } else { sample_guarded = true; } + } + + // invoke callback? + if (sample_profile) { + theap->profile_sample_countdown = theap->profile_sample_rate; // reset countdown + theap->sample_requested = 0; // reset requested as we pass it to malloc_profiled + #if MI_PROFILE + return _mi_theap_malloc_profiled(theap,size,requested,zero,ppage); + #endif + } + else if (sample_guarded) { + if (req_size >= theap->guarded_size_min && req_size <= theap->guarded_size_max) { + // use guarded allocation + theap->guarded_sample_countdown = theap->guarded_sample_rate; // reset countdown + #if MI_GUARDED + return _mi_theap_malloc_guarded(theap,size,zero,ppage); + #endif + } + #if MI_GUARDED + else { + // failed size criteria, rewind the sample countdown so we sample asap again + // todo: can we do better here as this will cause many samples until it fits the size.. + theap->sample_countdown = 0; + } + #endif + } + // take generic path + return _mi_malloc_generic_no_sample(theap,size,zero,ppage); +} + + + +//---------------------------------------------------------------------------- +// Util +//---------------------------------------------------------------------------- + +static mi_profiler_t* mi_heap_profiler(const mi_heap_t* heap) { + return mi_atomic_load_ptr_acquire(mi_profiler_t,&heap->profiler); +} + +static mi_profiler_t* mi_theap_get_enabled_profiler(const mi_theap_t* theap) { + mi_heap_t* const heap = mi_theap_heap(theap); + mi_profiler_t* prof = mi_atomic_load_ptr_relaxed(mi_profiler_t, &heap->profiler); + if (prof!=NULL && mi_profiler_is_enabled(prof) && prof->on_alloc != NULL) { + return prof; + } + else { + return NULL; + } +} + +//---------------------------------------------------------------------------- +// Profile an allocation +//----------------------------------------------------------------------------- + +mi_decl_noinline mi_decl_restrict void* _mi_theap_malloc_profiled(mi_theap_t* theap, size_t size, uint64_t requested_since_last_sample, bool zero, mi_page_t** ppage) mi_attr_noexcept +{ + mi_assert_internal(theap!=NULL); + mi_assert_internal(size>=MI_PADDING_SIZE); + const size_t req_size = size - MI_PADDING_SIZE; + mi_assert_internal(req_size<=requested_since_last_sample); + mi_profiler_t* const prof = mi_theap_get_enabled_profiler(theap); + if (prof == NULL) { return _mi_malloc_generic_no_sample(theap,size,zero,ppage); } + mi_assert_internal(prof!=NULL && prof->on_alloc!=NULL && mi_profiler_is_enabled(prof)); + + void* p = NULL; + size_t new_sample_rate = 0; + if (prof->on_free==NULL || prof->sample_data_size==0) { + // just allocate without profiler data + p = _mi_malloc_generic_no_sample(theap,size,zero,ppage); + if (p==NULL) { return p; } + if (prof->on_alloc!=NULL) { + // we are just allocation profiling (not heap profiling as on_free == NULL) + new_sample_rate = (*prof->on_alloc)(prof, NULL /* no data */, p, req_size, theap->profile_sample_rate, requested_since_last_sample, mi_theap_heap(theap) ); + } + } + else { + // overallocate a larger block to store the profiler data + // [MI_BLOCK_TAG_PROFILE] [usable size] [ ... profile data ... ] [... user data ...] + const size_t sample_data_offset = sizeof(mi_block_t); + const size_t sample_user_data_size = mi_align_up(prof->sample_data_size > MI_PROFILE_SAMPLE_DATA_MAX_SIZE ? MI_PROFILE_SAMPLE_DATA_MAX_SIZE : prof->sample_data_size, sizeof(void*)); + const size_t sample_data_size = sizeof(mi_profiler_sample_data_t) + sample_user_data_size; // one void* too many just in case + const size_t user_offset = mi_align_up(sample_data_offset + sample_data_size, MI_MAX_ALIGN_SIZE); + const size_t oversize = user_offset + size; + mi_page_t* page = NULL; + mi_block_t* const block = (mi_block_t*)_mi_malloc_generic_no_sample(theap,oversize,zero,&page); + if (block==NULL) return NULL; + mi_assert_internal(page!=NULL); + if (ppage!=NULL) { *ppage = page; } + #if MI_PAGE_META_SMALL_IS_ALIGNED + // we should never allocate something allocated as small in a non-small page or otherwise aligned mi_free_small may fail. + // (that is why we need to limit the profiler_data_size as well) + // we should never allocate something allocated as small in a non-small page or otherwise aligned mi_free_small may fail. + if (size <= MI_SMALL_SIZE_MAX) { mi_assert_internal(mi_page_block_size(page) <= MI_SMALL_MAX_OBJ_SIZE); } + #endif + + // Set up the profiled block as an interior pointer so the interior "slow" path is taken on mi_free (where we catch it to call on_free) + mi_page_set_has_interior_pointers(page, true); + block->next = MI_BLOCK_TAG_PROFILED; + p = (uint8_t*)block + user_offset; + mi_profiler_sample_data_t* const sample_data = (mi_profiler_sample_data_t*)((uint8_t*)block + sample_data_offset); + sample_data->user_data_size = sample_user_data_size; + + // and call the profiler on_alloc + if (prof->on_alloc!=NULL) { + new_sample_rate = (*prof->on_alloc)(prof, sample_data, p, req_size, theap->profile_sample_rate, requested_since_last_sample, mi_theap_heap(theap) ); + } + } + if (new_sample_rate!=0 && new_sample_rate != (size_t)theap->profile_sample_rate) { + _mi_theap_set_profile_sample_rate(theap,new_sample_rate); + } + mi_theap_stat_counter_increase(theap,profile_samples,1); + return p; +} + +void _mi_page_profile_on_free(mi_page_t* page, mi_block_t* block, void* p) { + mi_assert_internal(mi_block_ptr_is_sampled(block,p)); + + // get the heap and profiler + mi_heap_t* const heap = mi_page_heap(page); + if (heap==NULL) return; + mi_profiler_t* prof = mi_heap_profiler(heap); + if (prof==NULL || !mi_profiler_is_enabled(prof) || prof->on_free==NULL) return; + + // call the on_free callback + mi_profiler_sample_data_t* const sample_data = (mi_profiler_sample_data_t*)((uint8_t*)block + sizeof(mi_block_t)); + (*prof->on_free)(prof, sample_data, p, heap); +} + + +//---------------------------------------------------------------------------- +// Profiling API +//-----------------------------------------------------------------------------* + +size_t _mi_theap_set_profile_sample_rate(mi_theap_t* theap, size_t sample_rate) { + const size_t old_sample_rate = theap->profile_sample_rate; + theap->profile_sample_rate = (sample_rate > MI_SAMPLE_RATE_MAX ? MI_SAMPLE_RATE_MAX : sample_rate); + if (theap->profile_sample_countdown > theap->profile_sample_rate) { + theap->profile_sample_countdown = theap->profile_sample_rate; // todo: adjust difference? + } + _mi_theap_update_sample_rate(theap); + return old_sample_rate; +} + +static bool mi_heap_set_profiler(mi_heap_t* heap, mi_profiler_t* profiler) { + mi_profiler_t* previous = (profiler==NULL ? mi_atomic_load_ptr_acquire(mi_profiler_t,&heap->profiler) : NULL); // don't overwrite unless it is NULL + return mi_atomic_cas_ptr_strong_acq_rel(mi_profiler_t, &heap->profiler, &previous, profiler); +} + +mi_decl_export bool mi_heap_profile(mi_heap_t* heap, mi_profiler_t* profiler) { + mi_profiler_stop(profiler); + return mi_heap_set_profiler(heap,profiler); +} + +mi_decl_export void mi_heap_profile_disable(mi_heap_t* heap) { + static mi_profiler_t mi_nosample_profiler = { NULL, 0, MI_SAMPLE_RATE_MAX, NULL, NULL, NULL, NULL }; + mi_heap_set_profiler(heap,NULL); // first clear any previous one + mi_heap_set_profiler(heap,&mi_nosample_profiler); // and then install the nosample one +} + +mi_decl_export bool mi_subproc_profile(mi_subproc_id_t subproc_id, mi_profiler_t* profiler) { + mi_subproc_t* subproc = _mi_subproc_from_id(subproc_id); + if (subproc==NULL) return false; + mi_profiler_stop(profiler); + mi_profiler_t* previous = (profiler==NULL ? mi_atomic_load_ptr_acquire(mi_profiler_t,&subproc->profiler) : NULL); // don't overwrite unless it is NULL + if (!mi_atomic_cas_ptr_strong_acq_rel(mi_profiler_t,&subproc->profiler, &previous, profiler)) { return false; } + mi_lock(&subproc->heaps_lock) { + for (mi_heap_t* heap = subproc->heaps; heap!=NULL; heap = heap->next) { + mi_heap_set_profiler(heap,profiler); + } + } + return true; +} + +mi_decl_export bool mi_profile( mi_profiler_t* profiler) { + return mi_subproc_profile(mi_subproc_main(),profiler); +} + +bool mi_profiler_start(mi_profiler_t* profiler ) { + if (profiler==NULL) return false; + const bool was_running = mi_profiler_set_enabled(profiler,true); + if (was_running) return true; + // for the main heap, if this is the profiler, start the theap more aggressively + // otherwise it will be picked up when theaps take the slow generic malloc path. + mi_heap_t* heap = mi_heap_main(); + if (mi_heap_profiler(heap)==profiler) { + mi_theap_t* theap = _mi_heap_theap_peek(heap); + if (theap!=NULL) { + _mi_theap_set_profile_sample_rate(theap,1); + } + } + return false; +} + +bool mi_profiler_stop(mi_profiler_t* profiler) { + if (profiler==NULL) return false; + return mi_profiler_set_enabled(profiler,false); +} + +void mi_profiler_snapshot(mi_profiler_t* profiler) { + if (profiler==NULL) return; + if (profiler->on_snapshot==NULL) return; + static mi_atomic_guard_t guard; + mi_atomic_guard(&guard) { + (*profiler->on_snapshot)(profiler); + } +} diff --git a/Objects/mimalloc/readme.md b/Objects/mimalloc/readme.md new file mode 100644 index 000000000000000..4902af4189271a5 --- /dev/null +++ b/Objects/mimalloc/readme.md @@ -0,0 +1,16 @@ +# Sources + +[`prim`](/include/mimalloc/prim.h) → [`os`](/include/mimalloc/prim/os.h) → [`arena`](/include/mimalloc/arena/arena-page.h`) → [`page`](/include/mimalloc/alloc/page.h) → [`alloc`](alloc/alloc.c) + +* [`alloc`](alloc): "front-end": main allocation and free functions. + +* [`arena`](arena): "back-end": management of large memory arenas allocated from the OS. + +* [`heap`](heap): initialization, first-class heaps, thread local _theaps_, and subprocesses. + +* [`prim`](prim): primitive functions needed from the host, and OS level allocations. + +* [`profile`](profile): sampling for guarded allocations, profiled allocations, and the default profiler. + +* [`util`](util): utility functions, libc, options, random numbers, statistics, and dynamic thread locals. + diff --git a/Objects/mimalloc/segment-map.c b/Objects/mimalloc/segment-map.c deleted file mode 100644 index 5141f2e1b1dffa9..000000000000000 --- a/Objects/mimalloc/segment-map.c +++ /dev/null @@ -1,153 +0,0 @@ -/* ---------------------------------------------------------------------------- -Copyright (c) 2019-2023, Microsoft Research, Daan Leijen -This is free software; you can redistribute it and/or modify it under the -terms of the MIT license. A copy of the license can be found in the file -"LICENSE" at the root of this distribution. ------------------------------------------------------------------------------*/ - -/* ----------------------------------------------------------- - The following functions are to reliably find the segment or - block that encompasses any pointer p (or NULL if it is not - in any of our segments). - We maintain a bitmap of all memory with 1 bit per MI_SEGMENT_SIZE (64MiB) - set to 1 if it contains the segment meta data. ------------------------------------------------------------ */ -#include "mimalloc.h" -#include "mimalloc/internal.h" -#include "mimalloc/atomic.h" - -#if (MI_INTPTR_SIZE==8) -#define MI_MAX_ADDRESS ((size_t)40 << 40) // 40TB (to include huge page areas) -#else -#define MI_MAX_ADDRESS ((size_t)2 << 30) // 2Gb -#endif - -#define MI_SEGMENT_MAP_BITS (MI_MAX_ADDRESS / MI_SEGMENT_SIZE) -#define MI_SEGMENT_MAP_SIZE (MI_SEGMENT_MAP_BITS / 8) -#define MI_SEGMENT_MAP_WSIZE (MI_SEGMENT_MAP_SIZE / MI_INTPTR_SIZE) - -static _Atomic(uintptr_t) mi_segment_map[MI_SEGMENT_MAP_WSIZE + 1]; // 2KiB per TB with 64MiB segments - -static size_t mi_segment_map_index_of(const mi_segment_t* segment, size_t* bitidx) { - mi_assert_internal(_mi_ptr_segment(segment + 1) == segment); // is it aligned on MI_SEGMENT_SIZE? - if ((uintptr_t)segment >= MI_MAX_ADDRESS) { - *bitidx = 0; - return MI_SEGMENT_MAP_WSIZE; - } - else { - const uintptr_t segindex = ((uintptr_t)segment) / MI_SEGMENT_SIZE; - *bitidx = segindex % MI_INTPTR_BITS; - const size_t mapindex = segindex / MI_INTPTR_BITS; - mi_assert_internal(mapindex < MI_SEGMENT_MAP_WSIZE); - return mapindex; - } -} - -void _mi_segment_map_allocated_at(const mi_segment_t* segment) { - size_t bitidx; - size_t index = mi_segment_map_index_of(segment, &bitidx); - mi_assert_internal(index <= MI_SEGMENT_MAP_WSIZE); - if (index==MI_SEGMENT_MAP_WSIZE) return; - uintptr_t mask = mi_atomic_load_relaxed(&mi_segment_map[index]); - uintptr_t newmask; - do { - newmask = (mask | ((uintptr_t)1 << bitidx)); - } while (!mi_atomic_cas_weak_release(&mi_segment_map[index], &mask, newmask)); -} - -void _mi_segment_map_freed_at(const mi_segment_t* segment) { - size_t bitidx; - size_t index = mi_segment_map_index_of(segment, &bitidx); - mi_assert_internal(index <= MI_SEGMENT_MAP_WSIZE); - if (index == MI_SEGMENT_MAP_WSIZE) return; - uintptr_t mask = mi_atomic_load_relaxed(&mi_segment_map[index]); - uintptr_t newmask; - do { - newmask = (mask & ~((uintptr_t)1 << bitidx)); - } while (!mi_atomic_cas_weak_release(&mi_segment_map[index], &mask, newmask)); -} - -// Determine the segment belonging to a pointer or NULL if it is not in a valid segment. -static mi_segment_t* _mi_segment_of(const void* p) { - if (p == NULL) return NULL; - mi_segment_t* segment = _mi_ptr_segment(p); - mi_assert_internal(segment != NULL); - size_t bitidx; - size_t index = mi_segment_map_index_of(segment, &bitidx); - // fast path: for any pointer to valid small/medium/large object or first MI_SEGMENT_SIZE in huge - const uintptr_t mask = mi_atomic_load_relaxed(&mi_segment_map[index]); - if mi_likely((mask & ((uintptr_t)1 << bitidx)) != 0) { - return segment; // yes, allocated by us - } - if (index==MI_SEGMENT_MAP_WSIZE) return NULL; - - // TODO: maintain max/min allocated range for efficiency for more efficient rejection of invalid pointers? - - // search downwards for the first segment in case it is an interior pointer - // could be slow but searches in MI_INTPTR_SIZE * MI_SEGMENT_SIZE (512MiB) steps through - // valid huge objects - // note: we could maintain a lowest index to speed up the path for invalid pointers? - size_t lobitidx; - size_t loindex; - uintptr_t lobits = mask & (((uintptr_t)1 << bitidx) - 1); - if (lobits != 0) { - loindex = index; - lobitidx = mi_bsr(lobits); // lobits != 0 - } - else if (index == 0) { - return NULL; - } - else { - mi_assert_internal(index > 0); - uintptr_t lomask = mask; - loindex = index; - do { - loindex--; - lomask = mi_atomic_load_relaxed(&mi_segment_map[loindex]); - } while (lomask != 0 && loindex > 0); - if (lomask == 0) return NULL; - lobitidx = mi_bsr(lomask); // lomask != 0 - } - mi_assert_internal(loindex < MI_SEGMENT_MAP_WSIZE); - // take difference as the addresses could be larger than the MAX_ADDRESS space. - size_t diff = (((index - loindex) * (8*MI_INTPTR_SIZE)) + bitidx - lobitidx) * MI_SEGMENT_SIZE; - segment = (mi_segment_t*)((uint8_t*)segment - diff); - - if (segment == NULL) return NULL; - mi_assert_internal((void*)segment < p); - bool cookie_ok = (_mi_ptr_cookie(segment) == segment->cookie); - mi_assert_internal(cookie_ok); - if mi_unlikely(!cookie_ok) return NULL; - if (((uint8_t*)segment + mi_segment_size(segment)) <= (uint8_t*)p) return NULL; // outside the range - mi_assert_internal(p >= (void*)segment && (uint8_t*)p < (uint8_t*)segment + mi_segment_size(segment)); - return segment; -} - -// Is this a valid pointer in our heap? -static bool mi_is_valid_pointer(const void* p) { - return ((_mi_segment_of(p) != NULL) || (_mi_arena_contains(p))); -} - -mi_decl_nodiscard mi_decl_export bool mi_is_in_heap_region(const void* p) mi_attr_noexcept { - return mi_is_valid_pointer(p); -} - -/* -// Return the full segment range belonging to a pointer -static void* mi_segment_range_of(const void* p, size_t* size) { - mi_segment_t* segment = _mi_segment_of(p); - if (segment == NULL) { - if (size != NULL) *size = 0; - return NULL; - } - else { - if (size != NULL) *size = segment->segment_size; - return segment; - } - mi_assert_expensive(page == NULL || mi_segment_is_valid(_mi_page_segment(page),tld)); - mi_assert_internal(page == NULL || (mi_segment_page_size(_mi_page_segment(page)) - (MI_SECURE == 0 ? 0 : _mi_os_page_size())) >= block_size); - mi_reset_delayed(tld); - mi_assert_internal(page == NULL || mi_page_not_in_queue(page, tld)); - return page; -} -*/ diff --git a/Objects/mimalloc/segment.c b/Objects/mimalloc/segment.c deleted file mode 100644 index 9dad69c995e7a05..000000000000000 --- a/Objects/mimalloc/segment.c +++ /dev/null @@ -1,1684 +0,0 @@ -/* ---------------------------------------------------------------------------- -Copyright (c) 2018-2020, Microsoft Research, Daan Leijen -This is free software; you can redistribute it and/or modify it under the -terms of the MIT license. A copy of the license can be found in the file -"LICENSE" at the root of this distribution. ------------------------------------------------------------------------------*/ -#include "mimalloc.h" -#include "mimalloc/internal.h" -#include "mimalloc/atomic.h" - -#include // memset -#include - -#define MI_PAGE_HUGE_ALIGN (256*1024) - -static void mi_segment_try_purge(mi_segment_t* segment, bool force, mi_stats_t* stats); - - -// ------------------------------------------------------------------- -// commit mask -// ------------------------------------------------------------------- - -static bool mi_commit_mask_all_set(const mi_commit_mask_t* commit, const mi_commit_mask_t* cm) { - for (size_t i = 0; i < MI_COMMIT_MASK_FIELD_COUNT; i++) { - if ((commit->mask[i] & cm->mask[i]) != cm->mask[i]) return false; - } - return true; -} - -static bool mi_commit_mask_any_set(const mi_commit_mask_t* commit, const mi_commit_mask_t* cm) { - for (size_t i = 0; i < MI_COMMIT_MASK_FIELD_COUNT; i++) { - if ((commit->mask[i] & cm->mask[i]) != 0) return true; - } - return false; -} - -static void mi_commit_mask_create_intersect(const mi_commit_mask_t* commit, const mi_commit_mask_t* cm, mi_commit_mask_t* res) { - for (size_t i = 0; i < MI_COMMIT_MASK_FIELD_COUNT; i++) { - res->mask[i] = (commit->mask[i] & cm->mask[i]); - } -} - -static void mi_commit_mask_clear(mi_commit_mask_t* res, const mi_commit_mask_t* cm) { - for (size_t i = 0; i < MI_COMMIT_MASK_FIELD_COUNT; i++) { - res->mask[i] &= ~(cm->mask[i]); - } -} - -static void mi_commit_mask_set(mi_commit_mask_t* res, const mi_commit_mask_t* cm) { - for (size_t i = 0; i < MI_COMMIT_MASK_FIELD_COUNT; i++) { - res->mask[i] |= cm->mask[i]; - } -} - -static void mi_commit_mask_create(size_t bitidx, size_t bitcount, mi_commit_mask_t* cm) { - mi_assert_internal(bitidx < MI_COMMIT_MASK_BITS); - mi_assert_internal((bitidx + bitcount) <= MI_COMMIT_MASK_BITS); - if (bitcount == MI_COMMIT_MASK_BITS) { - mi_assert_internal(bitidx==0); - mi_commit_mask_create_full(cm); - } - else if (bitcount == 0) { - mi_commit_mask_create_empty(cm); - } - else { - mi_commit_mask_create_empty(cm); - size_t i = bitidx / MI_COMMIT_MASK_FIELD_BITS; - size_t ofs = bitidx % MI_COMMIT_MASK_FIELD_BITS; - while (bitcount > 0) { - mi_assert_internal(i < MI_COMMIT_MASK_FIELD_COUNT); - size_t avail = MI_COMMIT_MASK_FIELD_BITS - ofs; - size_t count = (bitcount > avail ? avail : bitcount); - size_t mask = (count >= MI_COMMIT_MASK_FIELD_BITS ? ~((size_t)0) : (((size_t)1 << count) - 1) << ofs); - cm->mask[i] = mask; - bitcount -= count; - ofs = 0; - i++; - } - } -} - -size_t _mi_commit_mask_committed_size(const mi_commit_mask_t* cm, size_t total) { - mi_assert_internal((total%MI_COMMIT_MASK_BITS)==0); - size_t count = 0; - for (size_t i = 0; i < MI_COMMIT_MASK_FIELD_COUNT; i++) { - size_t mask = cm->mask[i]; - if (~mask == 0) { - count += MI_COMMIT_MASK_FIELD_BITS; - } - else { - for (; mask != 0; mask >>= 1) { // todo: use popcount - if ((mask&1)!=0) count++; - } - } - } - // we use total since for huge segments each commit bit may represent a larger size - return ((total / MI_COMMIT_MASK_BITS) * count); -} - - -size_t _mi_commit_mask_next_run(const mi_commit_mask_t* cm, size_t* idx) { - size_t i = (*idx) / MI_COMMIT_MASK_FIELD_BITS; - size_t ofs = (*idx) % MI_COMMIT_MASK_FIELD_BITS; - size_t mask = 0; - // find first ones - while (i < MI_COMMIT_MASK_FIELD_COUNT) { - mask = cm->mask[i]; - mask >>= ofs; - if (mask != 0) { - while ((mask&1) == 0) { - mask >>= 1; - ofs++; - } - break; - } - i++; - ofs = 0; - } - if (i >= MI_COMMIT_MASK_FIELD_COUNT) { - // not found - *idx = MI_COMMIT_MASK_BITS; - return 0; - } - else { - // found, count ones - size_t count = 0; - *idx = (i*MI_COMMIT_MASK_FIELD_BITS) + ofs; - do { - mi_assert_internal(ofs < MI_COMMIT_MASK_FIELD_BITS && (mask&1) == 1); - do { - count++; - mask >>= 1; - } while ((mask&1) == 1); - if ((((*idx + count) % MI_COMMIT_MASK_FIELD_BITS) == 0)) { - i++; - if (i >= MI_COMMIT_MASK_FIELD_COUNT) break; - mask = cm->mask[i]; - ofs = 0; - } - } while ((mask&1) == 1); - mi_assert_internal(count > 0); - return count; - } -} - - -/* -------------------------------------------------------------------------------- - Segment allocation - - If a thread ends, it "abandons" pages with used blocks - and there is an abandoned segment list whose segments can - be reclaimed by still running threads, much like work-stealing. --------------------------------------------------------------------------------- */ - - -/* ----------------------------------------------------------- - Slices ------------------------------------------------------------ */ - - -static const mi_slice_t* mi_segment_slices_end(const mi_segment_t* segment) { - return &segment->slices[segment->slice_entries]; -} - -static uint8_t* mi_slice_start(const mi_slice_t* slice) { - mi_segment_t* segment = _mi_ptr_segment(slice); - mi_assert_internal(slice >= segment->slices && slice < mi_segment_slices_end(segment)); - return ((uint8_t*)segment + ((slice - segment->slices)*MI_SEGMENT_SLICE_SIZE)); -} - - -/* ----------------------------------------------------------- - Bins ------------------------------------------------------------ */ -// Use bit scan forward to quickly find the first zero bit if it is available - -static inline size_t mi_slice_bin8(size_t slice_count) { - if (slice_count<=1) return slice_count; - mi_assert_internal(slice_count <= MI_SLICES_PER_SEGMENT); - slice_count--; - size_t s = mi_bsr(slice_count); // slice_count > 1 - if (s <= 2) return slice_count + 1; - size_t bin = ((s << 2) | ((slice_count >> (s - 2))&0x03)) - 4; - return bin; -} - -static inline size_t mi_slice_bin(size_t slice_count) { - mi_assert_internal(slice_count*MI_SEGMENT_SLICE_SIZE <= MI_SEGMENT_SIZE); - mi_assert_internal(mi_slice_bin8(MI_SLICES_PER_SEGMENT) <= MI_SEGMENT_BIN_MAX); - size_t bin = mi_slice_bin8(slice_count); - mi_assert_internal(bin <= MI_SEGMENT_BIN_MAX); - return bin; -} - -static inline size_t mi_slice_index(const mi_slice_t* slice) { - mi_segment_t* segment = _mi_ptr_segment(slice); - ptrdiff_t index = slice - segment->slices; - mi_assert_internal(index >= 0 && index < (ptrdiff_t)segment->slice_entries); - return index; -} - - -/* ----------------------------------------------------------- - Slice span queues ------------------------------------------------------------ */ - -static void mi_span_queue_push(mi_span_queue_t* sq, mi_slice_t* slice) { - // todo: or push to the end? - mi_assert_internal(slice->prev == NULL && slice->next==NULL); - slice->prev = NULL; // paranoia - slice->next = sq->first; - sq->first = slice; - if (slice->next != NULL) slice->next->prev = slice; - else sq->last = slice; - slice->xblock_size = 0; // free -} - -static mi_span_queue_t* mi_span_queue_for(size_t slice_count, mi_segments_tld_t* tld) { - size_t bin = mi_slice_bin(slice_count); - mi_span_queue_t* sq = &tld->spans[bin]; - mi_assert_internal(sq->slice_count >= slice_count); - return sq; -} - -static void mi_span_queue_delete(mi_span_queue_t* sq, mi_slice_t* slice) { - mi_assert_internal(slice->xblock_size==0 && slice->slice_count>0 && slice->slice_offset==0); - // should work too if the queue does not contain slice (which can happen during reclaim) - if (slice->prev != NULL) slice->prev->next = slice->next; - if (slice == sq->first) sq->first = slice->next; - if (slice->next != NULL) slice->next->prev = slice->prev; - if (slice == sq->last) sq->last = slice->prev; - slice->prev = NULL; - slice->next = NULL; - slice->xblock_size = 1; // no more free -} - - -/* ----------------------------------------------------------- - Invariant checking ------------------------------------------------------------ */ - -static bool mi_slice_is_used(const mi_slice_t* slice) { - return (slice->xblock_size > 0); -} - - -#if (MI_DEBUG>=3) -static bool mi_span_queue_contains(mi_span_queue_t* sq, mi_slice_t* slice) { - for (mi_slice_t* s = sq->first; s != NULL; s = s->next) { - if (s==slice) return true; - } - return false; -} - -static bool mi_segment_is_valid(mi_segment_t* segment, mi_segments_tld_t* tld) { - mi_assert_internal(segment != NULL); - mi_assert_internal(_mi_ptr_cookie(segment) == segment->cookie); - mi_assert_internal(segment->abandoned <= segment->used); - mi_assert_internal(segment->thread_id == 0 || segment->thread_id == _mi_thread_id()); - mi_assert_internal(mi_commit_mask_all_set(&segment->commit_mask, &segment->purge_mask)); // can only decommit committed blocks - //mi_assert_internal(segment->segment_info_size % MI_SEGMENT_SLICE_SIZE == 0); - mi_slice_t* slice = &segment->slices[0]; - const mi_slice_t* end = mi_segment_slices_end(segment); - size_t used_count = 0; - mi_span_queue_t* sq; - while(slice < end) { - mi_assert_internal(slice->slice_count > 0); - mi_assert_internal(slice->slice_offset == 0); - size_t index = mi_slice_index(slice); - size_t maxindex = (index + slice->slice_count >= segment->slice_entries ? segment->slice_entries : index + slice->slice_count) - 1; - if (mi_slice_is_used(slice)) { // a page in use, we need at least MAX_SLICE_OFFSET valid back offsets - used_count++; - for (size_t i = 0; i <= MI_MAX_SLICE_OFFSET && index + i <= maxindex; i++) { - mi_assert_internal(segment->slices[index + i].slice_offset == i*sizeof(mi_slice_t)); - mi_assert_internal(i==0 || segment->slices[index + i].slice_count == 0); - mi_assert_internal(i==0 || segment->slices[index + i].xblock_size == 1); - } - // and the last entry as well (for coalescing) - const mi_slice_t* last = slice + slice->slice_count - 1; - if (last > slice && last < mi_segment_slices_end(segment)) { - mi_assert_internal(last->slice_offset == (slice->slice_count-1)*sizeof(mi_slice_t)); - mi_assert_internal(last->slice_count == 0); - mi_assert_internal(last->xblock_size == 1); - } - } - else { // free range of slices; only last slice needs a valid back offset - mi_slice_t* last = &segment->slices[maxindex]; - if (segment->kind != MI_SEGMENT_HUGE || slice->slice_count <= (segment->slice_entries - segment->segment_info_slices)) { - mi_assert_internal((uint8_t*)slice == (uint8_t*)last - last->slice_offset); - } - mi_assert_internal(slice == last || last->slice_count == 0 ); - mi_assert_internal(last->xblock_size == 0 || (segment->kind==MI_SEGMENT_HUGE && last->xblock_size==1)); - if (segment->kind != MI_SEGMENT_HUGE && segment->thread_id != 0) { // segment is not huge or abandoned - sq = mi_span_queue_for(slice->slice_count,tld); - mi_assert_internal(mi_span_queue_contains(sq,slice)); - } - } - slice = &segment->slices[maxindex+1]; - } - mi_assert_internal(slice == end); - mi_assert_internal(used_count == segment->used + 1); - return true; -} -#endif - -/* ----------------------------------------------------------- - Segment size calculations ------------------------------------------------------------ */ - -static size_t mi_segment_info_size(mi_segment_t* segment) { - return segment->segment_info_slices * MI_SEGMENT_SLICE_SIZE; -} - -static uint8_t* _mi_segment_page_start_from_slice(const mi_segment_t* segment, const mi_slice_t* slice, size_t xblock_size, size_t* page_size) -{ - ptrdiff_t idx = slice - segment->slices; - size_t psize = (size_t)slice->slice_count * MI_SEGMENT_SLICE_SIZE; - // make the start not OS page aligned for smaller blocks to avoid page/cache effects - // note: the offset must always be an xblock_size multiple since we assume small allocations - // are aligned (see `mi_heap_malloc_aligned`). - size_t start_offset = 0; - if (xblock_size >= MI_INTPTR_SIZE) { - if (xblock_size <= 64) { start_offset = 3*xblock_size; } - else if (xblock_size <= 512) { start_offset = xblock_size; } - } - if (page_size != NULL) { *page_size = psize - start_offset; } - return (uint8_t*)segment + ((idx*MI_SEGMENT_SLICE_SIZE) + start_offset); -} - -// Start of the page available memory; can be used on uninitialized pages -uint8_t* _mi_segment_page_start(const mi_segment_t* segment, const mi_page_t* page, size_t* page_size) -{ - const mi_slice_t* slice = mi_page_to_slice((mi_page_t*)page); - uint8_t* p = _mi_segment_page_start_from_slice(segment, slice, page->xblock_size, page_size); - mi_assert_internal(page->xblock_size > 0 || _mi_ptr_page(p) == page); - mi_assert_internal(_mi_ptr_segment(p) == segment); - return p; -} - - -static size_t mi_segment_calculate_slices(size_t required, size_t* pre_size, size_t* info_slices) { - size_t page_size = _mi_os_page_size(); - size_t isize = _mi_align_up(sizeof(mi_segment_t), page_size); - size_t guardsize = 0; - - if (MI_SECURE>0) { - // in secure mode, we set up a protected page in between the segment info - // and the page data (and one at the end of the segment) - guardsize = page_size; - if (required > 0) { - required = _mi_align_up(required, MI_SEGMENT_SLICE_SIZE) + page_size; - } - } - - if (pre_size != NULL) *pre_size = isize; - isize = _mi_align_up(isize + guardsize, MI_SEGMENT_SLICE_SIZE); - if (info_slices != NULL) *info_slices = isize / MI_SEGMENT_SLICE_SIZE; - size_t segment_size = (required==0 ? MI_SEGMENT_SIZE : _mi_align_up( required + isize + guardsize, MI_SEGMENT_SLICE_SIZE) ); - mi_assert_internal(segment_size % MI_SEGMENT_SLICE_SIZE == 0); - return (segment_size / MI_SEGMENT_SLICE_SIZE); -} - - -/* ---------------------------------------------------------------------------- -Segment caches -We keep a small segment cache per thread to increase local -reuse and avoid setting/clearing guard pages in secure mode. -------------------------------------------------------------------------------- */ - -static void mi_segments_track_size(long segment_size, mi_segments_tld_t* tld) { - if (segment_size>=0) _mi_stat_increase(&tld->stats->segments,1); - else _mi_stat_decrease(&tld->stats->segments,1); - tld->count += (segment_size >= 0 ? 1 : -1); - if (tld->count > tld->peak_count) tld->peak_count = tld->count; - tld->current_size += segment_size; - if (tld->current_size > tld->peak_size) tld->peak_size = tld->current_size; -} - -static void mi_segment_os_free(mi_segment_t* segment, mi_segments_tld_t* tld) { - segment->thread_id = 0; - _mi_segment_map_freed_at(segment); - mi_segments_track_size(-((long)mi_segment_size(segment)),tld); - if (MI_SECURE>0) { - // _mi_os_unprotect(segment, mi_segment_size(segment)); // ensure no more guard pages are set - // unprotect the guard pages; we cannot just unprotect the whole segment size as part may be decommitted - size_t os_pagesize = _mi_os_page_size(); - _mi_os_unprotect((uint8_t*)segment + mi_segment_info_size(segment) - os_pagesize, os_pagesize); - uint8_t* end = (uint8_t*)segment + mi_segment_size(segment) - os_pagesize; - _mi_os_unprotect(end, os_pagesize); - } - - // purge delayed decommits now? (no, leave it to the arena) - // mi_segment_try_purge(segment,true,tld->stats); - - const size_t size = mi_segment_size(segment); - const size_t csize = _mi_commit_mask_committed_size(&segment->commit_mask, size); - - _mi_abandoned_await_readers(tld->abandoned); // wait until safe to free - _mi_arena_free(segment, mi_segment_size(segment), csize, segment->memid, tld->stats); -} - -// called by threads that are terminating -void _mi_segment_thread_collect(mi_segments_tld_t* tld) { - MI_UNUSED(tld); - // nothing to do -} - - -/* ----------------------------------------------------------- - Commit/Decommit ranges ------------------------------------------------------------ */ - -static void mi_segment_commit_mask(mi_segment_t* segment, bool conservative, uint8_t* p, size_t size, uint8_t** start_p, size_t* full_size, mi_commit_mask_t* cm) { - mi_assert_internal(_mi_ptr_segment(p + 1) == segment); - mi_assert_internal(segment->kind != MI_SEGMENT_HUGE); - mi_commit_mask_create_empty(cm); - if (size == 0 || size > MI_SEGMENT_SIZE || segment->kind == MI_SEGMENT_HUGE) return; - const size_t segstart = mi_segment_info_size(segment); - const size_t segsize = mi_segment_size(segment); - if (p >= (uint8_t*)segment + segsize) return; - - size_t pstart = (p - (uint8_t*)segment); - mi_assert_internal(pstart + size <= segsize); - - size_t start; - size_t end; - if (conservative) { - // decommit conservative - start = _mi_align_up(pstart, MI_COMMIT_SIZE); - end = _mi_align_down(pstart + size, MI_COMMIT_SIZE); - mi_assert_internal(start >= segstart); - mi_assert_internal(end <= segsize); - } - else { - // commit liberal - start = _mi_align_down(pstart, MI_MINIMAL_COMMIT_SIZE); - end = _mi_align_up(pstart + size, MI_MINIMAL_COMMIT_SIZE); - } - if (pstart >= segstart && start < segstart) { // note: the mask is also calculated for an initial commit of the info area - start = segstart; - } - if (end > segsize) { - end = segsize; - } - - mi_assert_internal(start <= pstart && (pstart + size) <= end); - mi_assert_internal(start % MI_COMMIT_SIZE==0 && end % MI_COMMIT_SIZE == 0); - *start_p = (uint8_t*)segment + start; - *full_size = (end > start ? end - start : 0); - if (*full_size == 0) return; - - size_t bitidx = start / MI_COMMIT_SIZE; - mi_assert_internal(bitidx < MI_COMMIT_MASK_BITS); - - size_t bitcount = *full_size / MI_COMMIT_SIZE; // can be 0 - if (bitidx + bitcount > MI_COMMIT_MASK_BITS) { - _mi_warning_message("commit mask overflow: idx=%zu count=%zu start=%zx end=%zx p=0x%p size=%zu fullsize=%zu\n", bitidx, bitcount, start, end, p, size, *full_size); - } - mi_assert_internal((bitidx + bitcount) <= MI_COMMIT_MASK_BITS); - mi_commit_mask_create(bitidx, bitcount, cm); -} - -static bool mi_segment_commit(mi_segment_t* segment, uint8_t* p, size_t size, mi_stats_t* stats) { - mi_assert_internal(mi_commit_mask_all_set(&segment->commit_mask, &segment->purge_mask)); - - // commit liberal - uint8_t* start = NULL; - size_t full_size = 0; - mi_commit_mask_t mask; - mi_segment_commit_mask(segment, false /* conservative? */, p, size, &start, &full_size, &mask); - if (mi_commit_mask_is_empty(&mask) || full_size == 0) return true; - - if (!mi_commit_mask_all_set(&segment->commit_mask, &mask)) { - // committing - bool is_zero = false; - mi_commit_mask_t cmask; - mi_commit_mask_create_intersect(&segment->commit_mask, &mask, &cmask); - _mi_stat_decrease(&_mi_stats_main.committed, _mi_commit_mask_committed_size(&cmask, MI_SEGMENT_SIZE)); // adjust for overlap - if (!_mi_os_commit(start, full_size, &is_zero, stats)) return false; - mi_commit_mask_set(&segment->commit_mask, &mask); - } - - // increase purge expiration when using part of delayed purges -- we assume more allocations are coming soon. - if (mi_commit_mask_any_set(&segment->purge_mask, &mask)) { - segment->purge_expire = _mi_clock_now() + mi_option_get(mi_option_purge_delay); - } - - // always clear any delayed purges in our range (as they are either committed now) - mi_commit_mask_clear(&segment->purge_mask, &mask); - return true; -} - -static bool mi_segment_ensure_committed(mi_segment_t* segment, uint8_t* p, size_t size, mi_stats_t* stats) { - mi_assert_internal(mi_commit_mask_all_set(&segment->commit_mask, &segment->purge_mask)); - // note: assumes commit_mask is always full for huge segments as otherwise the commit mask bits can overflow - if (mi_commit_mask_is_full(&segment->commit_mask) && mi_commit_mask_is_empty(&segment->purge_mask)) return true; // fully committed - mi_assert_internal(segment->kind != MI_SEGMENT_HUGE); - return mi_segment_commit(segment, p, size, stats); -} - -static bool mi_segment_purge(mi_segment_t* segment, uint8_t* p, size_t size, mi_stats_t* stats) { - mi_assert_internal(mi_commit_mask_all_set(&segment->commit_mask, &segment->purge_mask)); - if (!segment->allow_purge) return true; - - // purge conservative - uint8_t* start = NULL; - size_t full_size = 0; - mi_commit_mask_t mask; - mi_segment_commit_mask(segment, true /* conservative? */, p, size, &start, &full_size, &mask); - if (mi_commit_mask_is_empty(&mask) || full_size==0) return true; - - if (mi_commit_mask_any_set(&segment->commit_mask, &mask)) { - // purging - mi_assert_internal((void*)start != (void*)segment); - mi_assert_internal(segment->allow_decommit); - const bool decommitted = _mi_os_purge(start, full_size, stats); // reset or decommit - if (decommitted) { - mi_commit_mask_t cmask; - mi_commit_mask_create_intersect(&segment->commit_mask, &mask, &cmask); - _mi_stat_increase(&_mi_stats_main.committed, full_size - _mi_commit_mask_committed_size(&cmask, MI_SEGMENT_SIZE)); // adjust for double counting - mi_commit_mask_clear(&segment->commit_mask, &mask); - } - } - - // always clear any scheduled purges in our range - mi_commit_mask_clear(&segment->purge_mask, &mask); - return true; -} - -static void mi_segment_schedule_purge(mi_segment_t* segment, uint8_t* p, size_t size, mi_stats_t* stats) { - if (!segment->allow_purge) return; - - if (mi_option_get(mi_option_purge_delay) == 0) { - mi_segment_purge(segment, p, size, stats); - } - else { - // register for future purge in the purge mask - uint8_t* start = NULL; - size_t full_size = 0; - mi_commit_mask_t mask; - mi_segment_commit_mask(segment, true /*conservative*/, p, size, &start, &full_size, &mask); - if (mi_commit_mask_is_empty(&mask) || full_size==0) return; - - // update delayed commit - mi_assert_internal(segment->purge_expire > 0 || mi_commit_mask_is_empty(&segment->purge_mask)); - mi_commit_mask_t cmask; - mi_commit_mask_create_intersect(&segment->commit_mask, &mask, &cmask); // only purge what is committed; span_free may try to decommit more - mi_commit_mask_set(&segment->purge_mask, &cmask); - mi_msecs_t now = _mi_clock_now(); - if (segment->purge_expire == 0) { - // no previous purgess, initialize now - segment->purge_expire = now + mi_option_get(mi_option_purge_delay); - } - else if (segment->purge_expire <= now) { - // previous purge mask already expired - if (segment->purge_expire + mi_option_get(mi_option_purge_extend_delay) <= now) { - mi_segment_try_purge(segment, true, stats); - } - else { - segment->purge_expire = now + mi_option_get(mi_option_purge_extend_delay); // (mi_option_get(mi_option_purge_delay) / 8); // wait a tiny bit longer in case there is a series of free's - } - } - else { - // previous purge mask is not yet expired, increase the expiration by a bit. - segment->purge_expire += mi_option_get(mi_option_purge_extend_delay); - } - } -} - -static void mi_segment_try_purge(mi_segment_t* segment, bool force, mi_stats_t* stats) { - if (!segment->allow_purge || mi_commit_mask_is_empty(&segment->purge_mask)) return; - mi_msecs_t now = _mi_clock_now(); - if (!force && now < segment->purge_expire) return; - - mi_commit_mask_t mask = segment->purge_mask; - segment->purge_expire = 0; - mi_commit_mask_create_empty(&segment->purge_mask); - - size_t idx; - size_t count; - mi_commit_mask_foreach(&mask, idx, count) { - // if found, decommit that sequence - if (count > 0) { - uint8_t* p = (uint8_t*)segment + (idx*MI_COMMIT_SIZE); - size_t size = count * MI_COMMIT_SIZE; - mi_segment_purge(segment, p, size, stats); - } - } - mi_commit_mask_foreach_end() - mi_assert_internal(mi_commit_mask_is_empty(&segment->purge_mask)); -} - - -/* ----------------------------------------------------------- - Span free ------------------------------------------------------------ */ - -static bool mi_segment_is_abandoned(mi_segment_t* segment) { - return (segment->thread_id == 0); -} - -// note: can be called on abandoned segments -static void mi_segment_span_free(mi_segment_t* segment, size_t slice_index, size_t slice_count, bool allow_purge, mi_segments_tld_t* tld) { - mi_assert_internal(slice_index < segment->slice_entries); - mi_span_queue_t* sq = (segment->kind == MI_SEGMENT_HUGE || mi_segment_is_abandoned(segment) - ? NULL : mi_span_queue_for(slice_count,tld)); - if (slice_count==0) slice_count = 1; - mi_assert_internal(slice_index + slice_count - 1 < segment->slice_entries); - - // set first and last slice (the intermediates can be undetermined) - mi_slice_t* slice = &segment->slices[slice_index]; - slice->slice_count = (uint32_t)slice_count; - mi_assert_internal(slice->slice_count == slice_count); // no overflow? - slice->slice_offset = 0; - if (slice_count > 1) { - mi_slice_t* last = &segment->slices[slice_index + slice_count - 1]; - last->slice_count = 0; - last->slice_offset = (uint32_t)(sizeof(mi_page_t)*(slice_count - 1)); - last->xblock_size = 0; - } - - // perhaps decommit - if (allow_purge) { - mi_segment_schedule_purge(segment, mi_slice_start(slice), slice_count * MI_SEGMENT_SLICE_SIZE, tld->stats); - } - - // and push it on the free page queue (if it was not a huge page) - if (sq != NULL) mi_span_queue_push( sq, slice ); - else slice->xblock_size = 0; // mark huge page as free anyways -} - -/* -// called from reclaim to add existing free spans -static void mi_segment_span_add_free(mi_slice_t* slice, mi_segments_tld_t* tld) { - mi_segment_t* segment = _mi_ptr_segment(slice); - mi_assert_internal(slice->xblock_size==0 && slice->slice_count>0 && slice->slice_offset==0); - size_t slice_index = mi_slice_index(slice); - mi_segment_span_free(segment,slice_index,slice->slice_count,tld); -} -*/ - -static void mi_segment_span_remove_from_queue(mi_slice_t* slice, mi_segments_tld_t* tld) { - mi_assert_internal(slice->slice_count > 0 && slice->slice_offset==0 && slice->xblock_size==0); - mi_assert_internal(_mi_ptr_segment(slice)->kind != MI_SEGMENT_HUGE); - mi_span_queue_t* sq = mi_span_queue_for(slice->slice_count, tld); - mi_span_queue_delete(sq, slice); -} - -// note: can be called on abandoned segments -static mi_slice_t* mi_segment_span_free_coalesce(mi_slice_t* slice, mi_segments_tld_t* tld) { - mi_assert_internal(slice != NULL && slice->slice_count > 0 && slice->slice_offset == 0); - mi_segment_t* segment = _mi_ptr_segment(slice); - bool is_abandoned = mi_segment_is_abandoned(segment); - - // for huge pages, just mark as free but don't add to the queues - if (segment->kind == MI_SEGMENT_HUGE) { - // issue #691: segment->used can be 0 if the huge page block was freed while abandoned (reclaim will get here in that case) - mi_assert_internal((segment->used==0 && slice->xblock_size==0) || segment->used == 1); // decreased right after this call in `mi_segment_page_clear` - slice->xblock_size = 0; // mark as free anyways - // we should mark the last slice `xblock_size=0` now to maintain invariants but we skip it to - // avoid a possible cache miss (and the segment is about to be freed) - return slice; - } - - // otherwise coalesce the span and add to the free span queues - size_t slice_count = slice->slice_count; - mi_slice_t* next = slice + slice->slice_count; - mi_assert_internal(next <= mi_segment_slices_end(segment)); - if (next < mi_segment_slices_end(segment) && next->xblock_size==0) { - // free next block -- remove it from free and merge - mi_assert_internal(next->slice_count > 0 && next->slice_offset==0); - slice_count += next->slice_count; // extend - if (!is_abandoned) { mi_segment_span_remove_from_queue(next, tld); } - } - if (slice > segment->slices) { - mi_slice_t* prev = mi_slice_first(slice - 1); - mi_assert_internal(prev >= segment->slices); - if (prev->xblock_size==0) { - // free previous slice -- remove it from free and merge - mi_assert_internal(prev->slice_count > 0 && prev->slice_offset==0); - slice_count += prev->slice_count; - if (!is_abandoned) { mi_segment_span_remove_from_queue(prev, tld); } - slice = prev; - } - } - - // and add the new free page - mi_segment_span_free(segment, mi_slice_index(slice), slice_count, true, tld); - return slice; -} - - - -/* ----------------------------------------------------------- - Page allocation ------------------------------------------------------------ */ - -// Note: may still return NULL if committing the memory failed -static mi_page_t* mi_segment_span_allocate(mi_segment_t* segment, size_t slice_index, size_t slice_count, mi_segments_tld_t* tld) { - mi_assert_internal(slice_index < segment->slice_entries); - mi_slice_t* const slice = &segment->slices[slice_index]; - mi_assert_internal(slice->xblock_size==0 || slice->xblock_size==1); - - // commit before changing the slice data - if (!mi_segment_ensure_committed(segment, _mi_segment_page_start_from_slice(segment, slice, 0, NULL), slice_count * MI_SEGMENT_SLICE_SIZE, tld->stats)) { - return NULL; // commit failed! - } - - // convert the slices to a page - slice->slice_offset = 0; - slice->slice_count = (uint32_t)slice_count; - mi_assert_internal(slice->slice_count == slice_count); - const size_t bsize = slice_count * MI_SEGMENT_SLICE_SIZE; - slice->xblock_size = (uint32_t)(bsize >= MI_HUGE_BLOCK_SIZE ? MI_HUGE_BLOCK_SIZE : bsize); - mi_page_t* page = mi_slice_to_page(slice); - mi_assert_internal(mi_page_block_size(page) == bsize); - - // set slice back pointers for the first MI_MAX_SLICE_OFFSET entries - size_t extra = slice_count-1; - if (extra > MI_MAX_SLICE_OFFSET) extra = MI_MAX_SLICE_OFFSET; - if (slice_index + extra >= segment->slice_entries) extra = segment->slice_entries - slice_index - 1; // huge objects may have more slices than available entries in the segment->slices - - mi_slice_t* slice_next = slice + 1; - for (size_t i = 1; i <= extra; i++, slice_next++) { - slice_next->slice_offset = (uint32_t)(sizeof(mi_slice_t)*i); - slice_next->slice_count = 0; - slice_next->xblock_size = 1; - } - - // and also for the last one (if not set already) (the last one is needed for coalescing and for large alignments) - // note: the cast is needed for ubsan since the index can be larger than MI_SLICES_PER_SEGMENT for huge allocations (see #543) - mi_slice_t* last = slice + slice_count - 1; - mi_slice_t* end = (mi_slice_t*)mi_segment_slices_end(segment); - if (last > end) last = end; - if (last > slice) { - last->slice_offset = (uint32_t)(sizeof(mi_slice_t) * (last - slice)); - last->slice_count = 0; - last->xblock_size = 1; - } - - // and initialize the page - page->is_committed = true; - segment->used++; - return page; -} - -static void mi_segment_slice_split(mi_segment_t* segment, mi_slice_t* slice, size_t slice_count, mi_segments_tld_t* tld) { - mi_assert_internal(_mi_ptr_segment(slice) == segment); - mi_assert_internal(slice->slice_count >= slice_count); - mi_assert_internal(slice->xblock_size > 0); // no more in free queue - if (slice->slice_count <= slice_count) return; - mi_assert_internal(segment->kind != MI_SEGMENT_HUGE); - size_t next_index = mi_slice_index(slice) + slice_count; - size_t next_count = slice->slice_count - slice_count; - mi_segment_span_free(segment, next_index, next_count, false /* don't purge left-over part */, tld); - slice->slice_count = (uint32_t)slice_count; -} - -static mi_page_t* mi_segments_page_find_and_allocate(size_t slice_count, mi_arena_id_t req_arena_id, mi_segments_tld_t* tld) { - mi_assert_internal(slice_count*MI_SEGMENT_SLICE_SIZE <= MI_LARGE_OBJ_SIZE_MAX); - // search from best fit up - mi_span_queue_t* sq = mi_span_queue_for(slice_count, tld); - if (slice_count == 0) slice_count = 1; - while (sq <= &tld->spans[MI_SEGMENT_BIN_MAX]) { - for (mi_slice_t* slice = sq->first; slice != NULL; slice = slice->next) { - if (slice->slice_count >= slice_count) { - // found one - mi_segment_t* segment = _mi_ptr_segment(slice); - if (_mi_arena_memid_is_suitable(segment->memid, req_arena_id)) { - // found a suitable page span - mi_span_queue_delete(sq, slice); - - if (slice->slice_count > slice_count) { - mi_segment_slice_split(segment, slice, slice_count, tld); - } - mi_assert_internal(slice != NULL && slice->slice_count == slice_count && slice->xblock_size > 0); - mi_page_t* page = mi_segment_span_allocate(segment, mi_slice_index(slice), slice->slice_count, tld); - if (page == NULL) { - // commit failed; return NULL but first restore the slice - mi_segment_span_free_coalesce(slice, tld); - return NULL; - } - return page; - } - } - } - sq++; - } - // could not find a page.. - return NULL; -} - - -/* ----------------------------------------------------------- - Segment allocation ------------------------------------------------------------ */ - -static mi_segment_t* mi_segment_os_alloc( size_t required, size_t page_alignment, bool eager_delayed, mi_arena_id_t req_arena_id, - size_t* psegment_slices, size_t* ppre_size, size_t* pinfo_slices, - bool commit, mi_segments_tld_t* tld, mi_os_tld_t* os_tld) - -{ - mi_memid_t memid; - bool allow_large = (!eager_delayed && (MI_SECURE == 0)); // only allow large OS pages once we are no longer lazy - size_t align_offset = 0; - size_t alignment = MI_SEGMENT_ALIGN; - - if (page_alignment > 0) { - // mi_assert_internal(huge_page != NULL); - mi_assert_internal(page_alignment >= MI_SEGMENT_ALIGN); - alignment = page_alignment; - const size_t info_size = (*pinfo_slices) * MI_SEGMENT_SLICE_SIZE; - align_offset = _mi_align_up( info_size, MI_SEGMENT_ALIGN ); - const size_t extra = align_offset - info_size; - // recalculate due to potential guard pages - *psegment_slices = mi_segment_calculate_slices(required + extra, ppre_size, pinfo_slices); - - // mi_page_t.slice_count type is uint32_t - if (*psegment_slices > (size_t)UINT32_MAX) return NULL; - } - - const size_t segment_size = (*psegment_slices) * MI_SEGMENT_SLICE_SIZE; - mi_segment_t* segment = (mi_segment_t*)_mi_arena_alloc_aligned(segment_size, alignment, align_offset, commit, allow_large, req_arena_id, &memid, os_tld); - if (segment == NULL) { - return NULL; // failed to allocate - } - - // ensure metadata part of the segment is committed - mi_commit_mask_t commit_mask; - if (memid.initially_committed) { - mi_commit_mask_create_full(&commit_mask); - } - else { - // at least commit the info slices - const size_t commit_needed = _mi_divide_up((*pinfo_slices)*MI_SEGMENT_SLICE_SIZE, MI_COMMIT_SIZE); - mi_assert_internal(commit_needed>0); - mi_commit_mask_create(0, commit_needed, &commit_mask); - mi_assert_internal(commit_needed*MI_COMMIT_SIZE >= (*pinfo_slices)*MI_SEGMENT_SLICE_SIZE); - if (!_mi_os_commit(segment, commit_needed*MI_COMMIT_SIZE, NULL, tld->stats)) { - _mi_arena_free(segment,segment_size,0,memid,tld->stats); - return NULL; - } - } - mi_assert_internal(segment != NULL && (uintptr_t)segment % MI_SEGMENT_SIZE == 0); - - segment->memid = memid; - segment->allow_decommit = !memid.is_pinned; - segment->allow_purge = segment->allow_decommit && (mi_option_get(mi_option_purge_delay) >= 0); - segment->segment_size = segment_size; - segment->commit_mask = commit_mask; - segment->purge_expire = 0; - mi_commit_mask_create_empty(&segment->purge_mask); - mi_atomic_store_ptr_release(mi_segment_t, &segment->abandoned_next, NULL); // tsan - - mi_segments_track_size((long)(segment_size), tld); - _mi_segment_map_allocated_at(segment); - return segment; -} - - -// Allocate a segment from the OS aligned to `MI_SEGMENT_SIZE` . -static mi_segment_t* mi_segment_alloc(size_t required, size_t page_alignment, mi_arena_id_t req_arena_id, mi_segments_tld_t* tld, mi_os_tld_t* os_tld, mi_page_t** huge_page) -{ - mi_assert_internal((required==0 && huge_page==NULL) || (required>0 && huge_page != NULL)); - - // calculate needed sizes first - size_t info_slices; - size_t pre_size; - size_t segment_slices = mi_segment_calculate_slices(required, &pre_size, &info_slices); - - // mi_page_t.slice_count type is uint32_t - if (segment_slices > (size_t)UINT32_MAX) return NULL; - - // Commit eagerly only if not the first N lazy segments (to reduce impact of many threads that allocate just a little) - const bool eager_delay = (// !_mi_os_has_overcommit() && // never delay on overcommit systems - _mi_current_thread_count() > 1 && // do not delay for the first N threads - tld->count < (size_t)mi_option_get(mi_option_eager_commit_delay)); - const bool eager = !eager_delay && mi_option_is_enabled(mi_option_eager_commit); - bool commit = eager || (required > 0); - - // Allocate the segment from the OS - mi_segment_t* segment = mi_segment_os_alloc(required, page_alignment, eager_delay, req_arena_id, - &segment_slices, &pre_size, &info_slices, commit, tld, os_tld); - if (segment == NULL) return NULL; - - // zero the segment info? -- not always needed as it may be zero initialized from the OS - if (!segment->memid.initially_zero) { - ptrdiff_t ofs = offsetof(mi_segment_t, next); - size_t prefix = offsetof(mi_segment_t, slices) - ofs; - size_t zsize = prefix + (sizeof(mi_slice_t) * (segment_slices + 1)); // one more - _mi_memzero((uint8_t*)segment + ofs, zsize); - } - - // initialize the rest of the segment info - const size_t slice_entries = (segment_slices > MI_SLICES_PER_SEGMENT ? MI_SLICES_PER_SEGMENT : segment_slices); - segment->segment_slices = segment_slices; - segment->segment_info_slices = info_slices; - segment->thread_id = _mi_thread_id(); - segment->cookie = _mi_ptr_cookie(segment); - segment->slice_entries = slice_entries; - segment->kind = (required == 0 ? MI_SEGMENT_NORMAL : MI_SEGMENT_HUGE); - - // _mi_memzero(segment->slices, sizeof(mi_slice_t)*(info_slices+1)); - _mi_stat_increase(&tld->stats->page_committed, mi_segment_info_size(segment)); - - // set up guard pages - size_t guard_slices = 0; - if (MI_SECURE>0) { - // in secure mode, we set up a protected page in between the segment info - // and the page data, and at the end of the segment. - size_t os_pagesize = _mi_os_page_size(); - mi_assert_internal(mi_segment_info_size(segment) - os_pagesize >= pre_size); - _mi_os_protect((uint8_t*)segment + mi_segment_info_size(segment) - os_pagesize, os_pagesize); - uint8_t* end = (uint8_t*)segment + mi_segment_size(segment) - os_pagesize; - mi_segment_ensure_committed(segment, end, os_pagesize, tld->stats); - _mi_os_protect(end, os_pagesize); - if (slice_entries == segment_slices) segment->slice_entries--; // don't use the last slice :-( - guard_slices = 1; - } - - // reserve first slices for segment info - mi_page_t* page0 = mi_segment_span_allocate(segment, 0, info_slices, tld); - mi_assert_internal(page0!=NULL); if (page0==NULL) return NULL; // cannot fail as we always commit in advance - mi_assert_internal(segment->used == 1); - segment->used = 0; // don't count our internal slices towards usage - - // initialize initial free pages - if (segment->kind == MI_SEGMENT_NORMAL) { // not a huge page - mi_assert_internal(huge_page==NULL); - mi_segment_span_free(segment, info_slices, segment->slice_entries - info_slices, false /* don't purge */, tld); - } - else { - mi_assert_internal(huge_page!=NULL); - mi_assert_internal(mi_commit_mask_is_empty(&segment->purge_mask)); - mi_assert_internal(mi_commit_mask_is_full(&segment->commit_mask)); - *huge_page = mi_segment_span_allocate(segment, info_slices, segment_slices - info_slices - guard_slices, tld); - mi_assert_internal(*huge_page != NULL); // cannot fail as we commit in advance - } - - mi_assert_expensive(mi_segment_is_valid(segment,tld)); - return segment; -} - - -static void mi_segment_free(mi_segment_t* segment, bool force, mi_segments_tld_t* tld) { - MI_UNUSED(force); - mi_assert_internal(segment != NULL); - mi_assert_internal(segment->next == NULL); - mi_assert_internal(segment->used == 0); - - // Remove the free pages - mi_slice_t* slice = &segment->slices[0]; - const mi_slice_t* end = mi_segment_slices_end(segment); - #if MI_DEBUG>1 - size_t page_count = 0; - #endif - while (slice < end) { - mi_assert_internal(slice->slice_count > 0); - mi_assert_internal(slice->slice_offset == 0); - mi_assert_internal(mi_slice_index(slice)==0 || slice->xblock_size == 0); // no more used pages .. - if (slice->xblock_size == 0 && segment->kind != MI_SEGMENT_HUGE) { - mi_segment_span_remove_from_queue(slice, tld); - } - #if MI_DEBUG>1 - page_count++; - #endif - slice = slice + slice->slice_count; - } - mi_assert_internal(page_count == 2); // first page is allocated by the segment itself - - // stats - _mi_stat_decrease(&tld->stats->page_committed, mi_segment_info_size(segment)); - - // return it to the OS - mi_segment_os_free(segment, tld); -} - - -/* ----------------------------------------------------------- - Page Free ------------------------------------------------------------ */ - -static void mi_segment_abandon(mi_segment_t* segment, mi_segments_tld_t* tld); - -// note: can be called on abandoned pages -static mi_slice_t* mi_segment_page_clear(mi_page_t* page, mi_segments_tld_t* tld) { - mi_assert_internal(page->xblock_size > 0); - mi_assert_internal(mi_page_all_free(page)); - mi_segment_t* segment = _mi_ptr_segment(page); - mi_assert_internal(segment->used > 0); -#ifdef Py_GIL_DISABLED - mi_assert_internal(page->qsbr_goal == 0); - mi_assert_internal(page->qsbr_node.next == NULL); -#endif - - size_t inuse = page->capacity * mi_page_block_size(page); - _mi_stat_decrease(&tld->stats->page_committed, inuse); - _mi_stat_decrease(&tld->stats->pages, 1); - - // reset the page memory to reduce memory pressure? - if (segment->allow_decommit && mi_option_is_enabled(mi_option_deprecated_page_reset)) { - size_t psize; - uint8_t* start = _mi_page_start(segment, page, &psize); - _mi_os_reset(start, psize, tld->stats); - } - - // zero the page data, but not the segment fields - page->is_zero_init = false; - ptrdiff_t ofs = offsetof(mi_page_t, capacity); - _mi_memzero((uint8_t*)page + ofs, sizeof(*page) - ofs); - page->xblock_size = 1; - - // and free it - mi_slice_t* slice = mi_segment_span_free_coalesce(mi_page_to_slice(page), tld); - segment->used--; - // cannot assert segment valid as it is called during reclaim - // mi_assert_expensive(mi_segment_is_valid(segment, tld)); - return slice; -} - -void _mi_segment_page_free(mi_page_t* page, bool force, mi_segments_tld_t* tld) -{ - mi_assert(page != NULL); - - mi_segment_t* segment = _mi_page_segment(page); - mi_assert_expensive(mi_segment_is_valid(segment,tld)); - - // mark it as free now - mi_segment_page_clear(page, tld); - mi_assert_expensive(mi_segment_is_valid(segment, tld)); - - if (segment->used == 0) { - // no more used pages; remove from the free list and free the segment - mi_segment_free(segment, force, tld); - } - else if (segment->used == segment->abandoned) { - // only abandoned pages; remove from free list and abandon - mi_segment_abandon(segment,tld); - } -} - - -/* ----------------------------------------------------------- -Abandonment - -When threads terminate, they can leave segments with -live blocks (reachable through other threads). Such segments -are "abandoned" and will be reclaimed by other threads to -reuse their pages and/or free them eventually - -We maintain a global list of abandoned segments that are -reclaimed on demand. Since this is shared among threads -the implementation needs to avoid the A-B-A problem on -popping abandoned segments: -We use tagged pointers to avoid accidentally identifying -reused segments, much like stamped references in Java. -Secondly, we maintain a reader counter to avoid resetting -or decommitting segments that have a pending read operation. - -Note: the current implementation is one possible design; -another way might be to keep track of abandoned segments -in the arenas/segment_cache's. This would have the advantage of keeping -all concurrent code in one place and not needing to deal -with ABA issues. The drawback is that it is unclear how to -scan abandoned segments efficiently in that case as they -would be spread among all other segments in the arenas. ------------------------------------------------------------ */ - -// Use the bottom 20-bits (on 64-bit) of the aligned segment pointers -// to put in a tag that increments on update to avoid the A-B-A problem. -#define MI_TAGGED_MASK MI_SEGMENT_MASK - -static mi_segment_t* mi_tagged_segment_ptr(mi_tagged_segment_t ts) { - return (mi_segment_t*)(ts & ~MI_TAGGED_MASK); -} - -static mi_tagged_segment_t mi_tagged_segment(mi_segment_t* segment, mi_tagged_segment_t ts) { - mi_assert_internal(((uintptr_t)segment & MI_TAGGED_MASK) == 0); - uintptr_t tag = ((ts & MI_TAGGED_MASK) + 1) & MI_TAGGED_MASK; - return ((uintptr_t)segment | tag); -} - -mi_abandoned_pool_t _mi_abandoned_default; - -// Push on the visited list -static void mi_abandoned_visited_push(mi_abandoned_pool_t *pool, mi_segment_t* segment) { - mi_assert_internal(segment->thread_id == 0); - mi_assert_internal(mi_atomic_load_ptr_relaxed(mi_segment_t,&segment->abandoned_next) == NULL); - mi_assert_internal(segment->next == NULL); - mi_assert_internal(segment->used > 0); - mi_segment_t* anext = mi_atomic_load_ptr_relaxed(mi_segment_t, &pool->abandoned_visited); - do { - mi_atomic_store_ptr_release(mi_segment_t, &segment->abandoned_next, anext); - } while (!mi_atomic_cas_ptr_weak_release(mi_segment_t, &pool->abandoned_visited, &anext, segment)); - mi_atomic_increment_relaxed(&pool->abandoned_visited_count); -} - -// Move the visited list to the abandoned list. -static bool mi_abandoned_visited_revisit(mi_abandoned_pool_t *pool) -{ - // quick check if the visited list is empty - if (mi_atomic_load_ptr_relaxed(mi_segment_t, &pool->abandoned_visited) == NULL) return false; - - // grab the whole visited list - mi_segment_t* first = mi_atomic_exchange_ptr_acq_rel(mi_segment_t, &pool->abandoned_visited, NULL); - if (first == NULL) return false; - - // first try to swap directly if the abandoned list happens to be NULL - mi_tagged_segment_t afirst; - mi_tagged_segment_t ts = mi_atomic_load_relaxed(&pool->abandoned); - if (mi_tagged_segment_ptr(ts)==NULL) { - size_t count = mi_atomic_load_relaxed(&pool->abandoned_visited_count); - afirst = mi_tagged_segment(first, ts); - if (mi_atomic_cas_strong_acq_rel(&pool->abandoned, &ts, afirst)) { - mi_atomic_add_relaxed(&pool->abandoned_count, count); - mi_atomic_sub_relaxed(&pool->abandoned_visited_count, count); - return true; - } - } - - // find the last element of the visited list: O(n) - mi_segment_t* last = first; - mi_segment_t* next; - while ((next = mi_atomic_load_ptr_relaxed(mi_segment_t, &last->abandoned_next)) != NULL) { - last = next; - } - - // and atomically prepend to the abandoned list - // (no need to increase the readers as we don't access the abandoned segments) - mi_tagged_segment_t anext = mi_atomic_load_relaxed(&pool->abandoned); - size_t count; - do { - count = mi_atomic_load_relaxed(&pool->abandoned_visited_count); - mi_atomic_store_ptr_release(mi_segment_t, &last->abandoned_next, mi_tagged_segment_ptr(anext)); - afirst = mi_tagged_segment(first, anext); - } while (!mi_atomic_cas_weak_release(&pool->abandoned, &anext, afirst)); - mi_atomic_add_relaxed(&pool->abandoned_count, count); - mi_atomic_sub_relaxed(&pool->abandoned_visited_count, count); - return true; -} - -// Push on the abandoned list. -static void mi_abandoned_push(mi_abandoned_pool_t* pool, mi_segment_t* segment) { - mi_assert_internal(segment->thread_id == 0); - mi_assert_internal(mi_atomic_load_ptr_relaxed(mi_segment_t, &segment->abandoned_next) == NULL); - mi_assert_internal(segment->next == NULL); - mi_assert_internal(segment->used > 0); - mi_tagged_segment_t next; - mi_tagged_segment_t ts = mi_atomic_load_relaxed(&pool->abandoned); - do { - mi_atomic_store_ptr_release(mi_segment_t, &segment->abandoned_next, mi_tagged_segment_ptr(ts)); - next = mi_tagged_segment(segment, ts); - } while (!mi_atomic_cas_weak_release(&pool->abandoned, &ts, next)); - mi_atomic_increment_relaxed(&pool->abandoned_count); -} - -// Wait until there are no more pending reads on segments that used to be in the abandoned list -// called for example from `arena.c` before decommitting -void _mi_abandoned_await_readers(mi_abandoned_pool_t* pool) { - size_t n; - do { - n = mi_atomic_load_acquire(&pool->abandoned_readers); - if (n != 0) mi_atomic_yield(); - } while (n != 0); -} - -// Pop from the abandoned list -static mi_segment_t* mi_abandoned_pop(mi_abandoned_pool_t* pool) { - mi_segment_t* segment; - // Check efficiently if it is empty (or if the visited list needs to be moved) - mi_tagged_segment_t ts = mi_atomic_load_relaxed(&pool->abandoned); - segment = mi_tagged_segment_ptr(ts); - if mi_likely(segment == NULL) { - if mi_likely(!mi_abandoned_visited_revisit(pool)) { // try to swap in the visited list on NULL - return NULL; - } - } - - // Do a pop. We use a reader count to prevent - // a segment to be decommitted while a read is still pending, - // and a tagged pointer to prevent A-B-A link corruption. - // (this is called from `region.c:_mi_mem_free` for example) - mi_atomic_increment_relaxed(&pool->abandoned_readers); // ensure no segment gets decommitted - mi_tagged_segment_t next = 0; - ts = mi_atomic_load_acquire(&pool->abandoned); - do { - segment = mi_tagged_segment_ptr(ts); - if (segment != NULL) { - mi_segment_t* anext = mi_atomic_load_ptr_relaxed(mi_segment_t, &segment->abandoned_next); - next = mi_tagged_segment(anext, ts); // note: reads the segment's `abandoned_next` field so should not be decommitted - } - } while (segment != NULL && !mi_atomic_cas_weak_acq_rel(&pool->abandoned, &ts, next)); - mi_atomic_decrement_relaxed(&pool->abandoned_readers); // release reader lock - if (segment != NULL) { - mi_atomic_store_ptr_release(mi_segment_t, &segment->abandoned_next, NULL); - mi_atomic_decrement_relaxed(&pool->abandoned_count); - } - return segment; -} - -/* ----------------------------------------------------------- - Abandon segment/page ------------------------------------------------------------ */ - -static void mi_segment_abandon(mi_segment_t* segment, mi_segments_tld_t* tld) { - mi_assert_internal(segment->used == segment->abandoned); - mi_assert_internal(segment->used > 0); - mi_assert_internal(mi_atomic_load_ptr_relaxed(mi_segment_t, &segment->abandoned_next) == NULL); - mi_assert_internal(segment->abandoned_visits == 0); - mi_assert_expensive(mi_segment_is_valid(segment,tld)); - - // remove the free pages from the free page queues - mi_slice_t* slice = &segment->slices[0]; - const mi_slice_t* end = mi_segment_slices_end(segment); - while (slice < end) { - mi_assert_internal(slice->slice_count > 0); - mi_assert_internal(slice->slice_offset == 0); - if (slice->xblock_size == 0) { // a free page - mi_segment_span_remove_from_queue(slice,tld); - slice->xblock_size = 0; // but keep it free - } - slice = slice + slice->slice_count; - } - - // perform delayed decommits (forcing is much slower on mstress) - mi_segment_try_purge(segment, mi_option_is_enabled(mi_option_abandoned_page_purge) /* force? */, tld->stats); - - // all pages in the segment are abandoned; add it to the abandoned list - _mi_stat_increase(&tld->stats->segments_abandoned, 1); - mi_segments_track_size(-((long)mi_segment_size(segment)), tld); - segment->thread_id = 0; - mi_atomic_store_ptr_release(mi_segment_t, &segment->abandoned_next, NULL); - segment->abandoned_visits = 1; // from 0 to 1 to signify it is abandoned - mi_abandoned_push(tld->abandoned, segment); -} - -void _mi_segment_page_abandon(mi_page_t* page, mi_segments_tld_t* tld) { - mi_assert(page != NULL); - mi_assert_internal(mi_page_thread_free_flag(page)==MI_NEVER_DELAYED_FREE); - mi_assert_internal(mi_page_heap(page) == NULL); - mi_segment_t* segment = _mi_page_segment(page); - - mi_assert_expensive(mi_segment_is_valid(segment,tld)); - segment->abandoned++; - - _mi_stat_increase(&tld->stats->pages_abandoned, 1); - mi_assert_internal(segment->abandoned <= segment->used); - if (segment->used == segment->abandoned) { - // all pages are abandoned, abandon the entire segment - mi_segment_abandon(segment, tld); - } -} - -/* ----------------------------------------------------------- - Reclaim abandoned pages ------------------------------------------------------------ */ - -static mi_slice_t* mi_slices_start_iterate(mi_segment_t* segment, const mi_slice_t** end) { - mi_slice_t* slice = &segment->slices[0]; - *end = mi_segment_slices_end(segment); - mi_assert_internal(slice->slice_count>0 && slice->xblock_size>0); // segment allocated page - slice = slice + slice->slice_count; // skip the first segment allocated page - return slice; -} - -// Possibly free pages and check if free space is available -static bool mi_segment_check_free(mi_segment_t* segment, size_t slices_needed, size_t block_size, mi_segments_tld_t* tld) -{ - mi_assert_internal(block_size < MI_HUGE_BLOCK_SIZE); - mi_assert_internal(mi_segment_is_abandoned(segment)); - bool has_page = false; - - // for all slices - const mi_slice_t* end; - mi_slice_t* slice = mi_slices_start_iterate(segment, &end); - while (slice < end) { - mi_assert_internal(slice->slice_count > 0); - mi_assert_internal(slice->slice_offset == 0); - if (mi_slice_is_used(slice)) { // used page - // ensure used count is up to date and collect potential concurrent frees - mi_page_t* const page = mi_slice_to_page(slice); - _mi_page_free_collect(page, false); - if (mi_page_all_free(page) && _PyMem_mi_page_is_safe_to_free(page)) { - // if this page is all free now, free it without adding to any queues (yet) - mi_assert_internal(page->next == NULL && page->prev==NULL); - _mi_stat_decrease(&tld->stats->pages_abandoned, 1); -#ifdef Py_GIL_DISABLED - page->qsbr_goal = 0; - mi_assert_internal(page->qsbr_node.next == NULL); -#endif - segment->abandoned--; - slice = mi_segment_page_clear(page, tld); // re-assign slice due to coalesce! - mi_assert_internal(!mi_slice_is_used(slice)); - if (slice->slice_count >= slices_needed) { - has_page = true; - } - } - else { - if (page->xblock_size == block_size && mi_page_has_any_available(page)) { - // a page has available free blocks of the right size - has_page = true; - } - } - } - else { - // empty span - if (slice->slice_count >= slices_needed) { - has_page = true; - } - } - slice = slice + slice->slice_count; - } - return has_page; -} - -static mi_heap_t* mi_heap_by_tag(mi_heap_t* heap, uint8_t tag) { - if (heap->tag == tag) { - return heap; - } - for (mi_heap_t *curr = heap->tld->heaps; curr != NULL; curr = curr->next) { - if (curr->tag == tag) { - return curr; - } - } - return NULL; -} - -// Reclaim an abandoned segment; returns NULL if the segment was freed -// set `right_page_reclaimed` to `true` if it reclaimed a page of the right `block_size` that was not full. -static mi_segment_t* mi_segment_reclaim(mi_segment_t* segment, mi_heap_t* heap, size_t requested_block_size, bool* right_page_reclaimed, mi_segments_tld_t* tld) { - mi_assert_internal(mi_atomic_load_ptr_relaxed(mi_segment_t, &segment->abandoned_next) == NULL); - mi_assert_expensive(mi_segment_is_valid(segment, tld)); - if (right_page_reclaimed != NULL) { *right_page_reclaimed = false; } - - segment->thread_id = _mi_thread_id(); - segment->abandoned_visits = 0; - mi_segments_track_size((long)mi_segment_size(segment), tld); - mi_assert_internal(segment->next == NULL); - _mi_stat_decrease(&tld->stats->segments_abandoned, 1); - - // for all slices - const mi_slice_t* end; - mi_slice_t* slice = mi_slices_start_iterate(segment, &end); - while (slice < end) { - mi_assert_internal(slice->slice_count > 0); - mi_assert_internal(slice->slice_offset == 0); - if (mi_slice_is_used(slice)) { - // in use: reclaim the page in our heap - mi_page_t* page = mi_slice_to_page(slice); - mi_heap_t* target_heap = mi_heap_by_tag(heap, page->tag); - mi_assert_internal(page->is_committed); - mi_assert_internal(mi_page_thread_free_flag(page)==MI_NEVER_DELAYED_FREE); - mi_assert_internal(mi_page_heap(page) == NULL); - mi_assert_internal(page->next == NULL && page->prev==NULL); - _mi_stat_decrease(&tld->stats->pages_abandoned, 1); - segment->abandoned--; - // set the heap again and allow delayed free again - mi_page_set_heap(page, target_heap); - _mi_page_use_delayed_free(page, MI_USE_DELAYED_FREE, true); // override never (after heap is set) - _mi_page_free_collect(page, false); // ensure used count is up to date - if (mi_page_all_free(page) && _PyMem_mi_page_is_safe_to_free(page)) { - // if everything free by now, free the page -#ifdef Py_GIL_DISABLED - page->qsbr_goal = 0; - mi_assert_internal(page->qsbr_node.next == NULL); -#endif - slice = mi_segment_page_clear(page, tld); // set slice again due to coalesceing - } - else { - // otherwise reclaim it into the heap - _mi_page_reclaim(target_heap, page); - if (requested_block_size == page->xblock_size && mi_page_has_any_available(page) && - requested_block_size <= MI_MEDIUM_OBJ_SIZE_MAX && heap == target_heap) { - if (right_page_reclaimed != NULL) { *right_page_reclaimed = true; } - } - } - } - else { - // the span is free, add it to our page queues - slice = mi_segment_span_free_coalesce(slice, tld); // set slice again due to coalesceing - } - mi_assert_internal(slice->slice_count>0 && slice->slice_offset==0); - slice = slice + slice->slice_count; - } - - mi_assert(segment->abandoned == 0); - if (segment->used == 0) { // due to page_clear - mi_assert_internal(right_page_reclaimed == NULL || !(*right_page_reclaimed)); - mi_segment_free(segment, false, tld); - return NULL; - } - else { - return segment; - } -} - - -void _mi_abandoned_reclaim_all(mi_heap_t* heap, mi_segments_tld_t* tld) { - mi_segment_t* segment; - while ((segment = mi_abandoned_pop(tld->abandoned)) != NULL) { - mi_segment_reclaim(segment, heap, 0, NULL, tld); - } -} - -static mi_segment_t* mi_segment_try_reclaim(mi_heap_t* heap, size_t needed_slices, size_t block_size, bool* reclaimed, mi_segments_tld_t* tld) -{ - *reclaimed = false; - mi_segment_t* segment; - long max_tries = mi_option_get_clamp(mi_option_max_segment_reclaim, 8, 1024); // limit the work to bound allocation times - while ((max_tries-- > 0) && ((segment = mi_abandoned_pop(tld->abandoned)) != NULL)) { - segment->abandoned_visits++; - // todo: an arena exclusive heap will potentially visit many abandoned unsuitable segments - // and push them into the visited list and use many tries. Perhaps we can skip non-suitable ones in a better way? - bool is_suitable = _mi_heap_memid_is_suitable(heap, segment->memid); - bool has_page = mi_segment_check_free(segment,needed_slices,block_size,tld); // try to free up pages (due to concurrent frees) - if (segment->used == 0) { - // free the segment (by forced reclaim) to make it available to other threads. - // note1: we prefer to free a segment as that might lead to reclaiming another - // segment that is still partially used. - // note2: we could in principle optimize this by skipping reclaim and directly - // freeing but that would violate some invariants temporarily) - mi_segment_reclaim(segment, heap, 0, NULL, tld); - } - else if (has_page && is_suitable) { - // found a large enough free span, or a page of the right block_size with free space - // we return the result of reclaim (which is usually `segment`) as it might free - // the segment due to concurrent frees (in which case `NULL` is returned). - return mi_segment_reclaim(segment, heap, block_size, reclaimed, tld); - } - else if (segment->abandoned_visits > 3 && is_suitable) { - // always reclaim on 3rd visit to limit the abandoned queue length. - mi_segment_reclaim(segment, heap, 0, NULL, tld); - } - else { - // otherwise, push on the visited list so it gets not looked at too quickly again - mi_segment_try_purge(segment, true /* force? */, tld->stats); // force purge if needed as we may not visit soon again - mi_abandoned_visited_push(tld->abandoned, segment); - } - } - return NULL; -} - - -void _mi_abandoned_collect(mi_heap_t* heap, bool force, mi_segments_tld_t* tld) -{ - mi_segment_t* segment; - mi_abandoned_pool_t* pool = tld->abandoned; - int max_tries = (force ? 16*1024 : 1024); // limit latency - if (force) { - mi_abandoned_visited_revisit(pool); - } - while ((max_tries-- > 0) && ((segment = mi_abandoned_pop(pool)) != NULL)) { - mi_segment_check_free(segment,0,0,tld); // try to free up pages (due to concurrent frees) - if (segment->used == 0) { - // free the segment (by forced reclaim) to make it available to other threads. - // note: we could in principle optimize this by skipping reclaim and directly - // freeing but that would violate some invariants temporarily) - mi_segment_reclaim(segment, heap, 0, NULL, tld); - } - else { - // otherwise, purge if needed and push on the visited list - // note: forced purge can be expensive if many threads are destroyed/created as in mstress. - mi_segment_try_purge(segment, force, tld->stats); - mi_abandoned_visited_push(pool, segment); - } - } -} - -/* ----------------------------------------------------------- - Reclaim or allocate ------------------------------------------------------------ */ - -static mi_segment_t* mi_segment_reclaim_or_alloc(mi_heap_t* heap, size_t needed_slices, size_t block_size, mi_segments_tld_t* tld, mi_os_tld_t* os_tld) -{ - mi_assert_internal(block_size < MI_HUGE_BLOCK_SIZE); - mi_assert_internal(block_size <= MI_LARGE_OBJ_SIZE_MAX); - - // 1. try to reclaim an abandoned segment - bool reclaimed; - mi_segment_t* segment = mi_segment_try_reclaim(heap, needed_slices, block_size, &reclaimed, tld); - if (reclaimed) { - // reclaimed the right page right into the heap - mi_assert_internal(segment != NULL); - return NULL; // pretend out-of-memory as the page will be in the page queue of the heap with available blocks - } - else if (segment != NULL) { - // reclaimed a segment with a large enough empty span in it - return segment; - } - // 2. otherwise allocate a fresh segment - return mi_segment_alloc(0, 0, heap->arena_id, tld, os_tld, NULL); -} - - -/* ----------------------------------------------------------- - Page allocation ------------------------------------------------------------ */ - -static mi_page_t* mi_segments_page_alloc(mi_heap_t* heap, mi_page_kind_t page_kind, size_t required, size_t block_size, mi_segments_tld_t* tld, mi_os_tld_t* os_tld) -{ - mi_assert_internal(required <= MI_LARGE_OBJ_SIZE_MAX && page_kind <= MI_PAGE_LARGE); - - // find a free page - size_t page_size = _mi_align_up(required, (required > MI_MEDIUM_PAGE_SIZE ? MI_MEDIUM_PAGE_SIZE : MI_SEGMENT_SLICE_SIZE)); - size_t slices_needed = page_size / MI_SEGMENT_SLICE_SIZE; - mi_assert_internal(slices_needed * MI_SEGMENT_SLICE_SIZE == page_size); - mi_page_t* page = mi_segments_page_find_and_allocate(slices_needed, heap->arena_id, tld); //(required <= MI_SMALL_SIZE_MAX ? 0 : slices_needed), tld); - if (page==NULL) { - // no free page, allocate a new segment and try again - if (mi_segment_reclaim_or_alloc(heap, slices_needed, block_size, tld, os_tld) == NULL) { - // OOM or reclaimed a good page in the heap - return NULL; - } - else { - // otherwise try again - return mi_segments_page_alloc(heap, page_kind, required, block_size, tld, os_tld); - } - } - mi_assert_internal(page != NULL && page->slice_count*MI_SEGMENT_SLICE_SIZE == page_size); - mi_assert_internal(_mi_ptr_segment(page)->thread_id == _mi_thread_id()); - mi_segment_try_purge(_mi_ptr_segment(page), false, tld->stats); - return page; -} - - - -/* ----------------------------------------------------------- - Huge page allocation ------------------------------------------------------------ */ - -static mi_page_t* mi_segment_huge_page_alloc(size_t size, size_t page_alignment, mi_arena_id_t req_arena_id, mi_segments_tld_t* tld, mi_os_tld_t* os_tld) -{ - mi_page_t* page = NULL; - mi_segment_t* segment = mi_segment_alloc(size,page_alignment,req_arena_id,tld,os_tld,&page); - if (segment == NULL || page==NULL) return NULL; - mi_assert_internal(segment->used==1); - mi_assert_internal(mi_page_block_size(page) >= size); - #if MI_HUGE_PAGE_ABANDON - segment->thread_id = 0; // huge segments are immediately abandoned - #endif - - // for huge pages we initialize the xblock_size as we may - // overallocate to accommodate large alignments. - size_t psize; - uint8_t* start = _mi_segment_page_start(segment, page, &psize); - page->xblock_size = (psize > MI_HUGE_BLOCK_SIZE ? MI_HUGE_BLOCK_SIZE : (uint32_t)psize); - - // decommit the part of the prefix of a page that will not be used; this can be quite large (close to MI_SEGMENT_SIZE) - if (page_alignment > 0 && segment->allow_decommit) { - uint8_t* aligned_p = (uint8_t*)_mi_align_up((uintptr_t)start, page_alignment); - mi_assert_internal(_mi_is_aligned(aligned_p, page_alignment)); - mi_assert_internal(psize - (aligned_p - start) >= size); - uint8_t* decommit_start = start + sizeof(mi_block_t); // for the free list - ptrdiff_t decommit_size = aligned_p - decommit_start; - _mi_os_reset(decommit_start, decommit_size, &_mi_stats_main); // note: cannot use segment_decommit on huge segments - } - - return page; -} - -#if MI_HUGE_PAGE_ABANDON -// free huge block from another thread -void _mi_segment_huge_page_free(mi_segment_t* segment, mi_page_t* page, mi_block_t* block) { - // huge page segments are always abandoned and can be freed immediately by any thread - mi_assert_internal(segment->kind==MI_SEGMENT_HUGE); - mi_assert_internal(segment == _mi_page_segment(page)); - mi_assert_internal(mi_atomic_load_relaxed(&segment->thread_id)==0); - - // claim it and free - mi_heap_t* heap = mi_heap_get_default(); // issue #221; don't use the internal get_default_heap as we need to ensure the thread is initialized. - // paranoia: if this it the last reference, the cas should always succeed - size_t expected_tid = 0; - if (mi_atomic_cas_strong_acq_rel(&segment->thread_id, &expected_tid, heap->thread_id)) { - mi_block_set_next(page, block, page->free); - page->free = block; - page->used--; - page->is_zero = false; - mi_assert(page->used == 0); - mi_tld_t* tld = heap->tld; - _mi_segment_page_free(page, true, &tld->segments); - } -#if (MI_DEBUG!=0) - else { - mi_assert_internal(false); - } -#endif -} - -#else -// reset memory of a huge block from another thread -void _mi_segment_huge_page_reset(mi_segment_t* segment, mi_page_t* page, mi_block_t* block) { - MI_UNUSED(page); - mi_assert_internal(segment->kind == MI_SEGMENT_HUGE); - mi_assert_internal(segment == _mi_page_segment(page)); - mi_assert_internal(page->used == 1); // this is called just before the free - mi_assert_internal(page->free == NULL); - if (segment->allow_decommit) { - size_t csize = mi_usable_size(block); - if (csize > sizeof(mi_block_t)) { - csize = csize - sizeof(mi_block_t); - uint8_t* p = (uint8_t*)block + sizeof(mi_block_t); - _mi_os_reset(p, csize, &_mi_stats_main); // note: cannot use segment_decommit on huge segments - } - } -} -#endif - -/* ----------------------------------------------------------- - Page allocation and free ------------------------------------------------------------ */ -mi_page_t* _mi_segment_page_alloc(mi_heap_t* heap, size_t block_size, size_t page_alignment, mi_segments_tld_t* tld, mi_os_tld_t* os_tld) { - mi_page_t* page; - if mi_unlikely(page_alignment > MI_ALIGNMENT_MAX) { - mi_assert_internal(_mi_is_power_of_two(page_alignment)); - mi_assert_internal(page_alignment >= MI_SEGMENT_SIZE); - if (page_alignment < MI_SEGMENT_SIZE) { page_alignment = MI_SEGMENT_SIZE; } - page = mi_segment_huge_page_alloc(block_size,page_alignment,heap->arena_id,tld,os_tld); - } - else if (block_size <= MI_SMALL_OBJ_SIZE_MAX) { - page = mi_segments_page_alloc(heap,MI_PAGE_SMALL,block_size,block_size,tld,os_tld); - } - else if (block_size <= MI_MEDIUM_OBJ_SIZE_MAX) { - page = mi_segments_page_alloc(heap,MI_PAGE_MEDIUM,MI_MEDIUM_PAGE_SIZE,block_size,tld, os_tld); - } - else if (block_size <= MI_LARGE_OBJ_SIZE_MAX) { - page = mi_segments_page_alloc(heap,MI_PAGE_LARGE,block_size,block_size,tld, os_tld); - } - else { - page = mi_segment_huge_page_alloc(block_size,page_alignment,heap->arena_id,tld,os_tld); - } - mi_assert_internal(page == NULL || _mi_heap_memid_is_suitable(heap, _mi_page_segment(page)->memid)); - mi_assert_expensive(page == NULL || mi_segment_is_valid(_mi_page_segment(page),tld)); - return page; -} - -/* ----------------------------------------------------------- - Visit blocks in abandoned segments ------------------------------------------------------------ */ - -static bool mi_segment_visit_page(mi_segment_t* segment, mi_page_t* page, bool visit_blocks, mi_block_visit_fun* visitor, void* arg) -{ - mi_heap_area_t area; - _mi_heap_area_init(&area, page); - if (!visitor(NULL, &area, NULL, area.block_size, arg)) return false; - if (visit_blocks) { - return _mi_heap_area_visit_blocks(&area, page, visitor, arg); - } - else { - return true; - } -} - -static bool mi_segment_visit_pages(mi_segment_t* segment, uint8_t page_tag, bool visit_blocks, mi_block_visit_fun* visitor, void* arg) { - const mi_slice_t* end; - mi_slice_t* slice = mi_slices_start_iterate(segment, &end); - while (slice < end) { - if (mi_slice_is_used(slice)) { - mi_page_t* const page = mi_slice_to_page(slice); - if (page->tag == page_tag) { - if (!mi_segment_visit_page(segment, page, visit_blocks, visitor, arg)) return false; - } - } - slice = slice + slice->slice_count; - } - return true; -} - -// Visit all blocks in a abandoned segments -bool _mi_abandoned_pool_visit_blocks(mi_abandoned_pool_t* pool, uint8_t page_tag, bool visit_blocks, mi_block_visit_fun* visitor, void* arg) { - // Note: this is not safe in any other thread is abandoning or claiming segments from the pool - mi_segment_t* segment = mi_tagged_segment_ptr(pool->abandoned); - while (segment != NULL) { - if (!mi_segment_visit_pages(segment, page_tag, visit_blocks, visitor, arg)) return false; - segment = segment->abandoned_next; - } - - segment = pool->abandoned_visited; - while (segment != NULL) { - if (!mi_segment_visit_pages(segment, page_tag, visit_blocks, visitor, arg)) return false; - segment = segment->abandoned_next; - } - - return true; -} diff --git a/Objects/mimalloc/static.c b/Objects/mimalloc/static.c index 98c4aa18e450cd7..edf493c6b423d29 100644 --- a/Objects/mimalloc/static.c +++ b/Objects/mimalloc/static.c @@ -1,40 +1,65 @@ /* ---------------------------------------------------------------------------- -Copyright (c) 2018-2020, Microsoft Research, Daan Leijen +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen This is free software; you can redistribute it and/or modify it under the terms of the MIT license. A copy of the license can be found in the file "LICENSE" at the root of this distribution. -----------------------------------------------------------------------------*/ + +// -------------------------------------------------------------------------- +// This is a "single file include" of the entire mimalloc library. +// Just include this file in your build to link statically with mimalloc. +// This will also override the standard library allocation functions +// (if cmake -DMI_OVERRIDE=ON (or the macro MI_MALLOC_OVERRIDE=1 is defined)). +// +// This is also used for a static override where we create a single object +// file containing the whole library. +// If such object file is linked first it will override all the standard library +// allocation functions (on Unix's). +// -------------------------------------------------------------------------- + #ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #if defined(__sun) -// same remarks as os.c for the static's context. +// same remarks as prim/os.c for the static's context. #undef _XOPEN_SOURCE #undef _POSIX_C_SOURCE #endif #include "mimalloc.h" -#include "mimalloc/internal.h" - -// For a static override we create a single object file -// containing the whole library. If it is linked first -// it will override all the standard library allocation -// functions (on Unix's). -#include "alloc.c" // includes alloc-override.c -#include "alloc-aligned.c" -#include "alloc-posix.c" -#include "arena.c" -#include "bitmap.c" -#include "heap.c" -#include "init.c" -#include "options.c" -#include "os.c" -#include "page.c" // includes page-queue.c -#include "random.c" -#include "segment.c" -#include "segment-map.c" -#include "stats.c" -#include "prim/prim.c" + +#include "prim/os.c" +#include "prim/prim.c" // includes platform specific prim//prim.c +#include "prim/prim-tls.c" #if MI_OSX_ZONE #include "prim/osx/alloc-override-zone.c" #endif + +#include "util/libc.c" +#include "util/options.c" +#include "util/random.c" +#include "util/stats.c" +#include "util/threadlocal.c" + +#include "arena/arena.c" +#include "arena/arena-alloc.c" +#include "arena/arena-page.c" +#include "arena/bitmap-chunk.c" +#include "arena/bitmap.c" + +#include "alloc/alloc.c" // includes alloc-override.c and free.c (for aliasing to work) +#include "alloc/alloc-generic.c" +#include "alloc/alloc-aligned.c" +#include "alloc/alloc-posix.c" +#include "alloc/page.c" +#include "alloc/page-queue.c" + +#include "heap/heap.c" +#include "heap/init.c" +#include "heap/page-map.c" +#include "heap/subproc.c" +#include "heap/theap.c" + +#include "profile/sample-guarded.c" +#include "profile/sample-profile.c" +#include "profile/pprof.c" diff --git a/Objects/mimalloc/stats.c b/Objects/mimalloc/stats.c deleted file mode 100644 index e7ea397ec4c384e..000000000000000 --- a/Objects/mimalloc/stats.c +++ /dev/null @@ -1,467 +0,0 @@ -/* ---------------------------------------------------------------------------- -Copyright (c) 2018-2021, Microsoft Research, Daan Leijen -This is free software; you can redistribute it and/or modify it under the -terms of the MIT license. A copy of the license can be found in the file -"LICENSE" at the root of this distribution. ------------------------------------------------------------------------------*/ -#include "mimalloc.h" -#include "mimalloc/internal.h" -#include "mimalloc/atomic.h" -#include "mimalloc/prim.h" - -#include // snprintf -#include // memset - -#if defined(_MSC_VER) && (_MSC_VER < 1920) -#pragma warning(disable:4204) // non-constant aggregate initializer -#endif - -/* ----------------------------------------------------------- - Statistics operations ------------------------------------------------------------ */ - -static bool mi_is_in_main(void* stat) { - return ((uint8_t*)stat >= (uint8_t*)&_mi_stats_main - && (uint8_t*)stat < ((uint8_t*)&_mi_stats_main + sizeof(mi_stats_t))); -} - -static void mi_stat_update(mi_stat_count_t* stat, int64_t amount) { - if (amount == 0) return; - if (mi_is_in_main(stat)) - { - // add atomically (for abandoned pages) - int64_t current = mi_atomic_addi64_relaxed(&stat->current, amount); - mi_atomic_maxi64_relaxed(&stat->peak, current + amount); - if (amount > 0) { - mi_atomic_addi64_relaxed(&stat->allocated,amount); - } - else { - mi_atomic_addi64_relaxed(&stat->freed, -amount); - } - } - else { - // add thread local - stat->current += amount; - if (stat->current > stat->peak) stat->peak = stat->current; - if (amount > 0) { - stat->allocated += amount; - } - else { - stat->freed += -amount; - } - } -} - -void _mi_stat_counter_increase(mi_stat_counter_t* stat, size_t amount) { - if (mi_is_in_main(stat)) { - mi_atomic_addi64_relaxed( &stat->count, 1 ); - mi_atomic_addi64_relaxed( &stat->total, (int64_t)amount ); - } - else { - stat->count++; - stat->total += amount; - } -} - -void _mi_stat_increase(mi_stat_count_t* stat, size_t amount) { - mi_stat_update(stat, (int64_t)amount); -} - -void _mi_stat_decrease(mi_stat_count_t* stat, size_t amount) { - mi_stat_update(stat, -((int64_t)amount)); -} - -// must be thread safe as it is called from stats_merge -static void mi_stat_add(mi_stat_count_t* stat, const mi_stat_count_t* src, int64_t unit) { - if (stat==src) return; - if (src->allocated==0 && src->freed==0) return; - mi_atomic_addi64_relaxed( &stat->allocated, src->allocated * unit); - mi_atomic_addi64_relaxed( &stat->current, src->current * unit); - mi_atomic_addi64_relaxed( &stat->freed, src->freed * unit); - // peak scores do not work across threads.. - mi_atomic_addi64_relaxed( &stat->peak, src->peak * unit); -} - -static void mi_stat_counter_add(mi_stat_counter_t* stat, const mi_stat_counter_t* src, int64_t unit) { - if (stat==src) return; - mi_atomic_addi64_relaxed( &stat->total, src->total * unit); - mi_atomic_addi64_relaxed( &stat->count, src->count * unit); -} - -// must be thread safe as it is called from stats_merge -static void mi_stats_add(mi_stats_t* stats, const mi_stats_t* src) { - if (stats==src) return; - mi_stat_add(&stats->segments, &src->segments,1); - mi_stat_add(&stats->pages, &src->pages,1); - mi_stat_add(&stats->reserved, &src->reserved, 1); - mi_stat_add(&stats->committed, &src->committed, 1); - mi_stat_add(&stats->reset, &src->reset, 1); - mi_stat_add(&stats->purged, &src->purged, 1); - mi_stat_add(&stats->page_committed, &src->page_committed, 1); - - mi_stat_add(&stats->pages_abandoned, &src->pages_abandoned, 1); - mi_stat_add(&stats->segments_abandoned, &src->segments_abandoned, 1); - mi_stat_add(&stats->threads, &src->threads, 1); - - mi_stat_add(&stats->malloc, &src->malloc, 1); - mi_stat_add(&stats->segments_cache, &src->segments_cache, 1); - mi_stat_add(&stats->normal, &src->normal, 1); - mi_stat_add(&stats->huge, &src->huge, 1); - mi_stat_add(&stats->large, &src->large, 1); - - mi_stat_counter_add(&stats->pages_extended, &src->pages_extended, 1); - mi_stat_counter_add(&stats->mmap_calls, &src->mmap_calls, 1); - mi_stat_counter_add(&stats->commit_calls, &src->commit_calls, 1); - mi_stat_counter_add(&stats->reset_calls, &src->reset_calls, 1); - mi_stat_counter_add(&stats->purge_calls, &src->purge_calls, 1); - - mi_stat_counter_add(&stats->page_no_retire, &src->page_no_retire, 1); - mi_stat_counter_add(&stats->searches, &src->searches, 1); - mi_stat_counter_add(&stats->normal_count, &src->normal_count, 1); - mi_stat_counter_add(&stats->huge_count, &src->huge_count, 1); - mi_stat_counter_add(&stats->large_count, &src->large_count, 1); -#if MI_STAT>1 - for (size_t i = 0; i <= MI_BIN_HUGE; i++) { - if (src->normal_bins[i].allocated > 0 || src->normal_bins[i].freed > 0) { - mi_stat_add(&stats->normal_bins[i], &src->normal_bins[i], 1); - } - } -#endif -} - -/* ----------------------------------------------------------- - Display statistics ------------------------------------------------------------ */ - -// unit > 0 : size in binary bytes -// unit == 0: count as decimal -// unit < 0 : count in binary -static void mi_printf_amount(int64_t n, int64_t unit, mi_output_fun* out, void* arg, const char* fmt) { - char buf[32]; buf[0] = 0; - int len = 32; - const char* suffix = (unit <= 0 ? " " : "B"); - const int64_t base = (unit == 0 ? 1000 : 1024); - if (unit>0) n *= unit; - - const int64_t pos = (n < 0 ? -n : n); - if (pos < base) { - if (n!=1 || suffix[0] != 'B') { // skip printing 1 B for the unit column - snprintf(buf, len, "%d %-3s", (int)n, (n==0 ? "" : suffix)); - } - } - else { - int64_t divider = base; - const char* magnitude = "K"; - if (pos >= divider*base) { divider *= base; magnitude = "M"; } - if (pos >= divider*base) { divider *= base; magnitude = "G"; } - const int64_t tens = (n / (divider/10)); - const long whole = (long)(tens/10); - const long frac1 = (long)(tens%10); - char unitdesc[8]; - snprintf(unitdesc, 8, "%s%s%s", magnitude, (base==1024 ? "i" : ""), suffix); - snprintf(buf, len, "%ld.%ld %-3s", whole, (frac1 < 0 ? -frac1 : frac1), unitdesc); - } - _mi_fprintf(out, arg, (fmt==NULL ? "%12s" : fmt), buf); -} - - -static void mi_print_amount(int64_t n, int64_t unit, mi_output_fun* out, void* arg) { - mi_printf_amount(n,unit,out,arg,NULL); -} - -static void mi_print_count(int64_t n, int64_t unit, mi_output_fun* out, void* arg) { - if (unit==1) _mi_fprintf(out, arg, "%12s"," "); - else mi_print_amount(n,0,out,arg); -} - -static void mi_stat_print_ex(const mi_stat_count_t* stat, const char* msg, int64_t unit, mi_output_fun* out, void* arg, const char* notok ) { - _mi_fprintf(out, arg,"%10s:", msg); - if (unit > 0) { - mi_print_amount(stat->peak, unit, out, arg); - mi_print_amount(stat->allocated, unit, out, arg); - mi_print_amount(stat->freed, unit, out, arg); - mi_print_amount(stat->current, unit, out, arg); - mi_print_amount(unit, 1, out, arg); - mi_print_count(stat->allocated, unit, out, arg); - if (stat->allocated > stat->freed) { - _mi_fprintf(out, arg, " "); - _mi_fprintf(out, arg, (notok == NULL ? "not all freed" : notok)); - _mi_fprintf(out, arg, "\n"); - } - else { - _mi_fprintf(out, arg, " ok\n"); - } - } - else if (unit<0) { - mi_print_amount(stat->peak, -1, out, arg); - mi_print_amount(stat->allocated, -1, out, arg); - mi_print_amount(stat->freed, -1, out, arg); - mi_print_amount(stat->current, -1, out, arg); - if (unit==-1) { - _mi_fprintf(out, arg, "%24s", ""); - } - else { - mi_print_amount(-unit, 1, out, arg); - mi_print_count((stat->allocated / -unit), 0, out, arg); - } - if (stat->allocated > stat->freed) - _mi_fprintf(out, arg, " not all freed!\n"); - else - _mi_fprintf(out, arg, " ok\n"); - } - else { - mi_print_amount(stat->peak, 1, out, arg); - mi_print_amount(stat->allocated, 1, out, arg); - _mi_fprintf(out, arg, "%11s", " "); // no freed - mi_print_amount(stat->current, 1, out, arg); - _mi_fprintf(out, arg, "\n"); - } -} - -static void mi_stat_print(const mi_stat_count_t* stat, const char* msg, int64_t unit, mi_output_fun* out, void* arg) { - mi_stat_print_ex(stat, msg, unit, out, arg, NULL); -} - -static void mi_stat_peak_print(const mi_stat_count_t* stat, const char* msg, int64_t unit, mi_output_fun* out, void* arg) { - _mi_fprintf(out, arg, "%10s:", msg); - mi_print_amount(stat->peak, unit, out, arg); - _mi_fprintf(out, arg, "\n"); -} - -static void mi_stat_counter_print(const mi_stat_counter_t* stat, const char* msg, mi_output_fun* out, void* arg ) { - _mi_fprintf(out, arg, "%10s:", msg); - mi_print_amount(stat->total, -1, out, arg); - _mi_fprintf(out, arg, "\n"); -} - - -static void mi_stat_counter_print_avg(const mi_stat_counter_t* stat, const char* msg, mi_output_fun* out, void* arg) { - const int64_t avg_tens = (stat->count == 0 ? 0 : (stat->total*10 / stat->count)); - const long avg_whole = (long)(avg_tens/10); - const long avg_frac1 = (long)(avg_tens%10); - _mi_fprintf(out, arg, "%10s: %5ld.%ld avg\n", msg, avg_whole, avg_frac1); -} - - -static void mi_print_header(mi_output_fun* out, void* arg ) { - _mi_fprintf(out, arg, "%10s: %11s %11s %11s %11s %11s %11s\n", "heap stats", "peak ", "total ", "freed ", "current ", "unit ", "count "); -} - -#if MI_STAT>1 -static void mi_stats_print_bins(const mi_stat_count_t* bins, size_t max, const char* fmt, mi_output_fun* out, void* arg) { - bool found = false; - char buf[64]; - for (size_t i = 0; i <= max; i++) { - if (bins[i].allocated > 0) { - found = true; - int64_t unit = _mi_bin_size((uint8_t)i); - snprintf(buf, 64, "%s %3lu", fmt, (long)i); - mi_stat_print(&bins[i], buf, unit, out, arg); - } - } - if (found) { - _mi_fprintf(out, arg, "\n"); - mi_print_header(out, arg); - } -} -#endif - - - -//------------------------------------------------------------ -// Use an output wrapper for line-buffered output -// (which is nice when using loggers etc.) -//------------------------------------------------------------ -typedef struct buffered_s { - mi_output_fun* out; // original output function - void* arg; // and state - char* buf; // local buffer of at least size `count+1` - size_t used; // currently used chars `used <= count` - size_t count; // total chars available for output -} buffered_t; - -static void mi_buffered_flush(buffered_t* buf) { - buf->buf[buf->used] = 0; - _mi_fputs(buf->out, buf->arg, NULL, buf->buf); - buf->used = 0; -} - -static void mi_cdecl mi_buffered_out(const char* msg, void* arg) { - buffered_t* buf = (buffered_t*)arg; - if (msg==NULL || buf==NULL) return; - for (const char* src = msg; *src != 0; src++) { - char c = *src; - if (buf->used >= buf->count) mi_buffered_flush(buf); - mi_assert_internal(buf->used < buf->count); - buf->buf[buf->used++] = c; - if (c == '\n') mi_buffered_flush(buf); - } -} - -//------------------------------------------------------------ -// Print statistics -//------------------------------------------------------------ - -static void _mi_stats_print(mi_stats_t* stats, mi_output_fun* out0, void* arg0) mi_attr_noexcept { - // wrap the output function to be line buffered - char buf[256]; - buffered_t buffer = { out0, arg0, NULL, 0, 255 }; - buffer.buf = buf; - mi_output_fun* out = &mi_buffered_out; - void* arg = &buffer; - - // and print using that - mi_print_header(out,arg); - #if MI_STAT>1 - mi_stats_print_bins(stats->normal_bins, MI_BIN_HUGE, "normal",out,arg); - #endif - #if MI_STAT - mi_stat_print(&stats->normal, "normal", (stats->normal_count.count == 0 ? 1 : -(stats->normal.allocated / stats->normal_count.count)), out, arg); - mi_stat_print(&stats->large, "large", (stats->large_count.count == 0 ? 1 : -(stats->large.allocated / stats->large_count.count)), out, arg); - mi_stat_print(&stats->huge, "huge", (stats->huge_count.count == 0 ? 1 : -(stats->huge.allocated / stats->huge_count.count)), out, arg); - mi_stat_count_t total = { 0,0,0,0 }; - mi_stat_add(&total, &stats->normal, 1); - mi_stat_add(&total, &stats->large, 1); - mi_stat_add(&total, &stats->huge, 1); - mi_stat_print(&total, "total", 1, out, arg); - #endif - #if MI_STAT>1 - mi_stat_print(&stats->malloc, "malloc req", 1, out, arg); - _mi_fprintf(out, arg, "\n"); - #endif - mi_stat_print_ex(&stats->reserved, "reserved", 1, out, arg, ""); - mi_stat_print_ex(&stats->committed, "committed", 1, out, arg, ""); - mi_stat_peak_print(&stats->reset, "reset", 1, out, arg ); - mi_stat_peak_print(&stats->purged, "purged", 1, out, arg ); - mi_stat_print(&stats->page_committed, "touched", 1, out, arg); - mi_stat_print(&stats->segments, "segments", -1, out, arg); - mi_stat_print(&stats->segments_abandoned, "-abandoned", -1, out, arg); - mi_stat_print(&stats->segments_cache, "-cached", -1, out, arg); - mi_stat_print(&stats->pages, "pages", -1, out, arg); - mi_stat_print(&stats->pages_abandoned, "-abandoned", -1, out, arg); - mi_stat_counter_print(&stats->pages_extended, "-extended", out, arg); - mi_stat_counter_print(&stats->page_no_retire, "-noretire", out, arg); - mi_stat_counter_print(&stats->mmap_calls, "mmaps", out, arg); - mi_stat_counter_print(&stats->commit_calls, "commits", out, arg); - mi_stat_counter_print(&stats->reset_calls, "resets", out, arg); - mi_stat_counter_print(&stats->purge_calls, "purges", out, arg); - mi_stat_print(&stats->threads, "threads", -1, out, arg); - mi_stat_counter_print_avg(&stats->searches, "searches", out, arg); - _mi_fprintf(out, arg, "%10s: %5zu\n", "numa nodes", _mi_os_numa_node_count()); - - size_t elapsed; - size_t user_time; - size_t sys_time; - size_t current_rss; - size_t peak_rss; - size_t current_commit; - size_t peak_commit; - size_t page_faults; - mi_process_info(&elapsed, &user_time, &sys_time, ¤t_rss, &peak_rss, ¤t_commit, &peak_commit, &page_faults); - _mi_fprintf(out, arg, "%10s: %5ld.%03ld s\n", "elapsed", elapsed/1000, elapsed%1000); - _mi_fprintf(out, arg, "%10s: user: %ld.%03ld s, system: %ld.%03ld s, faults: %lu, rss: ", "process", - user_time/1000, user_time%1000, sys_time/1000, sys_time%1000, (unsigned long)page_faults ); - mi_printf_amount((int64_t)peak_rss, 1, out, arg, "%s"); - if (peak_commit > 0) { - _mi_fprintf(out, arg, ", commit: "); - mi_printf_amount((int64_t)peak_commit, 1, out, arg, "%s"); - } - _mi_fprintf(out, arg, "\n"); -} - -static mi_msecs_t mi_process_start; // = 0 - -static mi_stats_t* mi_stats_get_default(void) { - mi_heap_t* heap = mi_heap_get_default(); - return &heap->tld->stats; -} - -static void mi_stats_merge_from(mi_stats_t* stats) { - if (stats != &_mi_stats_main) { - mi_stats_add(&_mi_stats_main, stats); - memset(stats, 0, sizeof(mi_stats_t)); - } -} - -void mi_stats_reset(void) mi_attr_noexcept { - mi_stats_t* stats = mi_stats_get_default(); - if (stats != &_mi_stats_main) { memset(stats, 0, sizeof(mi_stats_t)); } - memset(&_mi_stats_main, 0, sizeof(mi_stats_t)); - if (mi_process_start == 0) { mi_process_start = _mi_clock_start(); }; -} - -void mi_stats_merge(void) mi_attr_noexcept { - mi_stats_merge_from( mi_stats_get_default() ); -} - -void _mi_stats_done(mi_stats_t* stats) { // called from `mi_thread_done` - mi_stats_merge_from(stats); -} - -void mi_stats_print_out(mi_output_fun* out, void* arg) mi_attr_noexcept { - mi_stats_merge_from(mi_stats_get_default()); - _mi_stats_print(&_mi_stats_main, out, arg); -} - -void mi_stats_print(void* out) mi_attr_noexcept { - // for compatibility there is an `out` parameter (which can be `stdout` or `stderr`) - mi_stats_print_out((mi_output_fun*)out, NULL); -} - -void mi_thread_stats_print_out(mi_output_fun* out, void* arg) mi_attr_noexcept { - _mi_stats_print(mi_stats_get_default(), out, arg); -} - - -// ---------------------------------------------------------------- -// Basic timer for convenience; use milli-seconds to avoid doubles -// ---------------------------------------------------------------- - -static mi_msecs_t mi_clock_diff; - -mi_msecs_t _mi_clock_now(void) { - return _mi_prim_clock_now(); -} - -mi_msecs_t _mi_clock_start(void) { - if (mi_clock_diff == 0.0) { - mi_msecs_t t0 = _mi_clock_now(); - mi_clock_diff = _mi_clock_now() - t0; - } - return _mi_clock_now(); -} - -mi_msecs_t _mi_clock_end(mi_msecs_t start) { - mi_msecs_t end = _mi_clock_now(); - return (end - start - mi_clock_diff); -} - - -// -------------------------------------------------------- -// Basic process statistics -// -------------------------------------------------------- - -mi_decl_export void mi_process_info(size_t* elapsed_msecs, size_t* user_msecs, size_t* system_msecs, size_t* current_rss, size_t* peak_rss, size_t* current_commit, size_t* peak_commit, size_t* page_faults) mi_attr_noexcept -{ - mi_process_info_t pinfo; - _mi_memzero_var(pinfo); - pinfo.elapsed = _mi_clock_end(mi_process_start); - pinfo.current_commit = (size_t)(mi_atomic_loadi64_relaxed((_Atomic(int64_t)*)&_mi_stats_main.committed.current)); - pinfo.peak_commit = (size_t)(mi_atomic_loadi64_relaxed((_Atomic(int64_t)*)&_mi_stats_main.committed.peak)); - pinfo.current_rss = pinfo.current_commit; - pinfo.peak_rss = pinfo.peak_commit; - pinfo.utime = 0; - pinfo.stime = 0; - pinfo.page_faults = 0; - - _mi_prim_process_info(&pinfo); - - if (elapsed_msecs!=NULL) *elapsed_msecs = (pinfo.elapsed < 0 ? 0 : (pinfo.elapsed < (mi_msecs_t)PTRDIFF_MAX ? (size_t)pinfo.elapsed : PTRDIFF_MAX)); - if (user_msecs!=NULL) *user_msecs = (pinfo.utime < 0 ? 0 : (pinfo.utime < (mi_msecs_t)PTRDIFF_MAX ? (size_t)pinfo.utime : PTRDIFF_MAX)); - if (system_msecs!=NULL) *system_msecs = (pinfo.stime < 0 ? 0 : (pinfo.stime < (mi_msecs_t)PTRDIFF_MAX ? (size_t)pinfo.stime : PTRDIFF_MAX)); - if (current_rss!=NULL) *current_rss = pinfo.current_rss; - if (peak_rss!=NULL) *peak_rss = pinfo.peak_rss; - if (current_commit!=NULL) *current_commit = pinfo.current_commit; - if (peak_commit!=NULL) *peak_commit = pinfo.peak_commit; - if (page_faults!=NULL) *page_faults = pinfo.page_faults; -} diff --git a/Objects/mimalloc/util/libc.c b/Objects/mimalloc/util/libc.c new file mode 100644 index 000000000000000..19bd60d55f53d4d --- /dev/null +++ b/Objects/mimalloc/util/libc.c @@ -0,0 +1,583 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +// -------------------------------------------------------- +// This module defines various standard libc functions to reduce +// the dependency on libc, and also prevent errors caused +// by some libc implementations when called before `main` +// executes (due to malloc redirection) +// -------------------------------------------------------- + +#include "mimalloc.h" +#include "mimalloc/prim.h" // mi_prim_getenv +#include "mimalloc/prim/prim-tls.h" +#include "mimalloc/util/options.h" +#include "mimalloc/util/libc.h" + +#if MI_DEBUG && NDEBUG +#if defined(_MSC_VER) +#pragma message("mimalloc assertions enabled in a release build") +#else +#warning "mimalloc assertions enabled in a release build" +#endif +#endif + +char _mi_toupper(char c) { + if (c >= 'a' && c <= 'z') return (c - 'a' + 'A'); + else return c; +} + +int _mi_strnicmp(const char* s, const char* t, size_t n) { + mi_assert_internal(s!=NULL && t!=NULL); + if (n == 0) return 0; + for (; *s != 0 && *t != 0 && n > 0; s++, t++, n--) { + if (_mi_toupper(*s) != _mi_toupper(*t)) break; + } + return (n == 0 ? 0 : *s - *t); +} + +bool _mi_streq(const char* s, const char* t) { + if (s==NULL && t==NULL) return true; + if (s==NULL || t==NULL) return false; + for (; *s != 0 && *t != 0; s++, t++) { + if (*s != *t) break; + } + return (*s == *t); +} + +bool _mi_strlcpy(char* dest, const char* src, size_t dest_size) { + if (dest==NULL || src==NULL || dest_size == 0) return (src==NULL || *src==0); + // copy until end of src, or when dest is (almost) full + while (*src != 0 && dest_size > 1) { + *dest++ = *src++; + dest_size--; + } + // always zero terminate + *dest = 0; + return (*src == 0); +} + +bool _mi_strlcat(char* dest, const char* src, size_t dest_size) { + if (dest==NULL || src==NULL || dest_size == 0) return (src==NULL || *src==0); + // find end of string in the dest buffer + while (*dest != 0 && dest_size > 1) { + dest++; + dest_size--; + } + // and catenate + return _mi_strlcpy(dest, src, dest_size); +} + +size_t _mi_strnlen(const char* s, size_t max_len) { + if (s==NULL) return 0; + size_t len = 0; + while(len < max_len && s[len] != 0) { len++; } + return len; +} + +size_t _mi_strlen(const char* s) { + return _mi_strnlen(s,PTRDIFF_MAX); +} + +char* _mi_strnstr(char* s, size_t max_len, const char* pat) { + if (s==NULL) return NULL; + if (pat==NULL) return s; + const size_t m = _mi_strnlen(s, max_len); + const size_t n = _mi_strlen(pat); + for (size_t start = 0; start + n <= m; start++) { + size_t i = 0; + while (i 0 ? 0 : (res == 0 ? ENOENT : EAGAIN)); +} +#endif + + +// -------------------------------------------------------- +// Define our own primitives for doing an action once +// -------------------------------------------------------- + +// Returns `true` only on the first invocation, signifying we can execute an action once. +// If it returns `true`, the caller should call `_mi_atomic_once_release` after performing the action. +// Other threads (than the initial thread that entered) will block until `_mi_atomic_once_release` has been called. +bool _mi_atomic_once_enter(mi_atomic_once_t* once) { + const uintptr_t once_tid = mi_atomic_load_acquire(&once->tid); + if mi_likely(once_tid == 1) { + return false; // already executed + } + const mi_threadid_t current_tid = _mi_thread_id(); + if (once_tid == current_tid) { + return false; // recursive invocation; don't block on ourselves + } + + mi_lock_acquire(&once->lock); + uintptr_t expected = 0; + if (mi_atomic_cas_strong_acq_rel(&once->tid, &expected, current_tid)) { // could use atomic_load/store as well + return true; // should execute and release + } + else { + mi_lock_release(&once->lock); + return false; // already another thread entered and released + } +} + +void _mi_atomic_once_release(mi_atomic_once_t* once) { + if (mi_atomic_load_acquire(&once->tid)>1) { // paranoia + mi_atomic_store_release(&once->tid,1); // done executing + mi_lock_release(&once->lock); + } +} + +#if MI_USE_PTHREADS +mi_decl_noinline bool _mi_pthread_key_create(pthread_key_t* pkey, void (*destruct)(void*), void* init) { + int err = pthread_key_create(pkey,destruct); + if mi_unlikely(err!=0) { + *pkey = MI_PTHREAD_KEY_INVALID; + _mi_error_message(ENOMEM,"unable to allocate a thread local variable (error %d)\n", err); + return false; + } + mi_assert_internal(*pkey != MI_PTHREAD_KEY_INVALID); + if (init!=NULL) { + pthread_setspecific(*pkey,init); + }; + mi_assert_internal(pthread_getspecific(*pkey)==init); + return true; +} +#endif + +// -------------------------------------------------------- +// Detect CPU features +// -------------------------------------------------------- +mi_decl_cache_align bool _mi_cpu_has_popcnt = false; + +#if (MI_ARCH_X64 || MI_ARCH_X86) +#if defined(__GNUC__) +// #include +static bool mi_cpuid(uint32_t* regs4, uint32_t level, uint32_t sublevel) { + // note: use explicit assembly instead of __get_cpuid as we need the sublevel (in ecx) + // (on Ubuntu 22 with WSL the __get_cpuid does not clear ecx for level 7 which is incorrect). + uint32_t eax, ebx, ecx, edx; + __asm __volatile("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) : "a"(level), "c"(sublevel) : ); + regs4[0] = eax; + regs4[1] = ebx; + regs4[2] = ecx; + regs4[3] = edx; + return true; +} + +#elif defined(_MSC_VER) +static bool mi_cpuid(uint32_t* regs4, uint32_t level, uint32_t sublevel) { + __cpuidex((int32_t*)regs4, (int32_t)level, (int32_t)sublevel); + return true; +} +#else +static bool mi_cpuid(uint32_t* regs4, uint32_t level, uint32_t sublevel) { + MI_UNUSED(regs4); MI_UNUSED(level); MI_UNUSED(sublevel); + return false; +} +#endif + +void _mi_detect_cpu_features(void) { + uint32_t cpu_info[4]; + if (mi_cpuid(cpu_info, 1, 0)) { + _mi_cpu_has_popcnt = ((cpu_info[2] & (1 << 23)) != 0); // bit 23 of ECX : see + } +} + +#else +void _mi_detect_cpu_features(void) { + #if MI_ARCH_ARM64 || defined(__riscv_zbb) || defined(__riscv_b) + _mi_cpu_has_popcnt = true; + #endif +} +#endif + + +// -------------------------------------------------------- +// Define our own limited `_mi_vsnprintf` and `_mi_snprintf` +// This is mostly to avoid calling these when libc is not yet +// initialized (and to reduce dependencies) +// +// format: d i, p x u, s +// prec: z l ll L l8 l4 // l8=64-bit, l4=32-bit +// width: 10 +// align-left: - +// fill: 0 +// plus: + +// -------------------------------------------------------- + +static void mi_outc(char c, char** out, char* end) { + char* p = *out; + if (p >= end) return; + *p = c; + *out = p + 1; +} + +static void mi_outs(const char* s, char** out, char* end) { + if (s == NULL) return; + char* p = *out; + while (*s != 0 && p < end) { + *p++ = *s++; + } + *out = p; +} + +static void mi_out_fill(char fill, size_t len, char** out, char* end) { + char* p = *out; + for (size_t i = 0; i < len && p < end; i++) { + *p++ = fill; + } + *out = p; +} + +static void mi_out_alignright(char fill, char* start, size_t len, size_t extra, char* end) { + if (len == 0 || extra == 0) return; + if (start + len + extra >= end) return; + // move `len` characters to the right (in reverse since it can overlap) + for (size_t i = 1; i <= len; i++) { + start[len + extra - i] = start[len - i]; + } + // and fill the start + for (size_t i = 0; i < extra; i++) { + start[i] = fill; + } +} + + +static void mi_out_num(uintmax_t x, size_t base, char prefix, char** out, char* end) +{ + if (x == 0 || base == 0 || base > 16) { + if (prefix != 0) { mi_outc(prefix, out, end); } + mi_outc('0',out,end); + } + else { + #define MI_MAX_OUT_DIGITS (160) /* a 512 bit number has 155 digits */ + char num[MI_MAX_OUT_DIGITS]; + int dcount = 0; + while(x>0 && dcount < MI_MAX_OUT_DIGITS) { + char digit = (char)(x % base); + num[dcount++] = (digit <= 9 ? '0' + digit : 'A' + digit - 10); + x = x / base; + } + if (dcount>=MI_MAX_OUT_DIGITS) return; // don't output anything? + if (prefix != 0) { + mi_outc(prefix, out, end); + } + while(dcount-- > 0) { + mi_outc(num[dcount], out, end); + } + } +} + + +#define MI_NEXTC() c = *in; if (c==0) break; in++; + +int _mi_vsnprintf(char* buf, size_t bufsize, const char* fmt, va_list args) { + if (buf == NULL || bufsize == 0 || fmt == NULL) return 0; + buf[bufsize - 1] = 0; + char* const end = buf + (bufsize - 1); + const char* in = fmt; + char* out = buf; + while (true) { + if (out >= end) break; + char c; + MI_NEXTC(); + if (c != '%') { + if (c == '\\') { + MI_NEXTC(); + switch (c) { + case 'e': mi_outc('\x1B', &out, end); break; + case 't': mi_outc('\t', &out, end); break; + case 'n': mi_outc('\n', &out, end); break; + case 'r': mi_outc('\r', &out, end); break; + case '\\': mi_outc('\\', &out, end); break; + default: /* ignore */ break; + } + } + else if ((c >= ' ' && c <= '~') || c=='\n' || c=='\r' || c=='\t' || c=='\x1b') { // output visible ascii or standard control only + mi_outc(c, &out, end); + } + } + else { + MI_NEXTC(); + char fill = ' '; + size_t width = 0; + char numtype = 'd'; + char numplus = 0; + bool alignright = true; + if (c == '+' || c == ' ') { numplus = c; MI_NEXTC(); } + if (c == '-') { alignright = false; MI_NEXTC(); } + if (c == '0') { fill = '0'; MI_NEXTC(); } + if (c >= '1' && c <= '9') { + width = (c - '0'); MI_NEXTC(); + while (c >= '0' && c <= '9') { + if (width < SIZE_MAX/1024) { // no overflow + width = (10 * width) + (c - '0'); + } + MI_NEXTC(); + } + if (c == 0) break; // extra check due to while + } + if (c == 'z' || c == 't' || c == 'L') { + numtype = c; MI_NEXTC(); + } + else if (c == 'l') { + numtype = c; MI_NEXTC(); + if (c == 'l') { numtype = 'L'; MI_NEXTC(); } + else if (c == '8') { numtype = '8'; MI_NEXTC(); } // 64-bit + else if (c == '4') { numtype = '4'; MI_NEXTC(); } // 32-bit + } + + + char* start = out; + if (c == '%') { + mi_outc('%', &out, end); + } + else if (c == 's') { + // string + const char* s = va_arg(args, const char*); + mi_outs(s, &out, end); + } + else if (c == 'p' || c == 'x' || c == 'u') { + // unsigned + uintmax_t x = 0; + if (c == 'x' || c == 'u') { + if (numtype == 'z') x = va_arg(args, size_t); + else if (numtype == 't') x = va_arg(args, uintptr_t); // unsigned ptrdiff_t + else if (numtype == 'L') x = va_arg(args, unsigned long long); + else if (numtype == 'l') x = va_arg(args, unsigned long); + else if (numtype == '8') x = va_arg(args, uint64_t); + else if (numtype == '4') x = va_arg(args, uint32_t); + else x = va_arg(args, unsigned int); + } + else if (c == 'p') { + void* const p = va_arg(args, void*); + x = (uintptr_t)p; + mi_outs("0x", &out, end); + start = out; + width = (width >= 2 ? width - 2 : 0); + } + if (width == 0 && (c == 'x' || c == 'p')) { + if (c == 'p') { width = 2 * (x <= UINT32_MAX ? 4 : ((x >> 16) <= UINT32_MAX ? 6 : sizeof(void*))); } + if (width == 0) { width = 2; } + if (alignright) { fill = '0'; } + } + mi_out_num(x, (c == 'x' || c == 'p' ? 16 : 10), numplus, &out, end); + } + else if (c == 'i' || c == 'd') { + // signed + intmax_t x = 0; + if (numtype == 'z') x = va_arg(args, intptr_t ); + else if (numtype == 't') x = va_arg(args, ptrdiff_t); + else if (numtype == 'L') x = va_arg(args, long long); + else if (numtype == 'l') x = va_arg(args, long); + else if (numtype == '8') x = va_arg(args, int64_t); + else if (numtype == '4') x = va_arg(args, int32_t); + else x = va_arg(args, int); + char pre = 0; + if (x < 0) { + pre = '-'; + if (x > INTMAX_MIN) { x = -x; } + } + else if (numplus != 0) { + pre = numplus; + } + mi_out_num((uintmax_t)x, 10, pre, &out, end); + } + else if (c >= ' ' && c <= '~') { + // unknown format + mi_outc('%', &out, end); + mi_outc(c, &out, end); + } + + // fill & align + mi_assert_internal(out <= end); + mi_assert_internal(out >= start); + const size_t len = out - start; + if (len < width) { + mi_out_fill(fill, width - len, &out, end); + if (alignright && out <= end) { + mi_out_alignright(fill, start, len, width - len, end); + } + } + } + } + mi_assert_internal(out <= end); + *out = 0; + return (int)(out - buf); +} + +int _mi_snprintf(char* buf, size_t buflen, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + const int written = _mi_vsnprintf(buf, buflen, fmt, args); + va_end(args); + return written; +} + +#undef MI_NEXTC + + +// -------------------------------------------------------- +// generic trailing and leading zero count, and popcount +// -------------------------------------------------------- + +#if !MI_HAS_FAST_BITSCAN + +static size_t mi_ctz_generic32(uint32_t x) { + // de Bruijn multiplication, see + static const uint8_t debruijn[32] = { + 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, + 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9 + }; + if (x==0) return 32; + return debruijn[(uint32_t)((x & (~x + 1U)) * (uint32_t)(0x077CB531U)) >> 27]; +} + +static size_t mi_clz_generic32(uint32_t x) { + // de Bruijn multiplication, see + static const uint8_t debruijn[32] = { + 31, 22, 30, 21, 18, 10, 29, 2, 20, 17, 15, 13, 9, 6, 28, 1, + 23, 19, 11, 3, 16, 14, 7, 24, 12, 4, 8, 25, 5, 26, 27, 0 + }; + if (x==0) return 32; + x |= x >> 1; + x |= x >> 2; + x |= x >> 4; + x |= x >> 8; + x |= x >> 16; + return debruijn[(uint32_t)(x * (uint32_t)(0x07C4ACDDU)) >> 27]; +} + +size_t _mi_ctz_generic(size_t x) { + if (x==0) return MI_SIZE_BITS; + #if (MI_SIZE_BITS <= 32) + return mi_ctz_generic32((uint32_t)x); + #else + const uint32_t lo = (uint32_t)x; + if (lo != 0) { + return mi_ctz_generic32(lo); + } + else { + return (32 + mi_ctz_generic32((uint32_t)(x>>32))); + } + #endif +} + +size_t _mi_clz_generic(size_t x) { + if (x==0) return MI_SIZE_BITS; + #if (MI_SIZE_BITS <= 32) + return mi_clz_generic32((uint32_t)x); + #else + const uint32_t hi = (uint32_t)(x>>32); + if (hi != 0) { + return mi_clz_generic32(hi); + } + else { + return 32 + mi_clz_generic32((uint32_t)x); + } + #endif +} + +#endif // bit scan + + +#if MI_SIZE_SIZE == 4 +#define mi_mask_even_bits32 (0x55555555) +#define mi_mask_even_pairs32 (0x33333333) +#define mi_mask_even_nibbles32 (0x0F0F0F0F) + +// sum of all the bytes in `x` if it is guaranteed that the sum < 256! +static size_t mi_byte_sum32(uint32_t x) { + // perform `x * 0x01010101`: the highest byte contains the sum of all bytes. + x += (x << 8); + x += (x << 16); + return (size_t)(x >> 24); +} + +static size_t mi_popcount_generic32(uint32_t x) { + // first count each 2-bit group `a`, where: a==0b00 -> 00, a==0b01 -> 01, a==0b10 -> 01, a==0b11 -> 10 + // in other words, `a - (a>>1)`; to do this in parallel, we need to mask to prevent spilling a bit pair + // into the lower bit-pair: + x = x - ((x >> 1) & mi_mask_even_bits32); + // add the 2-bit pair results + x = (x & mi_mask_even_pairs32) + ((x >> 2) & mi_mask_even_pairs32); + // add the 4-bit nibble results + x = (x + (x >> 4)) & mi_mask_even_nibbles32; + // each byte now has a count of its bits, we can sum them now: + return mi_byte_sum32(x); +} + +mi_decl_noinline size_t _mi_popcount_generic(size_t x) { + if (x<=1) return x; + if (~x==0) return MI_SIZE_BITS; + return mi_popcount_generic32(x); +} + +#else +#define mi_mask_even_bits64 (0x5555555555555555) +#define mi_mask_even_pairs64 (0x3333333333333333) +#define mi_mask_even_nibbles64 (0x0F0F0F0F0F0F0F0F) + +// sum of all the bytes in `x` if it is guaranteed that the sum < 256! +static size_t mi_byte_sum64(uint64_t x) { + x += (x << 8); + x += (x << 16); + x += (x << 32); + return (size_t)(x >> 56); +} + +static size_t mi_popcount_generic64(uint64_t x) { + x = x - ((x >> 1) & mi_mask_even_bits64); + x = (x & mi_mask_even_pairs64) + ((x >> 2) & mi_mask_even_pairs64); + x = (x + (x >> 4)) & mi_mask_even_nibbles64; + return mi_byte_sum64(x); +} + +mi_decl_noinline size_t _mi_popcount_generic(size_t x) { + if (x<=1) return x; + if (~x==0) return MI_SIZE_BITS; + return mi_popcount_generic64(x); +} +#endif + diff --git a/Objects/mimalloc/util/options.c b/Objects/mimalloc/util/options.c new file mode 100644 index 000000000000000..a142d1570eacb12 --- /dev/null +++ b/Objects/mimalloc/util/options.c @@ -0,0 +1,753 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#include "mimalloc.h" +#include "mimalloc/prim.h" // mi_prim_out_stderr +#include "mimalloc/prim/prim-tls.h" +#include "mimalloc/alloc/page.h" +#include "mimalloc/util/options.h" + +#include // stdin/stdout +#include // abort + +static long mi_max_error_count = 16; // stop outputting errors after this (use < 0 for no limit) +static long mi_max_warning_count = 16; // stop outputting warnings after this (use < 0 for no limit) + +static void mi_add_stderr_output(void); + +int mi_version(void) { + return MI_MALLOC_VERSION; +} + + +// -------------------------------------------------------- +// Options +// These can be accessed by multiple threads and may be +// concurrently initialized, but an initializing data race +// is ok since they resolve to the same value. +// -------------------------------------------------------- + + +#define MI_OPTION(opt) mi_option_##opt, #opt, NULL +#define MI_OPTION_LEGACY(opt,legacy) mi_option_##opt, #opt, #legacy + +// Some options can be set at build time for statically linked libraries +// (use `-DMI_EXTRA_CPPDEFS="opt1=val1;opt2=val2"`) +// +// This is useful if we cannot pass them as environment variables +// (and setting them programmatically would be too late) + +#ifndef MI_DEFAULT_VERBOSE +#define MI_DEFAULT_VERBOSE 0 +#endif + +#ifndef MI_DEFAULT_ARENA_EAGER_COMMIT +#define MI_DEFAULT_ARENA_EAGER_COMMIT 2 +#endif + +// in KiB +#ifndef MI_DEFAULT_ARENA_RESERVE + #if (MI_SIZE_SIZE>4) + #define MI_DEFAULT_ARENA_RESERVE 1024L*1024L + #else + #define MI_DEFAULT_ARENA_RESERVE 128L*1024L + #endif +#endif + +#ifndef MI_DEFAULT_ARENA_MAX_OBJECT_SIZE +#define MI_DEFAULT_ARENA_MAX_OBJECT_SIZE ((MI_SIZE_BITS * MI_ARENA_MAX_CHUNK_OBJ_SIZE)/MI_KiB) /* 2 GiB (or 256 MiB on 32-bit), larger than this is alloc'd by the OS */ +#endif + +#ifndef MI_DEFAULT_DISALLOW_ARENA_ALLOC +#define MI_DEFAULT_DISALLOW_ARENA_ALLOC 0 +#endif + +#ifndef MI_DEFAULT_ALLOW_LARGE_OS_PAGES +#define MI_DEFAULT_ALLOW_LARGE_OS_PAGES 0 +#endif + +#ifndef MI_DEFAULT_RESERVE_HUGE_OS_PAGES +#define MI_DEFAULT_RESERVE_HUGE_OS_PAGES 0 +#endif + +#ifndef MI_DEFAULT_RESERVE_OS_MEMORY +#define MI_DEFAULT_RESERVE_OS_MEMORY 0 +#endif + +#ifndef MI_DEFAULT_GUARDED_SAMPLE_RATE +#if MI_GUARDED && MI_DEBUG +#define MI_DEFAULT_GUARDED_SAMPLE_RATE 0 /* MI_MiB */ +#else +#define MI_DEFAULT_GUARDED_SAMPLE_RATE 0 +#endif +#endif + +#ifndef MI_DEFAULT_PAGEMAP_COMMIT +#if defined(__APPLE__) && MI_PAGE_MAP_FLAT // when overloading malloc, we still get mixed pointers sometimes on macOS; this avoids a bad access +#define MI_DEFAULT_PAGEMAP_COMMIT 1 +#else +#define MI_DEFAULT_PAGEMAP_COMMIT 0 +#endif +#endif + +#ifndef MI_DEFAULT_PAGE_MAX_RECLAIM +#define MI_DEFAULT_PAGE_MAX_RECLAIM (-1) // unlimited +#endif + +#ifndef MI_DEFAULT_PAGE_FULL_RETAIN +#define MI_DEFAULT_PAGE_FULL_RETAIN 2 +#endif + +#ifndef MI_DEFAULT_PAGE_CROSS_THREAD_MAX_RECLAIM +#define MI_DEFAULT_PAGE_CROSS_THREAD_MAX_RECLAIM 32 +#endif + +#ifndef MI_DEFAULT_ALLOW_THP +#if defined(__ANDROID__) +#define MI_DEFAULT_ALLOW_THP 0 +#else +// #define MI_DEFAULT_ALLOW_THP 1 // allow THP but purging may split up THP pages +#define MI_DEFAULT_ALLOW_THP 2 // allow THP and set the minimal purge size to 2MiB to avoid breaking them up +#endif +#endif + +#ifndef MI_DEFAULT_STATS_MERGE_ON_COLLECT +#define MI_DEFAULT_STATS_MERGE_ON_COLLECT 1 +#endif + +#ifndef MI_DEFAULT_STATS_MERGE_THRESHOLD +#define MI_DEFAULT_STATS_MERGE_THRESHOLD (512*1024) // 512 KiB +#endif + +#ifndef MI_DEFAULT_ARENA_PURGE_IMMEDIATE_SIZE +#define MI_DEFAULT_ARENA_PURGE_IMMEDIATE_SIZE ((MI_SIZE_SIZE * MI_LARGE_PAGE_SIZE) / MI_KiB) // 32 MiB (in KiB) +#endif + +// Static options +static mi_option_desc_t mi_options[_mi_option_last] = +{ + // stable options +#if MI_DEBUG || defined(MI_SHOW_ERRORS) + { 1, MI_OPTION_UNINIT, MI_OPTION(show_errors) }, +#else + { 0, MI_OPTION_UNINIT, MI_OPTION(show_errors) }, +#endif + { 0, MI_OPTION_UNINIT, MI_OPTION(show_stats) }, + { MI_DEFAULT_VERBOSE, MI_OPTION_UNINIT, MI_OPTION(verbose) }, + + // some of the following options are experimental and not all combinations are allowed. + { 1, MI_OPTION_UNINIT, MI_OPTION(deprecated_eager_commit) }, + { MI_DEFAULT_ARENA_EAGER_COMMIT, + MI_OPTION_UNINIT, MI_OPTION_LEGACY(arena_eager_commit,eager_region_commit) }, // eager commit arena's? 2 is used to enable this only on an OS that has overcommit (i.e. linux) + { 1, MI_OPTION_UNINIT, MI_OPTION_LEGACY(purge_decommits,reset_decommits) }, // purge decommits memory (instead of reset) (note: on linux this uses MADV_DONTNEED for decommit) + { MI_DEFAULT_ALLOW_LARGE_OS_PAGES, + MI_OPTION_UNINIT, MI_OPTION_LEGACY(allow_large_os_pages,large_os_pages) }, // use large OS pages, use only with eager commit to prevent fragmentation of VMA's + { MI_DEFAULT_RESERVE_HUGE_OS_PAGES, + MI_OPTION_UNINIT, MI_OPTION(reserve_huge_os_pages) }, // per 1GiB huge pages + {-1, MI_OPTION_UNINIT, MI_OPTION(reserve_huge_os_pages_at) }, // reserve huge pages at node N + { MI_DEFAULT_RESERVE_OS_MEMORY, + MI_OPTION_UNINIT, MI_OPTION(reserve_os_memory) }, // reserve N KiB OS memory in advance (use `option_get_size`) + { 0, MI_OPTION_UNINIT, MI_OPTION(deprecated_segment_cache) }, // cache N segments per thread + { 0, MI_OPTION_UNINIT, MI_OPTION(deprecated_page_reset) }, // reset page memory on free + { 0, MI_OPTION_UNINIT, MI_OPTION(deprecated_abandoned_page_purge) }, + { 0, MI_OPTION_UNINIT, MI_OPTION(deprecated_segment_reset) }, // reset segment memory on free (needs eager commit) + { 1, MI_OPTION_UNINIT, MI_OPTION(deprecated_eager_commit_delay) }, + { 1000,MI_OPTION_UNINIT, MI_OPTION_LEGACY(purge_delay,reset_delay) }, // purge delay in milli-seconds + { 0, MI_OPTION_UNINIT, MI_OPTION(use_numa_nodes) }, // 0 = use available numa nodes, otherwise use at most N nodes. + { 0, MI_OPTION_UNINIT, MI_OPTION_LEGACY(disallow_os_alloc,limit_os_alloc) }, // 1 = do not use OS memory for allocation (but only reserved arenas) + { 100, MI_OPTION_UNINIT, MI_OPTION(os_tag) }, // only apple specific for now but might serve more or less related purpose + { 32, MI_OPTION_UNINIT, MI_OPTION(max_errors) }, // maximum errors that are output + { 32, MI_OPTION_UNINIT, MI_OPTION(max_warnings) }, // maximum warnings that are output + { 10, MI_OPTION_UNINIT, MI_OPTION(deprecated_max_segment_reclaim)}, // max. percentage of the abandoned segments to be reclaimed per try. + { 0, MI_OPTION_UNINIT, MI_OPTION(destroy_on_exit)}, // release all OS memory on process exit; careful with dangling pointer or after-exit frees! + { MI_DEFAULT_ARENA_RESERVE, MI_OPTION_UNINIT, MI_OPTION(arena_reserve) }, // reserve memory N KiB at a time (=1GiB) (use `option_get_size`) + { 4, MI_OPTION_UNINIT, MI_OPTION(arena_purge_mult) }, // purge delay multiplier for arena's + { 1, MI_OPTION_UNINIT, MI_OPTION_LEGACY(deprecated_purge_extend_delay, decommit_extend_delay) }, + { MI_DEFAULT_DISALLOW_ARENA_ALLOC, MI_OPTION_UNINIT, MI_OPTION(disallow_arena_alloc) }, // 1 = do not use arena's for allocation (except if using specific arena id's) + { 400, MI_OPTION_UNINIT, MI_OPTION(retry_on_oom) }, // windows only: retry on out-of-memory for N milli seconds (=400), set to 0 to disable retries. + { 1, MI_OPTION_UNINIT, MI_OPTION(deprecated_visit_abandoned) }, + { 0, MI_OPTION_UNINIT, MI_OPTION(guarded_min) }, // only used when building with MI_GUARDED: minimal rounded object size for guarded objects + { MI_GiB, MI_OPTION_UNINIT, MI_OPTION(guarded_max) }, // only used when building with MI_GUARDED: maximal rounded object size for guarded objects + { 0, MI_OPTION_UNINIT, MI_OPTION(guarded_precise) }, // disregard minimal alignment requirement to always place guarded blocks exactly in front of a guard page (=0) + { MI_DEFAULT_GUARDED_SAMPLE_RATE, + MI_OPTION_UNINIT, MI_OPTION(guarded_sample_rate)}, // 1 out of N allocations in the min/max range will be guarded (=4000) + { 0, MI_OPTION_UNINIT, MI_OPTION(guarded_sample_seed)}, + { 10000, MI_OPTION_UNINIT, MI_OPTION(generic_collect) }, // collect theaps every N (=10000) generic allocation calls + { 1, MI_OPTION_UNINIT, MI_OPTION_LEGACY(page_reclaim_on_free, abandoned_reclaim_on_free) },// reclaim abandoned (small) pages on a free: -1 = disable completely, 0 = only reclaim into the originating theap, 1 = reclaim on free across theaps + { MI_DEFAULT_PAGE_FULL_RETAIN, + MI_OPTION_UNINIT, MI_OPTION(page_full_retain) }, // number of (small) pages to retain in the free page queues + { 4, MI_OPTION_UNINIT, MI_OPTION(page_max_candidates) }, // max search to find a best page candidate + { 0, MI_OPTION_UNINIT, MI_OPTION(max_vabits) }, // max virtual address space bits + { MI_DEFAULT_PAGEMAP_COMMIT, + MI_OPTION_UNINIT, MI_OPTION(pagemap_commit) }, // commit the full pagemap upfront? + { 0, MI_OPTION_UNINIT, MI_OPTION(page_commit_on_demand) }, // commit pages on-demand (2 disables this only on overcommit systems (like Linux)) + { MI_DEFAULT_PAGE_MAX_RECLAIM, + MI_OPTION_UNINIT, MI_OPTION(page_max_reclaim) }, // don't reclaim (small) pages of the same originating theap if we already own N pages in that size class + { MI_DEFAULT_PAGE_CROSS_THREAD_MAX_RECLAIM, + MI_OPTION_UNINIT, MI_OPTION(page_cross_thread_max_reclaim) }, // don't reclaim (small) pages across threads if we already own N pages in that size class + { MI_DEFAULT_ALLOW_THP, + MI_OPTION_UNINIT, MI_OPTION(allow_thp) }, // allow transparent huge pages? (=1) (on Android =0 by default). Set to 0 to disable THP for the process. + { 0, MI_OPTION_UNINIT, MI_OPTION_LEGACY(arena_purge_min_size, minimal_purge_size) }, // set minimal purge size (in KiB) (=0). Using 0 resolves to either 64 (or 2048 if THP is enabled). + { MI_DEFAULT_ARENA_MAX_OBJECT_SIZE, + MI_OPTION_UNINIT, MI_OPTION(arena_max_object_size) }, // set maximal object size that can be allocated in an arena (in KiB) (=2GiB on 64-bit). + { 0, MI_OPTION_UNINIT, MI_OPTION(arena_is_numa_local) }, // associate local numa node with an initial arena allocation + { MI_DEFAULT_STATS_MERGE_ON_COLLECT, + MI_OPTION_UNINIT, MI_OPTION(stats_merge_on_collect) }, // on each theap collect, stats are merged with the parent heap + { MI_MiB, MI_OPTION_UNINIT, MI_OPTION(profile_alloc_interval) },// N KiB (=1 GiB default) between automatic dumps of the `MIMALLOC_PROFILE` heap profiler (use `option_get_size`); 0 disables automatic dumping + { 0, MI_OPTION_UNINIT, MI_OPTION(profile_inuse_interval) }, // N KiB between automatic dumps of the `MIMALLOC_PROFILE` heap profiler whenever in-use bytes grow by that amount (use `option_get_size`); 0 disables (default) + { 0, MI_OPTION_UNINIT, MI_OPTION(profile_time_interval) }, // N seconds between automatic dumps of the `MIMALLOC_PROFILE` heap profiler; 0 disables (default) + { 512, MI_OPTION_UNINIT, MI_OPTION(profile_sample_rate) }, // sample rate in KiB for the `MIMALLOC_PROFILE` heap profiler (use `option_get_size`) (=512 KiB) + { MI_DEFAULT_STATS_MERGE_THRESHOLD, + MI_OPTION_UNINIT, MI_OPTION(stats_merge_threshold) }, // on a page stats update, merge theap stats into the heap if N bytes were freed/allocated (=0, disabled) + { MI_DEFAULT_ARENA_PURGE_IMMEDIATE_SIZE, + MI_OPTION_UNINIT, MI_OPTION(arena_purge_immediate_size) }, // ranges above this size are purged immediately + +}; + +static void mi_option_init(mi_option_desc_t* desc); + +// option value is stored in KiB so it can fit a `long` +// todo: use mi_ssize_t for options? +static bool mi_option_has_size_in_kib(mi_option_t option) { + return (option == mi_option_reserve_os_memory || option == mi_option_arena_reserve || + option == mi_option_arena_purge_min_size || option == mi_option_arena_purge_immediate_size || + option == mi_option_arena_max_object_size + // || option == mi_option_profile_alloc_interval || option == mi_option_profile_inuse_interval || option == mi_option_profile_sample_rate + ); +} + +// option value is stored in bytes as a `long` is enough. +static bool mi_option_has_size_in_bytes(mi_option_t option) { + return (option == mi_option_stats_merge_threshold || + option == mi_option_profile_alloc_interval || option == mi_option_profile_inuse_interval || option == mi_option_profile_sample_rate); +} + +void _mi_options_init(void) { + // called on process load + for(int i = 0; i < _mi_option_last; i++ ) { + mi_option_t option = (mi_option_t)i; + long l = mi_option_get(option); MI_UNUSED(l); // initialize + } + mi_max_error_count = mi_option_get(mi_option_max_errors); + mi_max_warning_count = mi_option_get(mi_option_max_warnings); + #if MI_GUARDED + if (mi_option_get(mi_option_guarded_sample_rate) > 0) { + if (mi_option_is_enabled(mi_option_allow_large_os_pages)) { + mi_option_disable(mi_option_allow_large_os_pages); + _mi_warning_message("option 'allow_large_os_pages' is disabled to allow for guarded objects\n"); + } + } + #endif +} + +// called at actual process load, it should be safe to print now +void _mi_options_post_init(void) { + mi_add_stderr_output(); // now it safe to use stderr for output + if (mi_option_is_enabled(mi_option_verbose)) { mi_options_print(); } +} + +#define mi_stringifyx(str) #str // and stringify +#define mi_stringify(str) mi_stringifyx(str) // expand + +mi_decl_export void mi_options_print_out(mi_output_fun* out, void* arg) mi_attr_noexcept +{ + // show version + const int vermajor = MI_MALLOC_VERSION/10000; + const int verminor = (MI_MALLOC_VERSION%10000)/100; + const int verpatch = (MI_MALLOC_VERSION%100); + _mi_fprintf(out, arg, "v%i.%i.%i%s%s\n", vermajor, verminor, verpatch, + #if defined(MI_CMAKE_BUILD_TYPE) + ", " mi_stringify(MI_CMAKE_BUILD_TYPE) + #else + "" + #endif + , + #if defined(MI_GIT_DESCRIBE) + ", git " mi_stringify(MI_GIT_DESCRIBE) + #else + "" + #endif + ); + + // show options + for (int i = 0; i < _mi_option_last; i++) { + mi_option_t option = (mi_option_t)i; + long l = mi_option_get(option); MI_UNUSED(l); // possibly initialize + mi_option_desc_t* desc = &mi_options[option]; + _mi_fprintf(out, arg, "option '%s': %ld %s\n", desc->name, desc->value, (mi_option_has_size_in_kib(option) ? "KiB" : (mi_option_has_size_in_bytes(option) ? "B" : ""))); + } + + // show build configuration + #if MI_PAGE_META_IS_ALIGNED && MI_PAGE_META_SMALL_IS_ALIGNED + _mi_fprintf(out, arg, "free mode : (small) aligned, page size: %zu\n", sizeof(mi_page_t)); + #elif MI_PAGE_META_IS_ALIGNED + _mi_fprintf(out, arg, "free mode : aligned, page size: %zu\n", sizeof(mi_page_t)); + #elif MI_PAGE_META_SMALL_IS_ALIGNED + _mi_fprintf(out, arg, "free mode : small aligned + pagemap, page size: %zu\n", sizeof(mi_page_t)); + #elif MI_FREE_IS_CHECKED + _mi_fprintf(out, arg, "free mode : checked, page size: %zu\n", sizeof(mi_page_t)); + #else + _mi_fprintf(out, arg, "free mode : pagemap, page size: %zu\n", sizeof(mi_page_t)); + #endif + #if MI_ENCODE_FREELIST + _mi_fprintf(out, arg, "free lists : encoded with %d key(s)\n", MI_PAGE_KEY_COUNT); + #endif + #if MI_GUARDED + _mi_fprintf(out, arg, "guarded mode: %s, rate=%ld\n", MI_SAMPLE==2 ? "fine-grained" : "enabled", mi_option_get(mi_option_guarded_sample_rate)); + #endif + #if MI_PROFILE + _mi_fprintf(out, arg, "profiling : %s\n", MI_SAMPLE==2 ? "fine-grained" : "enabled"); + #endif + #if MI_TSAN + _mi_fprintf(out, arg, "thread santizer enabled\n"); + #endif + _mi_fprintf(out, arg, "mem tracking: %s\n", MI_TRACK_TOOL); + _mi_fprintf(out, arg, "debug level : %d\n", MI_DEBUG ); + _mi_fprintf(out, arg, "secure level: %d\n", MI_SECURE ); + _mi_fprintf(out, arg, "padding : %s\n", (MI_PADDING==0 ? "none" : (MI_PADDING==1 ? "enabled" : "byte precise"))); +} + +mi_decl_export void mi_options_print(void) mi_attr_noexcept { + mi_options_print_out(NULL, NULL); +} + +long _mi_option_get_fast(mi_option_t option) { + mi_assert(option >= 0 && option < _mi_option_last); + mi_option_desc_t* desc = &mi_options[option]; + mi_assert(desc->option == option); // index should match the option + //mi_assert(desc->init != MI_OPTION_UNINIT); + return desc->value; +} + + +mi_decl_nodiscard long mi_option_get(mi_option_t option) { + mi_assert(option >= 0 && option < _mi_option_last); + if (option < 0 || option >= _mi_option_last) return 0; + mi_option_desc_t* desc = &mi_options[option]; + mi_assert(desc->option == option); // index should match the option + if mi_unlikely(desc->init == MI_OPTION_UNINIT) { + mi_option_init(desc); + } + return desc->value; +} + +mi_decl_nodiscard long mi_option_get_clamp(mi_option_t option, long min, long max) { + long x = mi_option_get(option); + return (x < min ? min : (x > max ? max : x)); +} + +mi_decl_nodiscard size_t mi_option_get_size(mi_option_t option) { + const long x = mi_option_get(option); + size_t size = (x < 0 ? 0 : (size_t)x); + if (mi_option_has_size_in_kib(option)) { + if (mi_mul_overflow(size, MI_KiB, &size)) { + size = MI_MAX_ALLOC_SIZE; + } + } + return size; +} + +void mi_option_set(mi_option_t option, long value) { + mi_assert(option >= 0 && option < _mi_option_last); + if (option < 0 || option >= _mi_option_last) return; + mi_option_desc_t* desc = &mi_options[option]; + mi_assert(desc->option == option); // index should match the option + desc->value = value; + desc->init = MI_OPTION_INITIALIZED; + // ensure min/max range; be careful to not recurse. + if (desc->option == mi_option_guarded_min && _mi_option_get_fast(mi_option_guarded_max) < value) { + mi_option_set(mi_option_guarded_max, value); + } + else if (desc->option == mi_option_guarded_max && _mi_option_get_fast(mi_option_guarded_min) > value) { + mi_option_set(mi_option_guarded_min, value); + } +} + +void mi_option_set_default(mi_option_t option, long value) { + mi_assert(option >= 0 && option < _mi_option_last); + if (option < 0 || option >= _mi_option_last) return; + mi_option_desc_t* desc = &mi_options[option]; + if (desc->init != MI_OPTION_INITIALIZED) { + desc->value = value; + } +} + +mi_decl_nodiscard bool mi_option_is_enabled(mi_option_t option) { + return (mi_option_get(option) != 0); +} + +void mi_option_set_enabled(mi_option_t option, bool enable) { + mi_option_set(option, (enable ? 1 : 0)); +} + +void mi_option_set_enabled_default(mi_option_t option, bool enable) { + mi_option_set_default(option, (enable ? 1 : 0)); +} + +void mi_option_enable(mi_option_t option) { + mi_option_set_enabled(option,true); +} + +void mi_option_disable(mi_option_t option) { + mi_option_set_enabled(option,false); +} + +static void mi_cdecl mi_out_stderr(const char* msg, void* arg) { + MI_UNUSED(arg); + if (msg != NULL && msg[0] != 0) { + _mi_prim_out_stderr(msg); + } +} + +// Since an output function can be registered earliest in the `main` +// function we also buffer output that happens earlier. When +// an output function is registered it is called immediately with +// the output up to that point. +#ifndef MI_MAX_DELAY_OUTPUT +#define MI_MAX_DELAY_OUTPUT ((size_t)(16*1024)) +#endif +static char out_buf[MI_MAX_DELAY_OUTPUT+1]; +static _Atomic(size_t) out_len; +static mi_lock_t out_buf_lock = MI_LOCK_INITIALIZER; + +static void mi_cdecl mi_out_buf(const char* msg, void* arg) { + MI_UNUSED(arg); + if (msg==NULL) return; + if (mi_atomic_load_acquire(&out_len)>=MI_MAX_DELAY_OUTPUT) return; + size_t n = _mi_strlen(msg); + if (n==0 || n >= MI_MAX_DELAY_OUTPUT) return; + // copy msg into the buffer + mi_lock(&out_buf_lock) { + const size_t start = mi_atomic_add_acq_rel(&out_len, n); + if (start < MI_MAX_DELAY_OUTPUT) { + // check bound + if (start+n >= MI_MAX_DELAY_OUTPUT) { + n = MI_MAX_DELAY_OUTPUT-start-1; + } + mi_memcpy(&out_buf[start], msg, n); + } + } +} + +static void mi_out_buf_flush(mi_output_fun* out, bool no_more_buf, void* arg) { + if (out==NULL) return; + // claim (if `no_more_buf == true`, no more output will be added after this point) + mi_lock(&out_buf_lock) { + size_t count = mi_atomic_add_acq_rel(&out_len, (no_more_buf ? MI_MAX_DELAY_OUTPUT : 1)); + // and output the current contents + if (count>MI_MAX_DELAY_OUTPUT) count = MI_MAX_DELAY_OUTPUT; + out_buf[count] = 0; + out(out_buf,arg); + if (!no_more_buf) { + out_buf[count] = '\n'; // if continue with the buffer, insert a newline + } + } +} + + +// Once this module is loaded, switch to this routine +// which outputs to stderr and the delayed output buffer. +static void mi_cdecl mi_out_buf_stderr(const char* msg, void* arg) { + mi_out_stderr(msg,arg); + mi_out_buf(msg,arg); +} + + + +// -------------------------------------------------------- +// Default output handler +// -------------------------------------------------------- + +// The program should only install a single output handler from a single thread +// since otherwise the argument and output function may not match. +static _Atomic(void*) mi_out_default; // = // is `mi_output_fun*` (but some platforms don't support atomic function pointers) +static _Atomic(void*) mi_out_arg; // = NULL + +static mi_output_fun* mi_out_get_default(void** parg) { + mi_output_fun* const out = (mi_output_fun*)mi_atomic_load_ptr_acquire(void,&mi_out_default); + if (parg != NULL) { *parg = mi_atomic_load_ptr_acquire(void,&mi_out_arg); } + return (out == NULL ? &mi_out_buf : out); +} + +void mi_register_output(mi_output_fun* out, void* arg) mi_attr_noexcept { + mi_atomic_store_ptr_release(void,&mi_out_default, (void*)(out == NULL ? &mi_out_stderr : out)); // stop using the delayed output buffer + mi_atomic_store_ptr_release(void,&mi_out_arg, arg); + if (out!=NULL) { mi_out_buf_flush(out,true,arg); } // output all the delayed output now +} + +// add stderr to the delayed output after the module is loaded +static void mi_add_stderr_output(void) { + mi_assert_internal(mi_out_default == NULL); + mi_out_buf_flush(&mi_out_stderr, false, NULL); // flush current contents to stderr + mi_atomic_store_ptr_release(void,&mi_out_default,(void*)&mi_out_buf_stderr); // and add stderr to the delayed output + mi_atomic_store_ptr_release(void,&mi_out_arg,NULL); +} + +// -------------------------------------------------------- +// Messages, all end up calling `_mi_fputs`. +// -------------------------------------------------------- +static _Atomic(size_t) error_count; // = 0; // when >= max_error_count stop emitting errors +static _Atomic(size_t) warning_count; // = 0; // when >= max_warning_count stop emitting warnings + +static mi_decl_noinline bool mi_recurse_enter_prim(void) { + return true; +} + +static mi_decl_noinline void mi_recurse_exit_prim(void) { + /* nothing */ +} + +static bool mi_recurse_enter(void) { + #if defined(__APPLE__) || defined(__ANDROID__) || defined(MI_TLS_RECURSE_GUARD) + if (_mi_preloading()) return false; + #endif + return mi_recurse_enter_prim(); +} + +static void mi_recurse_exit(void) { + #if defined(__APPLE__) || defined(__ANDROID__) || defined(MI_TLS_RECURSE_GUARD) + if (_mi_preloading()) return; + #endif + mi_recurse_exit_prim(); +} + +void _mi_fputs(mi_output_fun* out, void* arg, const char* prefix, const char* message) { + if (out==NULL || (void*)out==(void*)stdout || (void*)out==(void*)stderr) { // todo: use mi_out_stderr for stderr? + if (!mi_recurse_enter()) return; + out = mi_out_get_default(&arg); + if (prefix != NULL) out(prefix, arg); + out(message, arg); + mi_recurse_exit(); + } + else { + if (prefix != NULL) out(prefix, arg); + out(message, arg); + } +} + +// Define our own limited `fprintf` that avoids memory allocation. +// We do this using `_mi_vsnprintf` with a limited buffer. +static void mi_vfprintf( mi_output_fun* out, void* arg, const char* prefix, const char* fmt, va_list args ) { + char buf[992]; + if (fmt==NULL) return; + if (!mi_recurse_enter()) return; + _mi_vsnprintf(buf, sizeof(buf)-1, fmt, args); + mi_recurse_exit(); + _mi_fputs(out,arg,prefix,buf); +} + +void _mi_fprintf( mi_output_fun* out, void* arg, const char* fmt, ... ) { + va_list args; + va_start(args,fmt); + mi_vfprintf(out,arg,NULL,fmt,args); + va_end(args); +} + +static void mi_vfprintf_thread(mi_output_fun* out, void* arg, const char* prefix, const char* fmt, va_list args) { + if (prefix != NULL && _mi_strnlen(prefix,33) <= 32) { // && !_mi_is_main_thread()) { + char tprefix[64]; + _mi_snprintf(tprefix, sizeof(tprefix), "%sthread 0x%tx: ", prefix, (uintptr_t)_mi_thread_id()); + mi_vfprintf(out, arg, tprefix, fmt, args); + } + else { + mi_vfprintf(out, arg, prefix, fmt, args); + } +} + +void _mi_raw_message(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + mi_vfprintf(NULL, NULL, NULL, fmt, args); + va_end(args); +} + +void _mi_trace_message(const char* fmt, ...) { + if (mi_option_get(mi_option_verbose) <= 1) return; // only with verbose level 2 or higher + va_list args; + va_start(args, fmt); + mi_vfprintf_thread(NULL, NULL, "mimalloc: ", fmt, args); + va_end(args); +} + +void _mi_verbose_message(const char* fmt, ...) { + if (!mi_option_is_enabled(mi_option_verbose)) return; + va_list args; + va_start(args,fmt); + mi_vfprintf(NULL, NULL, "mimalloc: ", fmt, args); + va_end(args); +} + +static void mi_show_error_message(const char* fmt, va_list args) { + if (!mi_option_is_enabled(mi_option_verbose)) { + if (!mi_option_is_enabled(mi_option_show_errors)) return; + if (mi_max_error_count >= 0 && (long)mi_atomic_increment_acq_rel(&error_count) > mi_max_error_count) return; + } + mi_vfprintf_thread(NULL, NULL, "mimalloc: error: ", fmt, args); +} + +void _mi_warning_message(const char* fmt, ...) { + if (!mi_option_is_enabled(mi_option_verbose)) { + if (!mi_option_is_enabled(mi_option_show_errors)) return; + if (mi_max_warning_count >= 0 && (long)mi_atomic_increment_acq_rel(&warning_count) > mi_max_warning_count) return; + } + va_list args; + va_start(args,fmt); + mi_vfprintf_thread(NULL, NULL, "mimalloc: warning: ", fmt, args); + va_end(args); +} + + +#if MI_DEBUG +mi_decl_noreturn mi_decl_cold void _mi_assert_fail(const char* assertion, const char* fname, unsigned line, const char* func ) mi_attr_noexcept { + _mi_fprintf(NULL, NULL, "mimalloc: assertion failed: at \"%s\":%u, %s\n assertion: \"%s\"\n", fname, line, (func==NULL?"":func), assertion); + abort(); +} +#endif + +// -------------------------------------------------------- +// Errors +// -------------------------------------------------------- + +static mi_error_fun* volatile mi_error_handler; // = NULL +static _Atomic(void*) mi_error_arg; // = NULL + +static void mi_error_default(int err) { + MI_UNUSED(err); + #if (MI_DEBUG>0) + if (err==EFAULT) { + #ifdef _MSC_VER + __debugbreak(); + #endif + abort(); + } + #endif + #if (MI_SECURE>0) + if (err==EFAULT) { // abort on serious errors in secure mode (corrupted meta-data) + abort(); + } + #endif + #if defined(MI_XMALLOC) + if (err==ENOMEM || err==EOVERFLOW || err==EINVAL) { // abort on memory allocation fails in xmalloc mode + abort(); + } + #endif + if (errno==0) { + errno = (err==EINVAL ? EINVAL : ENOMEM /* compatibility */ ); + } +} + +void mi_register_error(mi_error_fun* fun, void* arg) { + mi_error_handler = fun; // can be NULL + mi_atomic_store_ptr_release(void,&mi_error_arg, arg); +} + +void _mi_error_message(int err, const char* fmt, ...) { + // show detailed error message + va_list args; + va_start(args, fmt); + mi_show_error_message(fmt, args); + va_end(args); + // and call the error handler which may abort (or return normally, potentially setting errno) + if (mi_error_handler != NULL) { + mi_error_handler(err, mi_atomic_load_ptr_acquire(void,&mi_error_arg)); + } + else { + mi_error_default(err); + } +} + +mi_decl_noinline mi_block_t* _mi_block_next_is_corrupted(const mi_page_t* page, const mi_block_t* block, const mi_block_t* next) { + _mi_error_message(EFAULT, "corrupted free list entry of size %zub at %p: value 0x%zx\n", mi_page_block_size(page), block, (uintptr_t)next); + return NULL; +} + +// -------------------------------------------------------- +// Initialize options by checking the environment +// -------------------------------------------------------- + +// TODO: implement ourselves to reduce dependencies on the C runtime + +static void mi_option_init(mi_option_desc_t* desc) { + // Read option value from the environment + char s[64 + 1]; + char buf[64+1]; + _mi_strlcpy(buf, "mimalloc_", sizeof(buf)); + _mi_strlcat(buf, desc->name, sizeof(buf)); + int err = _mi_getenv(buf, s, sizeof(s)); + if (err==ENOENT && desc->legacy_name != NULL) { + _mi_strlcpy(buf, "mimalloc_", sizeof(buf)); + _mi_strlcat(buf, desc->legacy_name, sizeof(buf)); + err = _mi_getenv(buf, s, sizeof(s)); + if (err==0) { + _mi_warning_message("environment option \"mimalloc_%s\" is deprecated -- use \"mimalloc_%s\" instead.\n", desc->legacy_name, desc->name); + } + } + + if (err==0) { + size_t len = _mi_strnlen(s, sizeof(buf) - 1); + for (size_t i = 0; i < len; i++) { + buf[i] = _mi_toupper(s[i]); + } + buf[len] = 0; + if (buf[0] == 0 || _mi_streq(buf,"1") || _mi_streq(buf,"TRUE") || _mi_streq(buf,"YES") || _mi_streq(buf,"ON")) { + desc->value = 1; + desc->init = MI_OPTION_INITIALIZED; + } + else if (_mi_streq(buf,"0") || _mi_streq(buf,"FALSE") || _mi_streq(buf,"NO") || _mi_streq(buf,"OFF")) { + desc->value = 0; + desc->init = MI_OPTION_INITIALIZED; + } + else { + char* end = buf; + errno = 0; + long value = strtol(buf, &end, 10); + if (errno==0 && (mi_option_has_size_in_kib(desc->option) || mi_option_has_size_in_bytes(desc->option))) { + // this option is interpreted in bytes. + size_t size = (value < 0 ? 0 : (size_t)value); + bool overflow = false; + if (*end == 'K') { end++; } + #if MI_SIZE_SIZE >= 4 + else if (*end == 'M') { overflow = mi_mul_overflow(size,MI_MiB,&size); end++; } + else if (*end == 'G') { overflow = mi_mul_overflow(size,MI_GiB,&size); end++; } + #endif + #if MI_SIZE_SIZE >= 8 + else if (*end == 'T') { overflow = mi_mul_overflow(size,MI_GiB * MI_KiB,&size); end++; } + #endif + if (end[0] == 'I' && end[1] == 'B') { end += 2; } // KiB, MiB, GiB, TiB + else if (*end == 'B') { end++; } // Kb, Mb, Gb, Tb + if (overflow || size > MI_MAX_ALLOC_SIZE) { size = MI_MAX_ALLOC_SIZE; } + if (mi_option_has_size_in_kib(desc->option)) { + // This option is interpreted in KiB to prevent overflow of `long` for large allocations + // (long is 32-bit on 64-bit windows, which allows for 4TiB max.) + size = (size + MI_KiB - 1) / MI_KiB; // no overflow due to check before this + } + value = (size > LONG_MAX ? LONG_MAX : (long)size); + } + if (errno==0 && *end == 0) { + mi_option_set(desc->option, value); + } + else { + // set `init` first to avoid recursion through _mi_warning_message on mimalloc_verbose. + desc->init = MI_OPTION_DEFAULTED; + if (desc->option == mi_option_verbose && desc->value == 0) { + // if the 'mimalloc_verbose' env var has a bogus value we'd never know + // (since the value defaults to 'off') so in that case briefly enable verbose + desc->value = 1; + _mi_warning_message("environment option mimalloc_%s has an invalid value.\n", desc->name); + desc->value = 0; + } + else { + _mi_warning_message("environment option mimalloc_%s has an invalid value.\n", desc->name); + } + } + } + mi_assert_internal(desc->init != MI_OPTION_UNINIT); + } + else if (err==ENOENT) { + desc->init = MI_OPTION_DEFAULTED; + } + // and on another error, keep unitialized to try again (can happen during preloading if getenv is not available) +} diff --git a/Objects/mimalloc/random.c b/Objects/mimalloc/util/random.c similarity index 81% rename from Objects/mimalloc/random.c rename to Objects/mimalloc/util/random.c index 35a0633a800d6c6..1bfad4bc16863ed 100644 --- a/Objects/mimalloc/random.c +++ b/Objects/mimalloc/util/random.c @@ -1,13 +1,14 @@ /* ---------------------------------------------------------------------------- -Copyright (c) 2019-2021, Microsoft Research, Daan Leijen +Copyright (c) 2019-2026, Microsoft Research, Daan Leijen This is free software; you can redistribute it and/or modify it under the terms of the MIT license. A copy of the license can be found in the file "LICENSE" at the root of this distribution. -----------------------------------------------------------------------------*/ #include "mimalloc.h" -#include "mimalloc/internal.h" #include "mimalloc/prim.h" // _mi_prim_random_buf -#include // memset +#include "mimalloc/util/libc.h" +#include "mimalloc/util/options.h" +#include "mimalloc/util/random.h" /* ---------------------------------------------------------------------------- We use our own PRNG to keep predictable performance of random number generation @@ -33,15 +34,11 @@ The implementation uses regular C code which compiles very well on modern compil (gcc x64 has no register spills, and clang 6+ uses SSE instructions) -----------------------------------------------------------------------------*/ -static inline uint32_t rotl(uint32_t x, uint32_t shift) { - return (x << shift) | (x >> (32 - shift)); -} - static inline void qround(uint32_t x[16], size_t a, size_t b, size_t c, size_t d) { - x[a] += x[b]; x[d] = rotl(x[d] ^ x[a], 16); - x[c] += x[d]; x[b] = rotl(x[b] ^ x[c], 12); - x[a] += x[b]; x[d] = rotl(x[d] ^ x[a], 8); - x[c] += x[d]; x[b] = rotl(x[b] ^ x[c], 7); + x[a] += x[b]; x[d] = mi_rotl32(x[d] ^ x[a], 16); + x[c] += x[d]; x[b] = mi_rotl32(x[b] ^ x[c], 12); + x[a] += x[b]; x[d] = mi_rotl32(x[d] ^ x[a], 8); + x[c] += x[d]; x[b] = mi_rotl32(x[b] ^ x[c], 7); } static void chacha_block(mi_random_ctx_t* ctx) @@ -99,7 +96,8 @@ static void chacha_init(mi_random_ctx_t* ctx, const uint8_t key[32], uint64_t no // since we only use chacha for randomness (and not encryption) we // do not _need_ to read 32-bit values as little endian but we do anyways // just for being compatible :-) - memset(ctx, 0, sizeof(*ctx)); + ctx->output_available = 0; + mi_memzero(ctx->output,sizeof(ctx->output)); for (size_t i = 0; i < 4; i++) { const uint8_t* sigma = (uint8_t*)"expand 32-byte k"; ctx->input[i] = read32(sigma,i); @@ -114,8 +112,9 @@ static void chacha_init(mi_random_ctx_t* ctx, const uint8_t key[32], uint64_t no } static void chacha_split(mi_random_ctx_t* ctx, uint64_t nonce, mi_random_ctx_t* ctx_new) { - memset(ctx_new, 0, sizeof(*ctx_new)); - _mi_memcpy(ctx_new->input, ctx->input, sizeof(ctx_new->input)); + mi_memzero(ctx_new, sizeof(*ctx_new)); + ctx_new->weak = ctx->weak; + mi_memcpy(ctx_new->input, ctx->input, sizeof(ctx_new->input)); ctx_new->input[12] = 0; ctx_new->input[13] = 0; ctx_new->input[14] = (uint32_t)nonce; @@ -129,27 +128,30 @@ static void chacha_split(mi_random_ctx_t* ctx, uint64_t nonce, mi_random_ctx_t* Random interface -----------------------------------------------------------------------------*/ -#if MI_DEBUG>1 static bool mi_random_is_initialized(mi_random_ctx_t* ctx) { return (ctx != NULL && ctx->input[0] != 0); } -#endif void _mi_random_split(mi_random_ctx_t* ctx, mi_random_ctx_t* ctx_new) { - mi_assert_internal(mi_random_is_initialized(ctx)); mi_assert_internal(ctx != ctx_new); - chacha_split(ctx, (uintptr_t)ctx_new /*nonce*/, ctx_new); + if mi_unlikely(!mi_random_is_initialized(ctx)) { _mi_random_init(ctx); } + const uintptr_t nonce_rnd = _mi_random_next(ctx); + chacha_split(ctx, (uintptr_t)ctx_new ^ nonce_rnd /*nonce*/, ctx_new); } -uintptr_t _mi_random_next(mi_random_ctx_t* ctx) { - mi_assert_internal(mi_random_is_initialized(ctx)); - #if MI_INTPTR_SIZE <= 4 - return chacha_next32(ctx); - #elif MI_INTPTR_SIZE == 8 - return (((uintptr_t)chacha_next32(ctx) << 32) | chacha_next32(ctx)); - #else - # error "define mi_random_next for this platform" - #endif +size_t _mi_random_next(mi_random_ctx_t* ctx) { + if mi_unlikely(!mi_random_is_initialized(ctx)) { _mi_random_init(ctx); } + size_t r; + do { + #if MI_SIZE_SIZE <= 4 + r = chacha_next32(ctx); + #elif MI_SIZE_SIZE == 8 + r = (((size_t)chacha_next32(ctx) << 32) | chacha_next32(ctx)); + #else + # error "define mi_random_next for this platform" + #endif + } while (r==0); + return r; } @@ -158,30 +160,33 @@ To initialize a fresh random context. If we cannot get good randomness, we fall back to weak randomness based on a timer and ASLR. -----------------------------------------------------------------------------*/ -uintptr_t _mi_os_random_weak(uintptr_t extra_seed) { - uintptr_t x = (uintptr_t)&_mi_os_random_weak ^ extra_seed; // ASLR makes the address random +size_t _mi_os_random_weak(size_t extra_seed) { + size_t x = (size_t)&_mi_os_random_weak ^ extra_seed; // ASLR makes the address random x ^= _mi_prim_clock_now(); // and do a few randomization steps - uintptr_t max = ((x ^ (x >> 17)) & 0x0F) + 1; - for (uintptr_t i = 0; i < max; i++) { - x = _mi_random_shuffle(x); + size_t max = ((x ^ (x >> 17)) & 0x0F) + 1; + for (size_t i = 0; i < max || x==0; i++, x++) { + x = mi_random_shuffle(x); } mi_assert_internal(x != 0); return x; } static void mi_random_init_ex(mi_random_ctx_t* ctx, bool use_weak) { - uint8_t key[32] = {0}; + uint8_t key[32]; if (use_weak || !_mi_prim_random_buf(key, sizeof(key))) { // if we fail to get random data from the OS, we fall back to a // weak random source based on the current time #if !defined(__wasi__) if (!use_weak) { _mi_warning_message("unable to use secure randomness\n"); } #endif - uintptr_t x = _mi_os_random_weak(0); - for (size_t i = 0; i < 8; i++) { // key is eight 32-bit words. - x = _mi_random_shuffle(x); - ((uint32_t*)key)[i] = (uint32_t)x; + size_t x = _mi_os_random_weak(0); + for (size_t i = 0; i < 32; i+=4, x++) { + x = mi_random_shuffle(x); + key[i] = (uint8_t)(x); + key[i+1] = (uint8_t)(x>>8); + key[i+2] = (uint8_t)(x>>16); + key[i+3] = (uint8_t)(x>>24); } ctx->weak = true; } @@ -189,6 +194,7 @@ static void mi_random_init_ex(mi_random_ctx_t* ctx, bool use_weak) { ctx->weak = false; } chacha_init(ctx, key, (uintptr_t)ctx /*nonce*/ ); + mi_memzero(key, sizeof(key)); } void _mi_random_init(mi_random_ctx_t* ctx) { diff --git a/Objects/mimalloc/util/readme.md b/Objects/mimalloc/util/readme.md new file mode 100644 index 000000000000000..cc38c5b8cd9adfa --- /dev/null +++ b/Objects/mimalloc/util/readme.md @@ -0,0 +1,8 @@ +# Utilities + +- [`libc.c`](libc.c): Defines various standard libc functions to reduce +the dependency on libc: string functions, formatting, environment, and other runtime utility functions. +- [`options.c`](options.c): Allocator options, environment-based configuration, and diagnostic output. +- [`random.c`](random.c): Random number generation, seeding, and splitting of random streams. +- [`stats.c`](stats.c): Allocation statistics, aggregation, reporting, and process information. +- [`threadlocal.c`](threadlocal.c): Dynamically allocated thread-local keys and per-thread value storage. diff --git a/Objects/mimalloc/util/stats.c b/Objects/mimalloc/util/stats.c new file mode 100644 index 000000000000000..f2a45490241550d --- /dev/null +++ b/Objects/mimalloc/util/stats.c @@ -0,0 +1,857 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2018-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ +#include "mimalloc.h" +#include "mimalloc/prim.h" // _mi_prim_clock_now, mi_process_info_t +#include "mimalloc/prim/prim-tls.h" +#include "mimalloc/alloc/page.h" +#include "mimalloc/util/stats.h" + +#if defined(_MSC_VER) && (_MSC_VER < 1920) +#pragma warning(disable:4204) // non-constant aggregate initializer +#endif + +/* ----------------------------------------------------------- + Statistics operations +----------------------------------------------------------- */ + +static void mi_stat_update_mt(mi_stat_count_t* stat, int64_t amount) { + if (amount == 0) return; + // add atomically + int64_t current = mi_atomic_volatile_addi64_relaxed(&stat->current, amount); + mi_atomic_volatile_maxi64_relaxed(&stat->peak, current + amount); + if (amount > 0) { + mi_atomic_volatile_addi64_relaxed(&stat->total, amount); + } +} + +static void mi_stat_update(mi_stat_count_t* stat, int64_t amount) { + if (amount == 0) return; + // add thread local + stat->current += amount; + if (stat->current > stat->peak) { stat->peak = stat->current; } + if (amount > 0) { stat->total += amount; } +} + +void __mi_stat_increase_mt(mi_stat_count_t* stat, uint64_t amount) { + mi_assert_internal(amount<=INT64_MAX); + mi_stat_update_mt(stat, (int64_t)amount); +} +void __mi_stat_increase(mi_stat_count_t* stat, uint64_t amount) { + mi_assert_internal(amount<=INT64_MAX); + mi_stat_update(stat, (int64_t)amount); +} + +void __mi_stat_decrease_mt(mi_stat_count_t* stat, uint64_t amount) { + mi_assert_internal(amount<=INT64_MAX); + mi_stat_update_mt(stat, -((int64_t)amount)); +} +void __mi_stat_decrease(mi_stat_count_t* stat, uint64_t amount) { + mi_assert_internal(amount<=INT64_MAX); + mi_stat_update(stat, -((int64_t)amount)); +} + + +// Adjust stats to compensate; for example before committing a range, +// first adjust downwards with parts that were already committed so +// we avoid double counting. +static void mi_stat_adjust_mt(mi_stat_count_t* stat, int64_t amount) { + if (amount == 0) return; + // adjust atomically + const int64_t peak = mi_atomic_loadi64_relaxed((_Atomic(int64_t)*)&stat->peak); + mi_atomic_volatile_addi64_relaxed(&stat->current, amount); + const int64_t prev_total = mi_atomic_volatile_addi64_relaxed(&stat->total, amount); + if (prev_total == peak) { mi_atomic_volatile_addi64_relaxed(&stat->peak, amount); } +} + +static void mi_stat_adjust(mi_stat_count_t* stat, int64_t amount) { + if (amount == 0) return; + stat->current += amount; + if (stat->total==stat->peak) { stat->peak += amount; } + stat->total += amount; +} + +void __mi_stat_adjust_increase_mt(mi_stat_count_t* stat, uint64_t amount) { + mi_assert_internal(amount<=INT64_MAX); + mi_stat_adjust_mt(stat, (int64_t)amount); +} +void __mi_stat_adjust_increase(mi_stat_count_t* stat, uint64_t amount) { + mi_assert_internal(amount<=INT64_MAX); + mi_stat_adjust(stat, (int64_t)amount); +} +void __mi_stat_adjust_decrease_mt(mi_stat_count_t* stat, uint64_t amount) { + mi_assert_internal(amount<=INT64_MAX); + mi_stat_adjust_mt(stat, -((int64_t)amount)); +} +void __mi_stat_adjust_decrease(mi_stat_count_t* stat, uint64_t amount) { + mi_assert_internal(amount<=INT64_MAX); + mi_stat_adjust(stat, -((int64_t)amount)); +} + + +// must be thread safe as it is called from stats_merge +static void mi_stat_count_add_mt(mi_stat_count_t* stat, const mi_stat_count_t* src) { + if (stat==src) return; + mi_atomic_volatile_void_addi64_relaxed(&stat->total, &src->total); + const int64_t src_peak = mi_atomic_loadi64_relaxed((_Atomic(int64_t)*)&src->peak); + const int64_t src_current = mi_atomic_loadi64_relaxed((_Atomic(int64_t)*)&src->current); + // avoid atomic operations for common cases + if (src_current == 0 && src_peak == 0) return; + const int64_t prev_current = (src_current == 0 ? mi_atomic_loadi64_relaxed((_Atomic(int64_t)*)&stat->current) + : mi_atomic_volatile_addi64_relaxed(&stat->current, src_current)); + + // Global current plus thread peak approximates new global peak + // note: peak scores do not really work across threads. + // we used to just add them together but that often overestimates in practice. + // similarly, max does not seem to work well. The current approach + // by Artem Kharytoniuk (@artem-lunarg) seems to work better, see PR#1112 + // for a longer description. + mi_atomic_volatile_maxi64_relaxed(&stat->peak, prev_current + src_peak); +} + +static void mi_stat_counter_add_mt(mi_stat_counter_t* stat, const mi_stat_counter_t* src) { + if (stat==src) return; + mi_atomic_volatile_void_addi64_relaxed(&stat->total, &src->total); +} + +#define MI_STAT_COUNT(stat) mi_stat_count_add_mt(&stats->stat, &src->stat); +#define MI_STAT_COUNTER(stat) mi_stat_counter_add_mt(&stats->stat, &src->stat); + +// must be thread safe as it is called from stats_merge +static void mi_stats_add(mi_stats_t* stats, const mi_stats_t* src) { + if (stats==NULL || src==NULL || stats==src) return; + + // copy all fields + MI_STAT_FIELDS() + + #if MI_STATS + for (size_t i = 0; i <= MI_BIN_HUGE; i++) { + mi_stat_count_add_mt(&stats->malloc_bins[i], &src->malloc_bins[i]); + } + #endif + for (size_t i = 0; i <= MI_BIN_HUGE; i++) { + mi_stat_count_add_mt(&stats->page_bins[i], &src->page_bins[i]); + } + for (size_t i = 0; i < MI_CBIN_COUNT; i++) { + mi_stat_count_add_mt(&stats->chunk_bins[i], &src->chunk_bins[i]); + } +} + +#undef MI_STAT_COUNT +#undef MI_STAT_COUNTER + +/* ----------------------------------------------------------- + Display statistics +----------------------------------------------------------- */ + +static void mi_print_number(int64_t n, mi_output_fun* out, void* arg, bool limitwidth, bool as_bytes, int64_t base) { + char buf[32]; _mi_memzero_var(buf); + int len = 32; + const int64_t pos = (n < 0 ? -n : n); + if (pos < base) { + if (!(n==1 && as_bytes)) { // skip printing 1 B for the unit column + _mi_snprintf(buf, len, "%lld %-3s", (long long)n, ""); + } + } + else { + int64_t divider = base; + const char* magnitude = "K"; + if (pos >= divider*base) { divider *= base; magnitude = "M"; } + if (pos >= divider*base) { divider *= base; magnitude = "G"; } + const int64_t tens = (n / (divider/10)); + const long whole = (long)(tens/10); + const long frac1 = (long)(tens%10); + char unitdesc[8]; + _mi_snprintf(unitdesc, 8, "%s%s%s", magnitude, (base==1024 ? "i" : ""), (as_bytes ? "B" : "")); + _mi_snprintf(buf, len, "%ld.%ld %-3s", whole, (frac1 < 0 ? -frac1 : frac1), unitdesc); + } + if (limitwidth) { + _mi_fprintf(out, arg, "%12s", buf); + } + else { + _mi_fprintf(out, arg, "%s", buf); + } +} + +static void mi_print_size_narrow(int64_t n, mi_output_fun* out, void* arg) { + mi_print_number(n, out, arg, false, true, 1024); +} +static void mi_print_size(int64_t n, mi_output_fun* out, void* arg) { + mi_print_number(n, out, arg, true, true, 1024); +} +static void mi_print_decimal(int64_t n, mi_output_fun* out, void* arg) { + mi_print_number(n, out, arg, true, false, 1000); +} +// static void mi_print_binary(int64_t n, mi_output_fun* out, void* arg) { +// mi_print_number(n, out, arg, true, false, 1024); +// } + +static void mi_print_label(const char* label, mi_output_fun* out, void* arg) { + _mi_fprintf(out, arg, " %-12s:", label); +} + +static void mi_print_newline(mi_output_fun* out, void* arg) { + _mi_fprintf(out, arg, "\n"); +} + +static void mi_print_stat_decimal(const mi_stat_count_t* stat, const char* msg, mi_output_fun* out, void* arg) { + mi_print_label(msg, out, arg); + mi_print_decimal(stat->peak, out, arg); + mi_print_decimal(stat->total, out, arg); + mi_print_decimal(stat->current, out, arg); + mi_print_newline(out, arg); +} + +static void mi_print_stat_size(const mi_stat_count_t* stat, const char* msg, mi_output_fun* out, void* arg) { + mi_print_label(msg, out, arg); + mi_print_size(stat->peak, out, arg); + mi_print_size(stat->total, out, arg); + mi_print_size(stat->current, out, arg); + mi_print_newline(out, arg); +} + +static void mi_print_stat_counter(const mi_stat_counter_t* stat, const char* msg, mi_output_fun* out, void* arg ) { + mi_print_label(msg, out, arg); + mi_print_decimal(stat->total, out, arg); + mi_print_newline(out, arg); +} + +static void mi_print_stat_counter_size(const mi_stat_counter_t* stat, const char* msg, mi_output_fun* out, void* arg ) { + mi_print_label(msg, out, arg); + mi_print_size(stat->total, out, arg); + mi_print_newline(out, arg); +} + +static void mi_print_average(int64_t count, int64_t total, const char* msg, mi_output_fun* out, void* arg) { + const int64_t avg_tens = (count == 0 ? 0 : (total*10 / count)); + const int64_t avg_whole = avg_tens/10; + const int64_t avg_frac1 = avg_tens%10; + mi_print_label(msg, out, arg); + _mi_fprintf(out, arg, " %5lld.%lld avg\n", avg_whole, avg_frac1); +} + +static void mi_print_header(const char* name, mi_output_fun* out, void* arg ) { + _mi_fprintf(out, arg, " %-13s %11s %11s %11s\n", name, "peak ", "total ", "current "); +} + +#if MI_STATS +static void mi_print_empty(mi_output_fun* out, void* arg) { + _mi_fprintf(out, arg, "%12s", " "); +} + +static void mi_print_stat_total_size(const mi_stat_counter_t* stat, const char* msg, mi_output_fun* out, void* arg) { + mi_print_label(msg, out, arg); + mi_print_empty(out, arg); // no peak + mi_print_size(stat->total, out, arg); + mi_print_newline(out, arg); +} + +static void mi_print_check(const mi_stat_count_t* stat, mi_output_fun* out, void* arg, const char* not_ok) { + _mi_fprintf(out, arg, " %s\n", (stat->current==0 ? "ok" : (not_ok == NULL ? "not all freed" : not_ok))); +} + +static void mi_print_stat_bin(const mi_stat_count_t* stat, const char* msg, int64_t blocksize, int64_t total, mi_output_fun* out, void* arg) { + mi_print_label(msg, out, arg); + mi_print_size(stat->peak * blocksize, out, arg); + mi_print_size(stat->total * blocksize, out, arg); + mi_print_size(stat->current * blocksize, out, arg); + mi_print_size(blocksize, out, arg); + mi_print_decimal(total, out, arg); + mi_print_check(stat, out, arg, NULL); +} + +static void mi_print_header_bins(const char* name, mi_output_fun* out, void* arg ) { + _mi_fprintf(out, arg, " %-13s %11s %11s %11s %11s %11s\n", + name, "peak ", "total ", "current ", "block ", "total# "); +} + +static bool mi_stats_print_bins(const mi_stat_count_t* bins, const mi_stat_count_t* stat_huge, size_t max, mi_output_fun* out, void* arg) { + bool found = false; + char buf[64]; + for (size_t i = 0; i <= max; i++) { + if (bins[i].total > 0) { + found = true; + const size_t blocksize = _mi_bin_size((uint8_t)i); + const char* pagekind = (blocksize <= MI_SMALL_MAX_OBJ_SIZE ? "S" : + (mi_block_size_is_medium(blocksize) ? "M" : + (blocksize <= MI_LARGE_MAX_OBJ_SIZE ? "L" : "H"))); + _mi_snprintf(buf, 64, "bin%2s %3lu", pagekind, (long)i); + if (i!=MI_BIN_HUGE) { + mi_print_stat_bin(&bins[i], buf, (int64_t)blocksize, bins[i].total, out, arg); + } + else { + mi_print_stat_bin(stat_huge, buf, 1, bins[i].total, out, arg); + } + } + } + if (found) { + mi_print_newline(out, arg); + } + return found; +} +#endif + + + +//------------------------------------------------------------ +// Use an output wrapper for line-buffered output +// (which is nice when using loggers etc.) +//------------------------------------------------------------ +typedef struct buffered_s { + mi_output_fun* out; // original output function + void* arg; // and state + char* buf; // local buffer of at least size `count+1` + size_t used; // currently used chars `used <= count` + size_t count; // total chars available for output +} buffered_t; + +static void mi_buffered_flush(buffered_t* buf) { + buf->buf[buf->used] = 0; + _mi_fputs(buf->out, buf->arg, NULL, buf->buf); + buf->used = 0; +} + +static void mi_cdecl mi_buffered_out(const char* msg, void* arg) { + buffered_t* buf = (buffered_t*)arg; + if (msg==NULL || buf==NULL) return; + for (const char* src = msg; *src != 0; src++) { + char c = *src; + if (buf->used >= buf->count) mi_buffered_flush(buf); + mi_assert_internal(buf->used < buf->count); + buf->buf[buf->used++] = c; + if (c == '\n') mi_buffered_flush(buf); + } +} + +//------------------------------------------------------------ +// Print statistics +//------------------------------------------------------------ + +mi_decl_export void mi_process_info_print_out(mi_output_fun* out, void* arg) mi_attr_noexcept +{ + size_t elapsed; + size_t user_time; + size_t sys_time; + size_t current_rss; + size_t peak_rss; + size_t current_commit; + size_t peak_commit; + size_t page_faults; + mi_process_info(&elapsed, &user_time, &sys_time, ¤t_rss, &peak_rss, ¤t_commit, &peak_commit, &page_faults); + _mi_fprintf(out, arg, " %-12s: %5zu.%03zu s\n", "elapsed", elapsed/1000, elapsed%1000); + _mi_fprintf(out, arg, " %-12s: user: %zu.%03zu s, system: %zu.%03zu s, faults: %zu, peak rss: ", "process", + user_time/1000, user_time%1000, sys_time/1000, sys_time%1000, page_faults); + mi_print_size_narrow((int64_t)peak_rss, out, arg); + if (peak_commit > 0) { + _mi_fprintf(out, arg, ", peak commit: "); + mi_print_size_narrow((int64_t)peak_commit, out, arg); + } + mi_print_newline(out, arg); +} + +void _mi_stats_print(const char* name, size_t id, const mi_stats_t* stats, mi_output_fun* out0, void* arg0) mi_attr_noexcept { + // wrap the output function to be line buffered + char buf[256]; _mi_memzero_var(buf); + buffered_t buffer = { out0, arg0, NULL, 0, 255 }; + buffer.buf = buf; + mi_output_fun* out = &mi_buffered_out; + void* arg = &buffer; + + // and print using that + _mi_fprintf(out, arg, "%s %zu\n", name, id); + + if (stats->allocated.total != 0) { + #if MI_STATS + mi_print_header_bins("blocks", out, arg); + mi_stats_print_bins(stats->malloc_bins, &stats->malloc_huge, MI_BIN_HUGE, out, arg); + #endif + #if MI_STATS + mi_print_stat_size(&stats->allocated, "allocated", out, arg); + mi_print_stat_total_size(&stats->freed, "freed", out, arg); + #if MI_STATS>=2 + mi_print_stat_total_size(&stats->malloc_requested, "requested", out, arg); + #else + mi_print_stat_total_size(&stats->malloc_requested, "requested~", out, arg); + #endif + mi_print_newline(out, arg); + #endif + } + + if (stats->pages.total != 0) { + mi_print_header("pages", out, arg); + mi_print_stat_size(&stats->page_committed, "touched", out, arg); + // mi_print_stat(&stats->segments, "segments", -1, out, arg); + // mi_print_stat(&stats->segments_abandoned, "-abandoned", -1, out, arg); + // mi_print_stat(&stats->segments_cache, "-cached", -1, out, arg); + mi_print_stat_decimal(&stats->pages, "pages", out, arg); + mi_print_stat_decimal(&stats->pages_abandoned, "abandoned", out, arg); + mi_print_stat_decimal(&stats->pages_os_abandoned, "os abandoned", out, arg); + mi_print_stat_decimal(&stats->pages_os_allocated, "os allocated", out, arg); + + mi_print_stat_counter(&stats->pages_reclaim_on_alloc, "reclaima", out, arg); + mi_print_stat_counter(&stats->pages_reclaim_on_free, "reclaimf", out, arg); + mi_print_stat_counter(&stats->pages_reabandon_full, "reabandon", out, arg); + mi_print_stat_counter(&stats->pages_unabandon_busy_wait, "unabdn retry", out, arg); + mi_print_stat_counter(&stats->pages_unown_cas_retry, "unown retry", out, arg); + mi_print_stat_counter(&stats->pages_extended, "extended", out, arg); + mi_print_stat_counter(&stats->pages_retire, "retire", out, arg); + mi_print_stat_counter(&stats->pages_stat_updates, "stat updates", out, arg); + mi_print_average(stats->pages_stat_updates.total, stats->pages_stat_update_count.total, "stat avg", out, arg); + mi_print_average(stats->page_searches_count.total, stats->page_searches.total, "searches", out, arg); + mi_print_stat_counter(&stats->profile_samples, "prof samples", out, arg); + mi_print_newline(out, arg); + } + + if (stats->arena_count.total > 0) { + mi_print_header("arenas", out, arg); + mi_print_stat_size(&stats->reserved, "reserved", out, arg); + mi_print_stat_size(&stats->committed, "committed", out, arg); + mi_print_stat_counter_size(&stats->reset, "reset", out, arg); + mi_print_stat_counter_size(&stats->purged, "purged", out, arg); + + mi_print_stat_counter(&stats->arena_count, "arenas", out, arg); + mi_print_stat_counter(&stats->arena_rollback_count, "rollback", out, arg); + mi_print_stat_counter(&stats->mmap_calls, "mmaps", out, arg); + mi_print_stat_counter(&stats->commit_calls, "commits", out, arg); + mi_print_stat_counter(&stats->reset_calls, "resets", out, arg); + mi_print_stat_counter(&stats->purge_calls, "purges", out, arg); + mi_print_stat_counter(&stats->malloc_guarded_count, "guarded", out, arg); + mi_print_stat_decimal(&stats->theaps, "theaps", out, arg); + mi_print_stat_decimal(&stats->heaps, "heaps", out, arg); + mi_print_stat_counter(&stats->heaps_delete_wait, "heap waits", out, arg); + mi_print_newline(out, arg); + } + + mi_print_header("process", out, arg); + mi_print_stat_decimal(&stats->threads, "threads", out, arg); + mi_stat_counter_t numa_count = { _mi_os_numa_node_count() }; + mi_print_stat_counter(&numa_count, "numa nodes", out, arg); + mi_process_info_print_out(out, arg); + + mi_print_newline(out, arg); +} + + +static mi_msecs_t mi_process_start; // = 0 + +// called on process init +void _mi_stats_init(void) { + if (mi_process_start == 0) { mi_process_start = _mi_clock_start(); }; +} + +static void mi_stats_add_into(mi_stats_t* to, const mi_stats_t* from) { + mi_assert_internal(to != NULL && from != NULL); + if (to == from) return; + mi_stats_add(to, from); +} + +void _mi_stats_merge_into(mi_stats_t* to, mi_stats_t* from) { + mi_assert_internal(to != NULL && from != NULL); + if (to == from) return; + mi_stats_add(to, from); + mi_stats_init(from); // zero field and keep the header +} + +static const mi_stats_t* mi_stats_merge_theap_to_heap(mi_theap_t* theap) mi_attr_noexcept { + mi_stats_t* stats = &theap->stats; + mi_stats_t* heap_stats = &mi_theap_heap(theap)->stats; + _mi_stats_merge_into( heap_stats, stats ); + return heap_stats; +} + +static const mi_stats_t* mi_heap_get_stats(mi_heap_t* heap) { + if (heap==NULL) { heap = mi_heap_main(); } + mi_theap_t* theap = _mi_heap_theap_peek(heap); + if (theap==NULL) return &heap->stats; + else return mi_stats_merge_theap_to_heap(theap); +} + +static const mi_stats_t* mi_theap_get_stats(mi_theap_t* theap) { + return &theap->stats; +} + +// deprecated +void mi_stats_reset(void) mi_attr_noexcept { + if (!mi_theap_is_initialized(_mi_theap_default())) return; + mi_heap_t* heap_main = mi_heap_main(); + mi_heap_get_stats(heap_main); + mi_heap_stats_merge_to_subproc(heap_main); +} + + +void mi_heap_stats_print_out(mi_heap_t* heap, mi_output_fun* out, void* arg) mi_attr_noexcept { + if (heap==NULL) { heap = mi_heap_main(); } + _mi_stats_print("heap", heap->heap_seq, mi_heap_get_stats(heap), out, arg); +} + +typedef struct mi_heap_print_visit_info_s { + mi_output_fun* out; + void* out_arg; +} mi_heap_print_visit_info_t; + +static bool mi_cdecl mi_heap_print_visitor(mi_heap_t* heap, void* arg) { + mi_heap_print_visit_info_t* vinfo = (mi_heap_print_visit_info_t*)(arg); + mi_heap_stats_print_out(heap, vinfo->out, vinfo->out_arg); + return true; +} + + +// show each heap and then the subproc +void mi_subproc_heap_stats_print_out(mi_subproc_id_t subproc_id, mi_output_fun* out, void* arg) mi_attr_noexcept { + mi_subproc_t* subproc = _mi_subproc_from_id(subproc_id); + if (subproc==NULL) return; + mi_heap_print_visit_info_t vinfo = { out, arg }; + mi_subproc_visit_heaps(subproc_id, &mi_heap_print_visitor, &vinfo); + if (subproc->theap_meta!=NULL) { + _mi_stats_print("meta", subproc->subproc_seq, &subproc->theap_meta->stats, out, arg); + } + // if (subproc->theap_meta!=NULL) { _mi_stats_merge_into(&subproc->stats, &subproc->theap_meta->stats); } + _mi_stats_print("subproc", subproc->subproc_seq, &subproc->stats, out, arg); +} + + +// aggregate all stats from the heaps and subproc and print those +void mi_subproc_stats_print_out(mi_subproc_id_t subproc_id, mi_output_fun* out, void* arg) mi_attr_noexcept { + mi_subproc_t* subproc = _mi_subproc_from_id(subproc_id); + if (subproc==NULL) return; + mi_stats_t_decl(stats); + if (mi_subproc_stats_get(subproc_id, &stats)) { + _mi_stats_print("subproc", subproc->subproc_seq, &stats, out, arg); + } +} + +void mi_stats_print_out(mi_output_fun* out, void* arg) mi_attr_noexcept { + mi_subproc_stats_print_out(mi_subproc_current(),out, arg); +} + +// deprecated +void mi_stats_print(void* out) mi_attr_noexcept { + // for compatibility there is an `out` parameter (which can be `stdout` or `stderr`) + mi_stats_print_out((mi_output_fun*)out, NULL); +} + +// deprecated +void mi_thread_stats_print_out(mi_output_fun* out, void* arg) mi_attr_noexcept { + mi_theap_t* theap = _mi_theap_default(); + if (theap==NULL || !mi_theap_is_initialized(theap)) return; + _mi_stats_print("heap", mi_theap_heap(theap)->heap_seq, &theap->stats, out, arg); + mi_stats_merge_theap_to_heap(_mi_theap_default()); +} + + +// ---------------------------------------------------------------- +// Basic timer for convenience; use milli-seconds to avoid doubles +// ---------------------------------------------------------------- + +static mi_msecs_t mi_clock_diff; + +mi_msecs_t _mi_clock_now(void) { + return _mi_prim_clock_now(); +} + +mi_msecs_t _mi_clock_start(void) { + if (mi_clock_diff == 0.0) { + mi_msecs_t t0 = _mi_clock_now(); + mi_clock_diff = _mi_clock_now() - t0; + } + return _mi_clock_now(); +} + +mi_msecs_t _mi_clock_end(mi_msecs_t start) { + mi_msecs_t end = _mi_clock_now(); + return (end - start - mi_clock_diff); +} + + +// -------------------------------------------------------- +// Basic process statistics +// -------------------------------------------------------- + +mi_decl_export void mi_process_info(size_t* elapsed_msecs, size_t* user_msecs, size_t* system_msecs, size_t* current_rss, size_t* peak_rss, size_t* current_commit, size_t* peak_commit, size_t* page_faults) mi_attr_noexcept +{ + mi_process_info_t pinfo; + _mi_memzero_var(pinfo); + pinfo.elapsed = _mi_clock_end(mi_process_start); + { const mi_subproc_t* subproc = _mi_subproc_main(); + if (subproc!=NULL) { + const int64_t current = mi_atomic_loadi64_relaxed((_Atomic(int64_t)*)(&subproc->stats.committed.current)); + const int64_t peak = mi_atomic_loadi64_relaxed((_Atomic(int64_t)*)(&subproc->stats.committed.peak)); + pinfo.current_commit = (current < 0 ? 0 : (current < PTRDIFF_MAX ? (size_t)current : PTRDIFF_MAX)); + pinfo.peak_commit = (peak < 0 ? 0 : (peak < PTRDIFF_MAX ? (size_t)peak : PTRDIFF_MAX)); + } + } + pinfo.current_rss = pinfo.current_commit; + pinfo.peak_rss = pinfo.peak_commit; + pinfo.utime = 0; + pinfo.stime = 0; + pinfo.page_faults = 0; + + _mi_prim_process_info(&pinfo); + + if (elapsed_msecs!=NULL) *elapsed_msecs = (pinfo.elapsed < 0 ? 0 : (pinfo.elapsed < (mi_msecs_t)PTRDIFF_MAX ? (size_t)pinfo.elapsed : PTRDIFF_MAX)); + if (user_msecs!=NULL) *user_msecs = (pinfo.utime < 0 ? 0 : (pinfo.utime < (mi_msecs_t)PTRDIFF_MAX ? (size_t)pinfo.utime : PTRDIFF_MAX)); + if (system_msecs!=NULL) *system_msecs = (pinfo.stime < 0 ? 0 : (pinfo.stime < (mi_msecs_t)PTRDIFF_MAX ? (size_t)pinfo.stime : PTRDIFF_MAX)); + if (current_rss!=NULL) *current_rss = pinfo.current_rss; + if (peak_rss!=NULL) *peak_rss = pinfo.peak_rss; + if (current_commit!=NULL) *current_commit = pinfo.current_commit; + if (peak_commit!=NULL) *peak_commit = pinfo.peak_commit; + if (page_faults!=NULL) *page_faults = pinfo.page_faults; +} + +mi_decl_export void mi_process_info_print(void) mi_attr_noexcept { + mi_process_info_print_out(NULL, NULL); +} + + +// -------------------------------------------------------- +// Return statistics +// -------------------------------------------------------- + +size_t mi_stats_get_bin_size(size_t bin) mi_attr_noexcept { + if (bin > MI_BIN_HUGE) return 0; + return _mi_bin_size(bin); +} + +static bool mi_stats_copy(mi_stats_t* stats_to, const mi_stats_t* stats_from) mi_attr_noexcept { + if (stats_to == NULL || stats_to->size != sizeof(mi_stats_t) || stats_to->version != MI_STAT_VERSION) return false; + if (stats_from == NULL || stats_from->size != stats_to->size) return false; + mi_memcpy(stats_to, stats_from, stats_to->size); + return true; +} + +bool mi_subproc_stats_get_exclusive(mi_subproc_id_t subproc_id, mi_stats_t* stats) mi_attr_noexcept { + const mi_subproc_t* subproc = _mi_subproc_from_id(subproc_id); + if (subproc==NULL) return false; + return mi_stats_copy(stats,&subproc->stats); +} + +bool mi_heap_stats_get(mi_heap_t* heap, mi_stats_t* stats) mi_attr_noexcept { + return mi_stats_copy(stats, mi_heap_get_stats(heap)); +} + +bool mi_theap_stats_get(mi_theap_t* theap, mi_stats_t* stats) mi_attr_noexcept { + return mi_stats_copy(stats, mi_theap_get_stats(theap)); +} + +void mi_theap_stats_merge_to_heap(mi_theap_t* theap) mi_attr_noexcept { + if (theap == NULL) return; + _mi_theap_merge_stats(theap); +} + +static bool mi_cdecl mi_heap_aggregate_visitor(mi_heap_t* heap, void* arg) { + mi_stats_t* stats = (mi_stats_t*)arg; + mi_stats_add_into(stats, mi_heap_get_stats(heap)); + return true; +} + +bool mi_subproc_stats_get(mi_subproc_id_t subproc_id, mi_stats_t* stats) mi_attr_noexcept { + if (stats==NULL) return false; + mi_subproc_t* subproc = _mi_subproc_from_id(subproc_id); + if (subproc == NULL) return false; + if (!mi_stats_copy(stats, &subproc->stats)) return false; + mi_subproc_visit_heaps(subproc_id, &mi_heap_aggregate_visitor, stats); + // hide meta data stats + // if (subproc->theap_meta!=NULL) { mi_stats_add_into(stats, &subproc->theap_meta->stats); } + return true; +} + +bool mi_stats_get(mi_stats_t* stats) mi_attr_noexcept { + return mi_subproc_stats_get(mi_subproc_current(), stats); +} + + +// -------------------------------------------------------- +// Statics in json format +// -------------------------------------------------------- + +typedef struct mi_json_buf_s { + char* buf; + size_t size; + size_t used; + bool can_realloc; +} mi_json_buf_t; + +static bool mi_json_buf_expand(mi_json_buf_t* hbuf) { + if (hbuf==NULL) return false; + if (hbuf->buf != NULL && hbuf->size>0) { + hbuf->buf[hbuf->size-1] = 0; + } + if (hbuf->size > SIZE_MAX/2 || !hbuf->can_realloc) return false; + const size_t newsize = (hbuf->size == 0 ? mi_good_size(12*MI_KiB) : 2*hbuf->size); + char* const newbuf = (char*)mi_rezalloc(hbuf->buf, newsize); + if (newbuf == NULL) return false; + hbuf->buf = newbuf; + hbuf->size = newsize; + return true; +} + +static void mi_json_buf_print(mi_json_buf_t* hbuf, const char* msg) { + if (msg==NULL || hbuf==NULL) return; + if (hbuf->used + 1 >= hbuf->size && !hbuf->can_realloc) return; + for (const char* src = msg; *src != 0; src++) { + char c = *src; + if (hbuf->used + 1 >= hbuf->size) { + if (!mi_json_buf_expand(hbuf)) return; + } + mi_assert_internal(hbuf->used < hbuf->size); + hbuf->buf[hbuf->used++] = c; + } + mi_assert_internal(hbuf->used < hbuf->size); + hbuf->buf[hbuf->used] = 0; +} + +static void mi_json_buf_print_count_bin(mi_json_buf_t* hbuf, const char* prefix, const mi_stat_count_t* stat, size_t bin, bool add_comma) { + const size_t binsize = mi_stats_get_bin_size(bin); + const size_t pagesize = (binsize <= MI_SMALL_MAX_OBJ_SIZE ? MI_SMALL_PAGE_SIZE : + (mi_block_size_is_medium(binsize) ? MI_MEDIUM_PAGE_SIZE : + (binsize <= MI_LARGE_MAX_OBJ_SIZE ? MI_LARGE_PAGE_SIZE : 0))); + char buf[128]; + _mi_snprintf(buf, 128, "%s{ \"total\": %lld, \"peak\": %lld, \"current\": %lld, \"block_size\": %zu, \"page_size\": %zu }%s\n", prefix, stat->total, stat->peak, stat->current, binsize, pagesize, (add_comma ? "," : "")); + buf[127] = 0; + mi_json_buf_print(hbuf, buf); +} + +static void mi_json_buf_print_count_cbin(mi_json_buf_t* hbuf, const char* prefix, const mi_stat_count_t* stat, mi_chunkbin_t bin, bool add_comma) { + const char* cbin = " "; + switch(bin) { + case MI_CBIN_SMALL: cbin = "S"; break; + case MI_CBIN_MEDIUM: cbin = "M"; break; + case MI_CBIN_LARGE: cbin = "L"; break; + case MI_CBIN_HUGE: cbin = "H"; break; + case MI_CBIN_OTHER: cbin = "X"; break; + default: cbin = " "; break; + } + char buf[128]; + _mi_snprintf(buf, 128, "%s{ \"total\": %lld, \"peak\": %lld, \"current\": %lld, \"bin\": \"%s\" }%s\n", prefix, stat->total, stat->peak, stat->current, cbin, (add_comma ? "," : "")); + buf[127] = 0; + mi_json_buf_print(hbuf, buf); +} + +static void mi_json_buf_print_count(mi_json_buf_t* hbuf, const char* prefix, const mi_stat_count_t* stat, bool add_comma) { + char buf[128]; + _mi_snprintf(buf, 128, "%s{ \"total\": %lld, \"peak\": %lld, \"current\": %lld }%s\n", prefix, stat->total, stat->peak, stat->current, (add_comma ? "," : "")); + buf[127] = 0; + mi_json_buf_print(hbuf, buf); +} + +static void mi_json_buf_print_count_value(mi_json_buf_t* hbuf, const char* name, const mi_stat_count_t* stat) { + char buf[128]; + _mi_snprintf(buf, 128, " \"%s\": ", name); + buf[127] = 0; + mi_json_buf_print(hbuf, buf); + mi_json_buf_print_count(hbuf, "", stat, true); +} + +static void mi_json_buf_print_value(mi_json_buf_t* hbuf, const char* name, int64_t val) { + char buf[128]; + _mi_snprintf(buf, 128, " \"%s\": %lld,\n", name, val); + buf[127] = 0; + mi_json_buf_print(hbuf, buf); +} + +static void mi_json_buf_print_size(mi_json_buf_t* hbuf, const char* name, size_t val, bool add_comma) { + char buf[128]; + _mi_snprintf(buf, 128, " \"%s\": %zu%s\n", name, val, (add_comma ? "," : "")); + buf[127] = 0; + mi_json_buf_print(hbuf, buf); +} + +static void mi_json_buf_print_counter_value(mi_json_buf_t* hbuf, const char* name, const mi_stat_counter_t* stat) { + mi_json_buf_print_value(hbuf, name, stat->total); +} + +static char* mi_stats_get_json_from(const mi_stats_t* stats, size_t output_size, char* output_buf) mi_attr_noexcept { + if (stats==NULL || stats->size!=sizeof(mi_stats_t) || stats->version!=MI_STAT_VERSION) return NULL; + mi_json_buf_t hbuf = { NULL, 0, 0, true }; + if (output_size > 0 && output_buf != NULL) { + mi_memzero(output_buf, output_size); + hbuf.buf = output_buf; + hbuf.size = output_size; + hbuf.can_realloc = false; + } + else { + if (!mi_json_buf_expand(&hbuf)) return NULL; + } + mi_json_buf_print(&hbuf, "{\n"); + mi_json_buf_print_value(&hbuf, "stat_version", MI_STAT_VERSION); + mi_json_buf_print_value(&hbuf, "mimalloc_version", MI_MALLOC_VERSION); + + // process info + mi_json_buf_print(&hbuf, " \"process\": {\n"); + size_t elapsed; + size_t user_time; + size_t sys_time; + size_t current_rss; + size_t peak_rss; + size_t current_commit; + size_t peak_commit; + size_t page_faults; + mi_process_info(&elapsed, &user_time, &sys_time, ¤t_rss, &peak_rss, ¤t_commit, &peak_commit, &page_faults); + mi_json_buf_print_size(&hbuf, "elapsed_msecs", elapsed, true); + mi_json_buf_print_size(&hbuf, "user_msecs", user_time, true); + mi_json_buf_print_size(&hbuf, "system_msecs", sys_time, true); + mi_json_buf_print_size(&hbuf, "page_faults", page_faults, true); + mi_json_buf_print_size(&hbuf, "rss_current", current_rss, true); + mi_json_buf_print_size(&hbuf, "rss_peak", peak_rss, true); + mi_json_buf_print_size(&hbuf, "commit_current", current_commit, true); + mi_json_buf_print_size(&hbuf, "commit_peak", peak_commit, false); + mi_json_buf_print(&hbuf, " },\n"); + + // statistics + #define MI_STAT_COUNT(stat) mi_json_buf_print_count_value(&hbuf, #stat, &stats->stat); + #define MI_STAT_COUNTER(stat) mi_json_buf_print_counter_value(&hbuf, #stat, &stats->stat); + + MI_STAT_FIELDS() + + #undef MI_STAT_COUNT + #undef MI_STAT_COUNTER + + // size bins + mi_json_buf_print(&hbuf, " \"malloc_bins\": [\n"); + for (size_t i = 0; i <= MI_BIN_HUGE; i++) { + mi_json_buf_print_count_bin(&hbuf, " ", &stats->malloc_bins[i], i, i!=MI_BIN_HUGE); + } + mi_json_buf_print(&hbuf, " ],\n"); + mi_json_buf_print(&hbuf, " \"page_bins\": [\n"); + for (size_t i = 0; i <= MI_BIN_HUGE; i++) { + mi_json_buf_print_count_bin(&hbuf, " ", &stats->page_bins[i], i, i!=MI_BIN_HUGE); + } + mi_json_buf_print(&hbuf, " ],\n"); + mi_json_buf_print(&hbuf, " \"chunk_bins\": [\n"); + for (size_t i = 0; i < MI_CBIN_COUNT; i++) { + mi_json_buf_print_count_cbin(&hbuf, " ", &stats->chunk_bins[i], (mi_chunkbin_t)i, i!=MI_CBIN_COUNT-1); + } + mi_json_buf_print(&hbuf, " ]\n"); + mi_json_buf_print(&hbuf, "}\n"); + if (hbuf.used + 1 >= hbuf.size) { + // failed + if (hbuf.can_realloc) { mi_free(hbuf.buf); } + return NULL; + } + else { + return hbuf.buf; + } +} + +char* mi_subproc_stats_get_json(mi_subproc_id_t subproc_id, size_t buf_size, char* buf) mi_attr_noexcept { + mi_subproc_t* subproc = _mi_subproc_from_id(subproc_id); + if (subproc==NULL) return NULL; + mi_stats_t_decl(stats); + if (!mi_subproc_stats_get(subproc_id,&stats)) return NULL; + return mi_stats_get_json_from(&stats, buf_size, buf); +} + +char* mi_heap_stats_get_json(mi_heap_t* heap, size_t buf_size, char* buf) mi_attr_noexcept { + return mi_stats_get_json_from(mi_heap_get_stats(heap), buf_size, buf); +} + +char* mi_stats_get_json(size_t buf_size, char* buf) mi_attr_noexcept { + return mi_subproc_stats_get_json(mi_subproc_current(), buf_size, buf); +} + +char* mi_stats_as_json(mi_stats_t* stats, size_t buf_size, char* buf) mi_attr_noexcept { + return mi_stats_get_json_from(stats, buf_size, buf); +} diff --git a/Objects/mimalloc/util/threadlocal.c b/Objects/mimalloc/util/threadlocal.c new file mode 100644 index 000000000000000..3a8a8fdc463ccfe --- /dev/null +++ b/Objects/mimalloc/util/threadlocal.c @@ -0,0 +1,317 @@ +/* ---------------------------------------------------------------------------- +Copyright (c) 2019-2026, Microsoft Research, Daan Leijen +This is free software; you can redistribute it and/or modify it under the +terms of the MIT license. A copy of the license can be found in the file +"LICENSE" at the root of this distribution. +-----------------------------------------------------------------------------*/ + +/* ---------------------------------------------------------------------------- + Implement dynamic thread local variables (used by heap's for their theap's). + Unlike most OS native implementations there is no limit on the number + that can be allocated. +-----------------------------------------------------------------------------*/ + +#include "mimalloc.h" +#include "mimalloc/prim/prim-tls.h" +#include "mimalloc/util/libc.h" +#include "mimalloc/util/options.h" +#include "mimalloc/util/threadlocal.h" + +/* ----------------------------------------------------------- + Each thread can have (a dynamically expanding) array of + thread-local values. Each slot has a value and a version. + The version is used to safely reuse slots. +----------------------------------------------------------- */ +typedef struct mi_tls_slot_s { + size_t version; + void* value; +} mi_tls_slot_t; + +typedef struct mi_thread_locals_s { + size_t count; + mi_memid_t memid; + mi_tls_slot_t slots[1]; +} mi_thread_locals_t; + +static mi_thread_locals_t mi_thread_locals_empty = mi_init_struct_zero; + + +/* ----------------------------------------------------------- + We have 2 thread local variables which we implement with either + a C thread local declaration or using pthread keys. + - mi_thread_locals: points to an array of thread locals for most keys + - mi_slot_fast: a single dedicated thread local for slightly faster access. (used for the main heap's theap) +----------------------------------------------------------- */ + +#if MI_TLS_MODEL_PTHREADS || defined(__APPLE__) // macOS has fast pthreads +// Use pthreads +#define mi_define_thread_local(tp,name,initval) \ + static pthread_key_t __##name##_key = MI_PTHREAD_KEY_INVALID; \ + static inline tp name##_peek(void) { return (tp)mi_pthread_key_get(__##name##_key); } \ + static inline tp name##_get(void) { tp result = name##_peek(); return (result!=NULL ? result : initval); } \ + static inline bool name##_set(tp val) { return mi_pthread_key_set(&__##name##_key,val); } \ + static inline void name##_delete(void) { mi_pthread_key_delete(&__##name##_key); } + +#else +// Direct thread locals +#define mi_define_thread_local(tp,name,initval) \ + static mi_decl_thread tp __##name = initval; \ + static inline tp name##_peek(void) { return __##name; } \ + static inline tp name##_get(void) { tp result = __##name; return (result!=NULL ? result : initval); } \ + static inline bool name##_set(tp val) { __##name = val; return true; } \ + static inline void name##_delete(void) { } +#endif + +mi_define_thread_local(mi_thread_locals_t*, mi_thread_locals, &mi_thread_locals_empty) +mi_define_thread_local(void*, mi_slot_fast, NULL) + + +/* ----------------------------------------------------------- + Each key consists of the slot index in the lower bits, + and its version it the top bits. When we get a value + the version must match or we return NULL. When we set + a value, we also set the version of the key. +----------------------------------------------------------- */ + +#if MI_SIZE_BITS >= 64 +#define MI_TLS_IDX_BITS (MI_SIZE_BITS/4) /* 16 bits for the index, 48 bits for the version */ +#elif MI_SIZE_BITS >= 32 +#define MI_TLS_IDX_BITS (12) /* 12 bits for index, 20 for the version? */ +#else +#error not enough bits for the version for thread locals +#endif +#define MI_TLS_IDX_MASK ((MI_ZU(1)<> MI_TLS_IDX_BITS); +} + +static mi_thread_local_t mi_key_create( size_t index, size_t version ) { + mi_assert_internal(version != 0 && version <= MI_TLS_VERSION_MAX); + mi_assert_internal(index <= MI_TLS_IDX_MAX); + const mi_thread_local_t key = ((version << MI_TLS_IDX_BITS) | index); + mi_assert_internal(key != 0); + return key; +} + + +// dynamically reallocate the thread local slots when needed +static mi_thread_locals_t* mi_thread_locals_expand(size_t least_idx) { + mi_thread_locals_t* tls_old = mi_thread_locals_get(); + const size_t count_old = tls_old->count; + size_t count; + if (count_old==0) { + tls_old = NULL; // so we allocate fresh from mi_thread_locals_empty + count = 16; // start with 16 slots + } + else if (count_old >= 1024) { + count = count_old + 1024; // at some point increase linearly + } + else { + count = 2*count_old; // and double initially + } + if (count <= least_idx) { + count = least_idx + 1; + } + if (count > MI_TLS_IDX_MAX) { return NULL; } // too large + // allocate as meta (for secure mode) + // we could also allocate on the main heap; this is recursion safe as that uses the fast local key + mi_memid_t memid = (tls_old==NULL ? _mi_memid_none() : tls_old->memid); + mi_thread_locals_t* tls = (mi_thread_locals_t*)_mi_meta_rezalloc(_mi_subproc(), tls_old, sizeof(mi_thread_locals_t) + count*sizeof(mi_tls_slot_t), &memid); + if mi_unlikely(tls==NULL) return NULL; + tls->memid = memid; + tls->count = count; + mi_thread_locals_set(tls); + return tls; +} + +static mi_decl_noinline bool mi_thread_local_set_expand( mi_thread_local_t key, void* val ) { + if (val==NULL) return true; + const size_t idx = mi_key_index(key); + mi_thread_locals_t* tls = mi_thread_locals_expand(idx); + if (tls==NULL) { + _mi_error_message(EFAULT,"unable to allocate thread local variables\n"); + return false; + } + mi_assert_internal(tls == mi_thread_locals_get()); + mi_assert_internal(idx < tls->count); + tls->slots[idx].value = val; + tls->slots[idx].version = mi_key_version(key); + return true; +} + +// set a tls slot; returns `true` if successful. +// Can return `false` if we could not reallocate the slots array. +static mi_decl_noinline bool mi_thread_local_set_regular( mi_thread_local_t key, void* val ) { + mi_thread_locals_t* tls = mi_thread_locals_get(); + mi_assert_internal(tls!=NULL); + mi_assert_internal(key!=0); + const size_t idx = mi_key_index(key); + if mi_likely(idx < tls->count) { + tls->slots[idx].value = val; + tls->slots[idx].version = mi_key_version(key); + return true; + } + else { + return mi_thread_local_set_expand( key, val ); // tailcall + } +} + +bool _mi_thread_local_set( mi_thread_local_t key, void* val ) { + mi_assert_internal(key!=0); + if (key == mi_thread_local_key_fast) { + return mi_slot_fast_set(val); + } + else { + return mi_thread_local_set_regular(key,val); + } +} + +// get a tls slot value +static mi_decl_noinline void* mi_thread_local_get_regular( mi_thread_local_t key ) { + const mi_thread_locals_t* const tls = mi_thread_locals_peek(); + if mi_unlikely(tls==NULL) { + // this can happen if a thread local is accessed after the thread local has been freed + // from mi_thread_done or mi_process_done. + // todo: can we remove this check? now we can still call this from process done when stats are printed (which calls mi_heap_theap_peek) + return NULL; + } + const size_t idx = mi_key_index(key); + if mi_likely(idx < tls->count && mi_key_version(key) == tls->slots[idx].version) { + return tls->slots[idx].value; + } + else { + return NULL; + } +} + +// get a thread local value +void* _mi_thread_local_get( mi_thread_local_t key ) { + if mi_likely(key == mi_thread_local_key_fast) { + return mi_slot_fast_get(); + } + else { + void* p = mi_thread_local_get_regular(key); + if (key==0) { mi_assert_internal(p==NULL); } + return p; + } +} + +void _mi_thread_locals_thread_done(void) { + mi_thread_locals_t* const tls = mi_thread_locals_peek(); + if (tls!=NULL && tls->count > 0) { + _mi_meta_free(_mi_subproc(), tls, tls->memid); + mi_thread_locals_set(NULL); + } + if (mi_slot_fast_peek() != NULL) { + mi_slot_fast_set(NULL); + } +} + +/* ----------------------------------------------------------- +Create and free fresh TLS key's +----------------------------------------------------------- */ +#include "../arena/bitmap.h" + +static mi_lock_t mi_thread_locals_lock; // we need a lock in order to re-allocate the slot bits +static mi_bitmap_t* mi_thread_locals_free; // reuse an arena bitmap to track which slots were assigned (1=free, 0=in-use) +static mi_memid_t mi_thread_locals_memid; // provenance of mi_thread_locals_free +static size_t mi_thread_locals_version; // version to be able to reuse slots safely + +void _mi_thread_locals_init(void) { + mi_lock_init(&mi_thread_locals_lock); +} + +void _mi_thread_locals_done(void) { + mi_lock(&mi_thread_locals_lock) { + mi_bitmap_t* const slots = mi_thread_locals_free; + if (slots!=NULL) { + _mi_meta_free(_mi_subproc_main(), slots, mi_thread_locals_memid); + } + } + mi_lock_done(&mi_thread_locals_lock); + mi_thread_locals_delete(); + mi_slot_fast_delete(); +} + +// strange signature but allows us to reuse the arena code for claiming free pages +static bool mi_thread_local_claim_fun(size_t _slice_index, mi_arena_t* _arena, bool* keep_set) { + MI_UNUSED(_slice_index); MI_UNUSED(_arena); + *keep_set = false; + return true; +} + +// When we claim a free slot, we increase the global version counter +// (so if we reuse a slot it will be returning NULL initially when a thread tries to get it) +static mi_thread_local_t mi_thread_local_claim(void) { + size_t idx = 0; + if (mi_thread_locals_free != NULL && mi_bitmap_try_find_and_claim(mi_thread_locals_free,0,&idx,&mi_thread_local_claim_fun,NULL)) { + mi_thread_locals_version++; + if (mi_thread_locals_version >= MI_TLS_VERSION_MAX) { mi_thread_locals_version = 1; } /* wrap around the version */ + return mi_key_create( idx, mi_thread_locals_version); + } + else { + return 0; + } +} + +static bool mi_thread_local_create_expand(void) { + mi_bitmap_t* const slots = mi_thread_locals_free; + // 1024 bits at a time + const size_t oldcount = (slots==NULL ? 0 : mi_bitmap_max_bits(slots)); + const size_t newcount = 1024 + oldcount; + if (newcount > MI_TLS_IDX_MAX) { return false; } + const size_t newsize = mi_bitmap_size( newcount, NULL ); + // mi_bitmap_t* newslots = (mi_bitmap_t*)mi_zalloc_aligned(newsize, MI_BCHUNK_SIZE); + mi_memid_t memid; + mi_bitmap_t* newslots = (mi_bitmap_t*)_mi_meta_zalloc_aligned(_mi_subproc_main(), newsize, MI_BCHUNK_SIZE, &memid); // always allocate thread locals in the main subprocess + mi_assert_internal(mi_is_aligned(newslots,MI_BCHUNK_SIZE)); + if (newslots==NULL) { return false; } + if (slots!=NULL) { + // copy over the previous bitmap + const size_t oldsize = mi_bitmap_size(oldcount,NULL); + mi_memcpy_aligned(newslots, slots, oldsize); + _mi_meta_free(_mi_subproc_main(), slots, mi_thread_locals_memid); + } + mi_bitmap_init(newslots, newcount, true /* pretend already zero'd so we do not zero out the copied old entries */); + mi_bitmap_unsafe_setN(newslots, oldcount, newcount - oldcount); /* set the new expanded slots as available */ + mi_thread_locals_free = newslots; + mi_thread_locals_memid = memid; + return true; +} + + +// create a fresh key +mi_thread_local_t _mi_thread_local_create(void) { + mi_thread_local_t key = 0; + mi_lock(&mi_thread_locals_lock) { + key = mi_thread_local_claim(); + if (key==0) { + if (mi_thread_local_create_expand()) { + key = mi_thread_local_claim(); + } + } + } + mi_assert_internal(key!=0); + mi_assert_internal(key!=mi_thread_local_key_fast); + return key; +} + +// free a key +void _mi_thread_local_free(mi_thread_local_t key) { + if (key==0) return; + const size_t idx = mi_key_index(key); + mi_lock(&mi_thread_locals_lock) { + mi_bitmap_t* const slots = mi_thread_locals_free; + if (slots!=NULL && idx < mi_bitmap_max_bits(slots)) { + mi_bitmap_set(slots,idx); + } + } +} diff --git a/Objects/obmalloc.c b/Objects/obmalloc.c index 096b8b08aa73eba..14082fd6a9ea60b 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -16,12 +16,6 @@ #include // fopen(), fgets(), sscanf() #include // errno #ifdef WITH_MIMALLOC -// Forward declarations of functions used in our mimalloc modifications -static void _PyMem_mi_page_clear_qsbr(mi_page_t *page); -static bool _PyMem_mi_page_is_safe_to_free(mi_page_t *page); -static bool _PyMem_mi_page_maybe_free(mi_page_t *page, mi_page_queue_t *pq, bool force); -static void _PyMem_mi_page_reclaimed(mi_page_t *page); -static void _PyMem_mi_heap_collect_qsbr(mi_heap_t *heap); # include "pycore_mimalloc.h" # include "mimalloc/static.c" # include "mimalloc/internal.h" // for stats @@ -96,38 +90,11 @@ _PyMem_RawFree(void *Py_UNUSED(ctx), void *ptr) #ifdef WITH_MIMALLOC -static void -_PyMem_mi_page_clear_qsbr(mi_page_t *page) -{ -#ifdef Py_GIL_DISABLED - // Clear the QSBR goal and remove the page from the QSBR linked list. - page->qsbr_goal = 0; - if (page->qsbr_node.next != NULL) { - llist_remove(&page->qsbr_node); - } -#endif -} - -// Check if an empty, newly reclaimed page is safe to free now. -static bool -_PyMem_mi_page_is_safe_to_free(mi_page_t *page) -{ - assert(mi_page_all_free(page)); -#ifdef Py_GIL_DISABLED - assert(page->qsbr_node.next == NULL); - if (page->use_qsbr && page->qsbr_goal != 0) { - _PyThreadStateImpl *tstate = (_PyThreadStateImpl *)_PyThreadState_GET(); - if (tstate == NULL) { - return false; - } - return _Py_qbsr_goal_reached(tstate->qsbr, page->qsbr_goal); - } -#endif - return true; - -} +static bool count_blocks(const mi_heap_t *, const mi_heap_area_t *, + void *, size_t, void *); #ifdef Py_GIL_DISABLED +static _Py_thread_local struct _qsbr_shared *mimalloc_init_qsbr; // If we are deferring collection of more than this amount of memory for // mimalloc pages, advance the write sequence. Advancing allows these @@ -137,10 +104,8 @@ _PyMem_mi_page_is_safe_to_free(mi_page_t *page) // Return true if the global write sequence should be advanced for a mimalloc // page that is deferred from collection. static bool -should_advance_qsbr_for_page(struct _qsbr_thread_state *qsbr, mi_page_t *page) +should_advance_qsbr_for_page(struct _qsbr_thread_state *qsbr, size_t page_size) { - size_t bsize = mi_page_block_size(page); - size_t page_size = page->capacity*bsize; if (page_size > QSBR_PAGE_MEM_LIMIT) { qsbr->deferred_page_memory = 0; return true; @@ -153,116 +118,130 @@ should_advance_qsbr_for_page(struct _qsbr_thread_state *qsbr, mi_page_t *page) return false; } -static _PyThreadStateImpl * -tstate_from_heap(mi_heap_t *heap) +void +mi_heap_qsbr_init(mi_heap_qsbr_t *heap) { - return _Py_CONTAINER_OF(heap->tld, _PyThreadStateImpl, mimalloc.tld); + assert(mimalloc_init_qsbr != NULL); + heap->shared = mimalloc_init_qsbr; } -#endif -static bool -_PyMem_mi_page_maybe_free(mi_page_t *page, mi_page_queue_t *pq, bool force) +void +mi_page_qsbr_clear(mi_page_qsbr_t *page) { -#ifdef Py_GIL_DISABLED - assert(mi_page_all_free(page)); - if (page->use_qsbr) { - struct _qsbr_thread_state *qsbr = ((_PyThreadStateImpl *)PyThreadState_GET())->qsbr; - if (page->qsbr_goal != 0 && _Py_qbsr_goal_reached(qsbr, page->qsbr_goal)) { - _PyMem_mi_page_clear_qsbr(page); - _mi_page_free(page, pq, force); - return true; - } - - // gh-145615: since we are not freeing this page yet, we want to - // make it available for allocations. Note that the QSBR goal and - // linked list node remain set even if the page is later used for - // an allocation. We only detect and clear the QSBR goal when the - // page becomes empty again (used == 0). - if (mi_page_is_in_full(page)) { - _mi_page_unfull(page); - } + page->goal = 0; +} - _PyMem_mi_page_clear_qsbr(page); - page->retire_expire = 0; +bool +mi_page_qsbr_is_clear(const mi_page_qsbr_t *page) +{ + return page->goal == 0; +} - if (should_advance_qsbr_for_page(qsbr, page)) { - page->qsbr_goal = _Py_qsbr_advance(qsbr->shared); +bool +mi_page_qsbr_should_delay(mi_page_qsbr_t *page, mi_heap_qsbr_t *heap, + size_t page_size) +{ + if (heap->shared == NULL) { + // Retained allocations can outlive their interpreter and its readers. + page->goal = QSBR_INITIAL; + return true; + } + if (page->goal == 0) { + struct _qsbr_thread_state *qsbr = NULL; + PyThreadState *tstate = _PyThreadState_GET(); + if (tstate != NULL && &tstate->interp->qsbr == heap->shared && + _Py_atomic_load_int_relaxed(&tstate->state) == _Py_THREAD_ATTACHED) + { + qsbr = ((_PyThreadStateImpl *)tstate)->qsbr; + qsbr->should_process = true; + } + if (qsbr == NULL || should_advance_qsbr_for_page(qsbr, page_size)) { + page->goal = _Py_qsbr_advance(heap->shared); } else { - page->qsbr_goal = _Py_qsbr_shared_next(qsbr->shared); + page->goal = _Py_qsbr_shared_next(heap->shared); } + } + // Hooks can run under allocator locks, so only consult cached progress. + uint64_t rd_seq = _Py_atomic_load_uint64(&heap->shared->rd_seq); + return !QSBR_LEQ(page->goal, rd_seq); +} - // We may be freeing a page belonging to a different thread during a - // stop-the-world event. Find the _PyThreadStateImpl for the page. - _PyThreadStateImpl *tstate = tstate_from_heap(mi_page_heap(page)); - llist_insert_tail(&tstate->mimalloc.page_list, &page->qsbr_node); - return false; +int +_PyMem_InitMimalloc(PyInterpreterState *interp) +{ + mi_option_set(mi_option_max_warnings, 0); + size_t base_offset = offsetof(PyObject, ob_type); + if (_PyMem_DebugEnabled()) { + base_offset += 2 * sizeof(size_t); + } + size_t debug_offsets[_Py_MIMALLOC_HEAP_COUNT] = { + [_Py_MIMALLOC_HEAP_OBJECT] = base_offset, + [_Py_MIMALLOC_HEAP_GC] = base_offset, + [_Py_MIMALLOC_HEAP_GC_PRE] = base_offset + 2 * sizeof(PyObject *), + }; + + mimalloc_init_qsbr = &interp->qsbr; + for (int i = 0; i < _Py_MIMALLOC_HEAP_COUNT; i++) { + mi_heap_t *heap = (i == _Py_MIMALLOC_HEAP_MEM) + ? mi_heap_new() : mi_heap_new_qsbr(); + if (heap == NULL) { + mimalloc_init_qsbr = NULL; + _PyMem_FiniMimalloc(interp); + return -1; + } + mi_heap_set_debug_fill_preserve(heap, debug_offsets[i]); + interp->mimalloc.heaps[i] = heap; } -#endif - _mi_page_free(page, pq, force); - return true; + mimalloc_init_qsbr = NULL; + return 0; } -static void -_PyMem_mi_page_reclaimed(mi_page_t *page) +void +_PyMem_FiniMimalloc(PyInterpreterState *interp) { -#ifdef Py_GIL_DISABLED - assert(page->qsbr_node.next == NULL); - if (page->qsbr_goal != 0) { - if (mi_page_all_free(page)) { - assert(page->qsbr_node.next == NULL); - _PyThreadStateImpl *tstate = tstate_from_heap(mi_page_heap(page)); - assert(tstate == (_PyThreadStateImpl *)_PyThreadState_GET()); - page->retire_expire = 0; - llist_insert_tail(&tstate->mimalloc.page_list, &page->qsbr_node); + for (int i = 0; i < _Py_MIMALLOC_HEAP_COUNT; i++) { + mi_heap_t *heap = interp->mimalloc.heaps[i]; + if (heap == NULL) { + continue; + } + size_t used = 0; + mi_heap_visit_blocks(heap, false, count_blocks, &used); + if (used == 0) { + mi_heap_destroy(heap); + } + else if (i == _Py_MIMALLOC_HEAP_MEM) { + mi_heap_delete(heap); } else { - page->qsbr_goal = 0; + // Like pymalloc, retain leaked objects that extensions may still use. + heap->qsbr.shared = NULL; } + interp->mimalloc.heaps[i] = NULL; } -#endif } static void -_PyMem_mi_heap_collect_qsbr(mi_heap_t *heap) +mimalloc_collect_qsbr(_PyThreadStateImpl *tstate) { -#ifdef Py_GIL_DISABLED - if (!heap->page_use_qsbr) { - return; - } - - _PyThreadStateImpl *tstate = (_PyThreadStateImpl *)_PyThreadState_GET(); - struct llist_node *head = &tstate->mimalloc.page_list; - if (llist_empty(head)) { - return; - } - - struct llist_node *node; - llist_for_each_safe(node, head) { - mi_page_t *page = llist_data(node, mi_page_t, qsbr_node); - if (!mi_page_all_free(page)) { - // We allocated from this page some point after the delayed free - _PyMem_mi_page_clear_qsbr(page); - continue; - } - - if (!_Py_qsbr_poll(tstate->qsbr, page->qsbr_goal)) { - return; + _Py_qsbr_poll(tstate->qsbr, _Py_qsbr_shared_current(tstate->qsbr->shared)); + for (int i = _Py_MIMALLOC_HEAP_OBJECT; i < _Py_MIMALLOC_HEAP_COUNT; i++) { + mi_theap_t *theap = tstate->mimalloc.heaps[i]; + _mi_theap_qsbr_collect_delayed(theap); + if (theap->qsbr_first != NULL) { + tstate->qsbr->should_process = true; } - - _PyMem_mi_page_clear_qsbr(page); - _mi_page_free(page, mi_page_queue_of(page), false); } -#endif } +#endif void * _PyMem_MiMalloc(void *ctx, size_t size) { #ifdef Py_GIL_DISABLED _PyThreadStateImpl *tstate = (_PyThreadStateImpl *)_PyThreadState_GET(); - mi_heap_t *heap = &tstate->mimalloc.heaps[_Py_MIMALLOC_HEAP_MEM]; - return mi_heap_malloc(heap, size); + mi_theap_t *heap = tstate->mimalloc.heaps[_Py_MIMALLOC_HEAP_MEM]; + return mi_theap_malloc(heap, size); #else return mi_malloc(size); #endif @@ -273,8 +252,8 @@ _PyMem_MiCalloc(void *ctx, size_t nelem, size_t elsize) { #ifdef Py_GIL_DISABLED _PyThreadStateImpl *tstate = (_PyThreadStateImpl *)_PyThreadState_GET(); - mi_heap_t *heap = &tstate->mimalloc.heaps[_Py_MIMALLOC_HEAP_MEM]; - return mi_heap_calloc(heap, nelem, elsize); + mi_theap_t *heap = tstate->mimalloc.heaps[_Py_MIMALLOC_HEAP_MEM]; + return mi_theap_calloc(heap, nelem, elsize); #else return mi_calloc(nelem, elsize); #endif @@ -285,8 +264,8 @@ _PyMem_MiRealloc(void *ctx, void *ptr, size_t size) { #ifdef Py_GIL_DISABLED _PyThreadStateImpl *tstate = (_PyThreadStateImpl *)_PyThreadState_GET(); - mi_heap_t *heap = &tstate->mimalloc.heaps[_Py_MIMALLOC_HEAP_MEM]; - return mi_heap_realloc(heap, ptr, size); + mi_theap_t *heap = tstate->mimalloc.heaps[_Py_MIMALLOC_HEAP_MEM]; + return mi_theap_realloc(heap, ptr, size); #else return mi_realloc(ptr, size); #endif @@ -303,8 +282,8 @@ _PyObject_MiMalloc(void *ctx, size_t nbytes) { #ifdef Py_GIL_DISABLED _PyThreadStateImpl *tstate = (_PyThreadStateImpl *)_PyThreadState_GET(); - mi_heap_t *heap = tstate->mimalloc.current_object_heap; - return mi_heap_malloc(heap, nbytes); + mi_theap_t *heap = tstate->mimalloc.current_object_heap; + return mi_theap_malloc(heap, nbytes); #else return mi_malloc(nbytes); #endif @@ -315,8 +294,8 @@ _PyObject_MiCalloc(void *ctx, size_t nelem, size_t elsize) { #ifdef Py_GIL_DISABLED _PyThreadStateImpl *tstate = (_PyThreadStateImpl *)_PyThreadState_GET(); - mi_heap_t *heap = tstate->mimalloc.current_object_heap; - return mi_heap_calloc(heap, nelem, elsize); + mi_theap_t *heap = tstate->mimalloc.current_object_heap; + return mi_theap_calloc(heap, nelem, elsize); #else return mi_calloc(nelem, elsize); #endif @@ -335,8 +314,8 @@ _PyObject_MiRealloc(void *ctx, void *ptr, size_t nbytes) return ptr; } - mi_heap_t *heap = tstate->mimalloc.current_object_heap; - void* newp = mi_heap_malloc(heap, nbytes); + mi_theap_t *heap = tstate->mimalloc.current_object_heap; + void* newp = mi_theap_malloc(heap, nbytes); if (newp == NULL) { return NULL; } @@ -345,9 +324,9 @@ _PyObject_MiRealloc(void *ctx, void *ptr, size_t nbytes) // fields for a period of time after the object is freed (see InternalDocs/qsbr.md). // These fields are typically initialized by PyObject_Init() using relaxed // atomic stores. We need to copy these fields in a thread-safe way here. - // We use the "debug_offset" to determine how many bytes to copy -- it + // We use "debug_fill_preserve" to determine how many bytes to copy -- it // includes the PyObject header and plus any extra pre-headers. - size_t offset = heap->debug_offset; + size_t offset = mi_atomic_load_relaxed(&mi_theap_heap(heap)->debug_fill_preserve); assert(offset % sizeof(void*) == 0); size_t copy_size = (size < nbytes ? size : nbytes); @@ -360,12 +339,12 @@ _PyObject_MiRealloc(void *ctx, void *ptr, size_t nbytes) memcpy(&word, (char*)ptr + i, sizeof(void*)); _Py_atomic_store_ptr_relaxed((void**)((char*)newp + i), word); } - _mi_memcpy((char*)newp + offset, (char*)ptr + offset, copy_size - offset); + mi_memcpy((char*)newp + offset, (char*)ptr + offset, copy_size - offset); } else { // memcpy(dst, NULL, 0) is undefined behavior. See gh-151297. if mi_likely(ptr) { - _mi_memcpy(newp, ptr, copy_size); + mi_memcpy(newp, ptr, copy_size); } } mi_free(ptr); @@ -1623,6 +1602,9 @@ _PyMem_ProcessDelayed(PyThreadState *tstate) _PyThreadStateImpl *tstate_impl = (_PyThreadStateImpl *)tstate; tstate_impl->qsbr->should_process = false; +#ifdef Py_GIL_DISABLED + mimalloc_collect_qsbr(tstate_impl); +#endif // Process thread-local work process_queue(&tstate_impl->mem_free_queue, tstate_impl, true, NULL, NULL); @@ -1636,12 +1618,21 @@ _PyMem_ProcessDelayedNoDealloc(PyThreadState *tstate, delayed_dealloc_cb cb, voi { PyInterpreterState *interp = tstate->interp; _PyThreadStateImpl *tstate_impl = (_PyThreadStateImpl *)tstate; +#ifdef Py_GIL_DISABLED + assert(interp->stoptheworld.world_stopped); + mimalloc_collect_qsbr(tstate_impl); +#endif // Process thread-local work process_queue(&tstate_impl->mem_free_queue, tstate_impl, true, cb, state); // Process shared interpreter work maybe_process_interp_queue(&interp->mem_free_queue, tstate_impl, cb, state); +#ifdef Py_GIL_DISABLED + for (int i = 0; i < _Py_MIMALLOC_HEAP_COUNT; i++) { + _mi_heap_collect_abandoned(interp->mimalloc.heaps[i]); + } +#endif } void @@ -1816,23 +1807,14 @@ get_mimalloc_allocated_blocks(PyInterpreterState *interp) { size_t allocated_blocks = 0; #ifdef Py_GIL_DISABLED - _Py_FOR_EACH_TSTATE_UNLOCKED(interp, t) { - _PyThreadStateImpl *tstate = (_PyThreadStateImpl *)t; - for (int i = 0; i < _Py_MIMALLOC_HEAP_COUNT; i++) { - mi_heap_t *heap = &tstate->mimalloc.heaps[i]; - mi_heap_visit_blocks(heap, false, &count_blocks, &allocated_blocks); - } - } - - mi_abandoned_pool_t *pool = &interp->mimalloc.abandoned_pool; - for (uint8_t tag = 0; tag < _Py_MIMALLOC_HEAP_COUNT; tag++) { - _mi_abandoned_pool_visit_blocks(pool, tag, false, &count_blocks, - &allocated_blocks); + for (int i = 0; i < _Py_MIMALLOC_HEAP_COUNT; i++) { + mi_heap_visit_blocks(interp->mimalloc.heaps[i], false, + &count_blocks, &allocated_blocks); } #else // TODO(sgross): this only counts the current thread's blocks. - mi_heap_t *heap = mi_heap_get_default(); - mi_heap_visit_blocks(heap, false, &count_blocks, &allocated_blocks); + mi_theap_visit_blocks(mi_theap_get_default(), false, + &count_blocks, &allocated_blocks); #endif return allocated_blocks; } @@ -3032,11 +3014,12 @@ fill_mem_debug(debug_alloc_api_t *api, void *data, int c, size_t nbytes, _PyThreadStateImpl *tstate = (_PyThreadStateImpl *)_PyThreadState_GET(); size_t debug_offset; if (is_alloc) { - debug_offset = tstate->mimalloc.current_object_heap->debug_offset; + mi_heap_t *heap = mi_theap_heap(tstate->mimalloc.current_object_heap); + debug_offset = mi_atomic_load_relaxed(&heap->debug_fill_preserve); } else { char *alloc = (char *)data - 2*SST; // start of the allocation - debug_offset = _mi_ptr_page(alloc)->debug_offset; + debug_offset = mi_atomic_load_relaxed(&mi_heap_of(alloc)->debug_fill_preserve); } debug_offset -= 2*SST; // account for pymalloc extra bytes if (debug_offset < nbytes) { @@ -3675,16 +3658,15 @@ static void py_mimalloc_print_stats(FILE *out) { fprintf(out, "Small block threshold = %zu, in %u size classes.\n", - (size_t)MI_SMALL_OBJ_SIZE_MAX, MI_BIN_HUGE); + (size_t)MI_SMALL_SIZE_MAX, MI_BIN_HUGE); fprintf(out, "Medium block threshold = %zu\n", - (size_t)MI_MEDIUM_OBJ_SIZE_MAX); + (size_t)MI_MEDIUM_MAX_OBJ_SIZE); fprintf(out, "Large object max size = %zu\n", - (size_t)MI_LARGE_OBJ_SIZE_MAX); + (size_t)MI_LARGE_MAX_OBJ_SIZE); - mi_heap_t *heap = mi_heap_get_default(); struct _alloc_stats stats; memset(&stats, 0, sizeof(stats)); - mi_heap_visit_blocks(heap, false, &_collect_alloc_stats, &stats); + mi_theap_visit_blocks(mi_theap_get_default(), false, &_collect_alloc_stats, &stats); fprintf(out, " Allocated Blocks: %zd\n", stats.allocated_blocks); fprintf(out, " Allocated Bytes: %zd\n", stats.allocated_bytes); diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index c2d8f64a72b1191..c634898feb1ab26 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -353,12 +353,7 @@ - - - - - - + diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters index 2536fc971700deb..eab13fa97d3286e 100644 --- a/PCbuild/pythoncore.vcxproj.filters +++ b/PCbuild/pythoncore.vcxproj.filters @@ -939,22 +939,7 @@ Include\internal - - Include\internal\mimalloc - - - Include\internal\mimalloc - - - Include\internal\mimalloc - - - Include\internal\mimalloc - - - Include\internal\mimalloc - - + Include\internal\mimalloc diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index adf73d79a46661b..8d76bcd818f348c 100644 --- a/Python/gc_free_threading.c +++ b/Python/gc_free_threading.c @@ -303,16 +303,13 @@ static void gc_restore_tid(PyObject *op) { assert(_PyInterpreterState_GET()->stoptheworld.world_stopped); - mi_segment_t *segment = _mi_ptr_segment(op); + mi_page_t *page = mi_ptr_page(op); if (_Py_REF_IS_MERGED(op->ob_ref_shared)) { op->ob_tid = 0; } else { - // NOTE: may change ob_tid if the object was re-initialized by - // a different thread or its segment was abandoned and reclaimed. - // The segment thread id might be zero, in which case we should - // ensure the refcounts are now merged. - op->ob_tid = segment->thread_id; + // Reclaimed pages may have a new owner; abandoned pages have none. + op->ob_tid = mi_page_is_abandoned(page) ? 0 : mi_page_thread_id(page); if (op->ob_tid == 0) { merge_refcount(op, 0); } @@ -379,36 +376,15 @@ gc_visit_heaps_lock_held(PyInterpreterState *interp, mi_block_visit_fun *visitor // Objects with Py_TPFLAGS_PREHEADER have two extra fields Py_ssize_t offset_pre = offset_base + 2 * sizeof(PyObject*); - // visit each thread's heaps for GC objects - _Py_FOR_EACH_TSTATE_UNLOCKED(interp, p) { - struct _mimalloc_thread_state *m = &((_PyThreadStateImpl *)p)->mimalloc; - if (!_Py_atomic_load_int(&m->initialized)) { - // The thread may not have called tstate_mimalloc_bind() yet. - continue; - } - - arg->offset = offset_base; - if (!mi_heap_visit_blocks(&m->heaps[_Py_MIMALLOC_HEAP_GC], true, - visitor, arg)) { - return -1; - } - arg->offset = offset_pre; - if (!mi_heap_visit_blocks(&m->heaps[_Py_MIMALLOC_HEAP_GC_PRE], true, - visitor, arg)) { - return -1; - } - } - - // visit blocks in the per-interpreter abandoned pool (from dead threads) - mi_abandoned_pool_t *pool = &interp->mimalloc.abandoned_pool; + // Interpreter heaps include pages owned by threads and abandoned pages. arg->offset = offset_base; - if (!_mi_abandoned_pool_visit_blocks(pool, _Py_MIMALLOC_HEAP_GC, true, - visitor, arg)) { + if (!mi_heap_visit_blocks(interp->mimalloc.heaps[_Py_MIMALLOC_HEAP_GC], + true, visitor, arg)) { return -1; } arg->offset = offset_pre; - if (!_mi_abandoned_pool_visit_blocks(pool, _Py_MIMALLOC_HEAP_GC_PRE, true, - visitor, arg)) { + if (!mi_heap_visit_blocks(interp->mimalloc.heaps[_Py_MIMALLOC_HEAP_GC_PRE], + true, visitor, arg)) { return -1; } return 0; diff --git a/Python/pystate.c b/Python/pystate.c index f9debd1e20e2864..d065f6f2dfc4d60 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -466,6 +466,9 @@ alloc_interpreter(void) static void free_interpreter(PyInterpreterState *interp) { +#ifdef Py_GIL_DISABLED + _PyMem_FiniMimalloc(interp); +#endif #ifdef Py_STATS if (interp->pystats_struct) { PyMem_RawFree(interp->pystats_struct); @@ -673,6 +676,11 @@ init_interpreter(PyInterpreterState *interp, _Py_stackref_associate(interp, Py_True, PyStackRef_True); #endif +#ifdef Py_GIL_DISABLED + if (_PyMem_InitMimalloc(interp) < 0) { + return _PyStatus_NO_MEMORY(); + } +#endif interp->_initialized = 1; return _PyStatus_OK(); } @@ -1517,6 +1525,11 @@ static void free_threadstate(_PyThreadStateImpl *tstate) { PyInterpreterState *interp = tstate->base.interp; +#ifdef Py_GIL_DISABLED + for (int i = 0; i < _Py_MIMALLOC_HEAP_COUNT; i++) { + _mi_theap_decref(tstate->mimalloc.heaps[i]); + } +#endif #ifdef Py_STATS _PyStats_ThreadFini(tstate); #endif @@ -3330,48 +3343,16 @@ tstate_mimalloc_bind(PyThreadState *tstate) #ifdef Py_GIL_DISABLED struct _mimalloc_thread_state *mts = &((_PyThreadStateImpl*)tstate)->mimalloc; - // Initialize the mimalloc thread state. This must be called from the - // same thread that will use the thread state. The "mem" heap doubles as - // the "backing" heap. - mi_tld_t *tld = &mts->tld; - _mi_tld_init(tld, &mts->heaps[_Py_MIMALLOC_HEAP_MEM]); - llist_init(&mts->page_list); - - // Exiting threads push any remaining in-use segments to the abandoned - // pool to be re-claimed later by other threads. We use per-interpreter - // pools to keep Python objects from different interpreters separate. - tld->segments.abandoned = &tstate->interp->mimalloc.abandoned_pool; - - // Don't fill in the first N bytes up to ob_type in debug builds. We may - // access ob_tid and the refcount fields in the dict and list lock-less - // accesses, so they must remain valid for a while after deallocation. - size_t base_offset = offsetof(PyObject, ob_type); - if (_PyMem_DebugEnabled()) { - // The debug allocator adds two words at the beginning of each block. - base_offset += 2 * sizeof(size_t); - } - size_t debug_offsets[_Py_MIMALLOC_HEAP_COUNT] = { - [_Py_MIMALLOC_HEAP_OBJECT] = base_offset, - [_Py_MIMALLOC_HEAP_GC] = base_offset, - [_Py_MIMALLOC_HEAP_GC_PRE] = base_offset + 2 * sizeof(PyObject *), - }; - - // Initialize each heap - for (uint8_t i = 0; i < _Py_MIMALLOC_HEAP_COUNT; i++) { - _mi_heap_init_ex(&mts->heaps[i], tld, _mi_arena_id_none(), false, i); - mts->heaps[i].debug_offset = (uint8_t)debug_offsets[i]; + for (int i = 0; i < _Py_MIMALLOC_HEAP_COUNT; i++) { + mts->heaps[i] = mi_heap_theap(tstate->interp->mimalloc.heaps[i]); + // Keep the handle valid even if native TLS cleanup precedes tstate cleanup. + _mi_theap_incref(mts->heaps[i]); } - // Heaps that store Python objects should use QSBR to delay freeing - // mimalloc pages while there may be concurrent lock-free readers. - mts->heaps[_Py_MIMALLOC_HEAP_OBJECT].page_use_qsbr = true; - mts->heaps[_Py_MIMALLOC_HEAP_GC].page_use_qsbr = true; - mts->heaps[_Py_MIMALLOC_HEAP_GC_PRE].page_use_qsbr = true; - // By default, object allocations use _Py_MIMALLOC_HEAP_OBJECT. // _PyObject_GC_New() and similar functions temporarily override this to // use one of the GC heaps. - mts->current_object_heap = &mts->heaps[_Py_MIMALLOC_HEAP_OBJECT]; + mts->current_object_heap = mts->heaps[_Py_MIMALLOC_HEAP_OBJECT]; _Py_atomic_store_int(&mts->initialized, 1); #endif @@ -3388,11 +3369,8 @@ _PyThreadState_ClearMimallocHeaps(PyThreadState *tstate) _PyThreadStateImpl *tstate_impl = (_PyThreadStateImpl *)tstate; for (Py_ssize_t i = 0; i < _Py_MIMALLOC_HEAP_COUNT; i++) { - // Abandon all segments in use by this thread. This pushes them to - // a shared pool to later be reclaimed by other threads. It's important - // to do this before the thread state is destroyed so that objects - // remain visible to the GC. - _mi_heap_collect_abandon(&tstate_impl->mimalloc.heaps[i]); + // Preserve live pages in their interpreter's heap for reuse and GC. + mi_theap_quiescent_abandon(tstate_impl->mimalloc.heaps[i]); } #endif } diff --git a/Tools/build/smelly.py b/Tools/build/smelly.py index 17547d4d916e9f2..283669f688e1998 100755 --- a/Tools/build/smelly.py +++ b/Tools/build/smelly.py @@ -20,7 +20,7 @@ # mimalloc doesn't use static, but it's symbols are not exported # from the shared library. They do show up in the static library # before its linked into an executable. -ALLOWED_STATIC_PREFIXES = ('mi_', '_mi_') +ALLOWED_STATIC_PREFIXES = ('mi_', '_mi_', '__mi', '_ZSt') # "Legacy": some old symbols are prefixed by "PY_". EXCEPTIONS = frozenset({