JEMALLOC(3)

okay so bestie, this is the jemalloc(3) manual β€” general purpose memory allocation functions, and she's literally the blueprint. πŸ’… We're breaking down part 1, so let's get into it. ✨


πŸ’– NAME πŸ’–

jemalloc β€” general purpose memory allocation functions. That's the name, that's the vibe. πŸŽ€

πŸ“š LIBRARY πŸ“š

This manual describes jemalloc 5.3.0-0-g54eaed1d8b56b1aa528be3bdd1877e59c56fa90c. More information can be found at the jemalloc website[1]. Slay. 🌐


🌈 SYNOPSIS 🌈

First things first, you gotta include the header:

#include <jemalloc/jemalloc.h>

✨ Standard API ✨

Here are the standard besties:

void *malloc(size_t size);

void *calloc(size_t number, size_t size);

int posix_memalign(void **ptr, size_t alignment, size_t size);

void *aligned_alloc(size_t alignment, size_t size);

void *realloc(void *ptr, size_t size);

void free(void *ptr);

πŸ’« Non-standard API πŸ’«

And here's the non-standard squad:

void *mallocx(size_t size, int flags);

void *rallocx(void *ptr, size_t size, int flags);

size_t xallocx(void *ptr, size_t size, size_t extra, int flags);

size_t sallocx(void *ptr, int flags);

void dallocx(void *ptr, int flags);

void sdallocx(void *ptr, size_t size, int flags);

size_t nallocx(size_t size, int flags);

int mallctl(const char *name, void *oldp, size_t *oldlenp, void *newp,
            size_t newlen);

int mallctlnametomib(const char *name, size_t *mibp, size_t *miblenp);

int mallctlbymib(const size_t *mib, size_t miblen, void *oldp,
                 size_t *oldlenp, void *newp, size_t newlen);

void malloc_stats_print(void (*write_cb) (void *, const char *),
                        void *cbopaque, const char *opts);

size_t malloc_usable_size(const void *ptr);

void (*malloc_message)(void *cbopaque, const char *s);

const char *malloc_conf;

πŸ¦‹ DESCRIPTION πŸ¦‹

πŸ’– Standard API πŸ’–

The malloc() function allocates size bytes of uninitialized memory. The allocated space is suitably aligned (after possible pointer coercion) for storage of any type of object. So like, it gives you memory, but it's not zeroed out β€” it's whatever was there before. πŸ’€

The calloc() function allocates space for number objects, each size bytes in length. The result is identical to calling malloc() with an argument of number * size, with the exception that the allocated memory is explicitly initialized to zero bytes. So it's malloc but with a clean slate. ✨

The posix_memalign() function allocates size bytes of memory such that the allocation's base address is a multiple of alignment, and returns the allocation in the value pointed to by ptr. The requested alignment must be a power of 2 at least as large as sizeof(void *). So you get fancy aligned memory, but don't try any funny business with the alignment. 🚫

The aligned_alloc() function allocates size bytes of memory such that the allocation's base address is a multiple of alignment. The requested alignment must be a power of 2. Behavior is undefined if size is not an integral multiple of alignment. > bestie, undefined behavior is not the vibe. Make sure size is a multiple of alignment, fr fr. 😭

The realloc() function changes the size of the previously allocated memory referenced by ptr to size bytes. The contents of the memory are unchanged up to the lesser of the new and old sizes. If the new size is larger, the contents of the newly allocated portion of the memory are undefined. Upon success, the memory referenced by ptr is freed and a pointer to the newly allocated memory is returned. Note that realloc() may move the memory allocation, resulting in a different return value than ptr. If ptr is NULL, the realloc() function behaves identically to malloc() for the specified size. So it's like a glow-up for your memory, but sometimes it moves to a new address. πŸ’…

The free() function causes the allocated memory referenced by ptr to be made available for future allocations. If ptr is NULL, no action occurs. So you're releasing it back to the universe. 🌌

πŸŽ€ Non-standard API πŸŽ€

The mallocx(), rallocx(), xallocx(), sallocx(), dallocx(), sdallocx(), and nallocx() functions all have a flags argument that can be used to specify options. The functions only check the options that are contextually relevant. Use bitwise or (|) operations to specify one or more of the following:

  • MALLOCX_LG_ALIGN(la) β€” Align the memory allocation to start at an address that is a multiple of (1 << la). This macro does not validate that la is within the valid range. So like, you're on your own with that one. πŸ™ˆ
  • MALLOCX_ALIGN(a) β€” Align the memory allocation to start at an address that is a multiple of a, where a is a power of two. This macro does not validate that a is a power of 2. No cap, it just trusts you. 🫢
  • MALLOCX_ZERO β€” Initialize newly allocated memory to contain zero bytes. In the growing reallocation case, the real size prior to reallocation defines the boundary between untouched bytes and those that are initialized to contain zero bytes. If this macro is absent, newly allocated memory is uninitialized. So it's like calloc but for mallocx. ✨
  • MALLOCX_TCACHE(tc) β€” Use the thread-specific cache (tcache) specified by the identifier tc, which must have been acquired via the tcache.create mallctl. This macro does not validate that tc specifies a valid identifier. You gotta make sure it's valid, bestie. πŸ’…
  • MALLOCX_TCACHE_NONE β€” Do not use a thread-specific cache (tcache). Unless MALLOCX_TCACHE(tc) or MALLOCX_TCACHE_NONE is specified, an automatically managed tcache will be used under many circumstances. This macro cannot be used in the same flags argument as MALLOCX_TCACHE(tc). So pick one, okay? πŸŽ€
  • MALLOCX_ARENA(a) β€” Use the arena specified by the index a. This macro has no effect for regions that were allocated via an arena other than the one specified. This macro does not validate that a specifies an arena index in the valid range. Again, no validation, so be careful. πŸ”₯

The mallocx() function allocates at least size bytes of memory, and returns a pointer to the base address of the allocation. Behavior is undefined if size is 0. > zero size? undefined. don't do it. πŸ’€

The rallocx() function resizes the allocation at ptr to be at least size bytes, and returns a pointer to the base address of the resulting allocation, which may or may not have moved from its original location. Behavior is undefined if size is 0. Same energy. 😭

The xallocx() function resizes the allocation at ptr in place to be at least size bytes, and returns the real size of the allocation. If extra is non-zero, an attempt is made to resize the allocation to be at least (size + extra) bytes, though inability to allocate the extra byte(s) will not by itself result in failure to resize. Behavior is undefined if size is 0, or if (size + extra > SIZE_T_MAX). So watch out for that overflow. 🚨

The sallocx() function returns the real size of the allocation at ptr. Simple. πŸ’…

The dallocx() function causes the memory referenced by ptr to be made available for future allocations. It's like free but for the non-standard crew. 🌈

The sdallocx() function is an extension of dallocx() with a size parameter to allow the caller to pass in the allocation size as an optimization. The minimum valid input size is the original requested size of the allocation, and the maximum valid input size is the corresponding value returned by nallocx() or sallocx(). So you can speed things up if you know the size. ✨

The nallocx() function allocates no memory, but it performs the same size computation as the mallocx() function, and returns the real size of the allocation that would result from the equivalent mallocx() function call, or 0 if the inputs exceed the maximum supported size class and/or alignment. Behavior is undefined if size is 0. So it's like a preview of what you'd get. 🎬

The mallctl() function provides a general interface for introspecting the memory allocator, as well as setting modifiable parameters and triggering actions. The period-separated name argument specifies a location in a tree-structured namespace; see the MALLCTL NAMESPACE section for documentation on the tree contents. To read a value, pass a pointer via oldp to adequate space to contain the value, and a pointer to its length via oldlenp; otherwise pass NULL and NULL. Similarly, to write a value, pass a pointer to the value via newp, and its length via newlen; otherwise pass NULL and 0. So it's your all-access pass to the allocator's inner world. πŸ‘‘

The mallctlnametomib() function provides a way to avoid repeated name lookups for applications that repeatedly query the same portion of the namespace, by translating a name to a β€œManagement Information Base” (MIB) that can be passed repeatedly to mallctlbymib(). Upon successful return from mallctlnametomib(), mibp contains an array of *miblenp integers, where *miblenp is the lesser of the number of components in name and the input value of *miblenp. Thus it is possible to pass a *miblenp that is smaller than the number of period-separated name components, which results in a partial MIB that can be used as the basis for constructing a complete MIB. For name components that are integers (e.g. the 2 in arenas.bin.2.size), the corresponding MIB component will always be that integer. Therefore, it is legitimate to construct code like the following:

unsigned nbins, i;
size_t mib[4];
size_t len, miblen;

len = sizeof(nbins);
mallctl("arenas.nbins", &nbins, &len, NULL, 0);

miblen = 4;
mallctlnametomib("arenas.bin.0.size", mib, &miblen);
for (i = 0; i < nbins; i++) {
     size_t bin_size;

     mib[2] = i;
     len = sizeof(bin_size);
     mallctlbymib(mib, miblen, (void *)&bin_size, &len, NULL, 0);
     /* Do something with bin_size... */
}

The malloc_stats_print() function writes summary statistics via the write_cb callback function pointer and cbopaque data passed to write_cb, or malloc_message() if write_cb is NULL. The statistics are presented in human-readable form unless β€œJ” is specified as a character within the opts string, in which case the statistics are presented in JSON format[2]. This function can be called repeatedly. General information that never changes during execution can be omitted by specifying β€œg” as a character within the opts string. Note that malloc_stats_print() uses the mallctl*() functions internally, so inconsistent statistics can be reported if multiple threads use these functions simultaneously. If --enable-stats is specified during configuration, β€œm”, β€œd”, and β€œa” can be specified to omit merged arena, destroyed merged arena, and per arena statistics, respectively; β€œb” and β€œl” can be specified to omit per size class statistics for bins and large objects, respectively; β€œx” can be specified to omit all mutex statistics; β€œe” can be used to omit extent statistics. Unrecognized characters are silently ignored. Note that thread caching may prevent some statistics from being completely up to date, since extra locking would be required to merge counters that track thread cache operations. So it's a whole vibe, but watch out for those stats being a little behind. πŸ“Š

The malloc_usable_size() function returns the usable size of the allocation pointed to by ptr. The return value may be larger than the size that was requested during allocation. The malloc_usable_size() function is not a mechanism for in-place realloc(); rather it is provided solely as a tool for introspection purposes. Any discrepancy between the requested allocation size and the size reported by malloc_usable_size() should not be depended on, since such behavior is entirely implementation-dependent. So don't rely on it for anything but peeking. πŸ‘€


πŸŽ€ TUNING πŸŽ€

okay bestie, so here's the tea β˜• β€” the very first time anybody calls one of the memory allocation routines, the allocator has to wake up and initialize its whole internal situation 🧠✨ and it does that based in part on a bunch of options you can hand it at compile-time or run-time. she's customizable, she's got range, she's everything πŸ’…


🌸 where the options come from (in order, no skipping)

These four sources get interpreted in that order, from left to right, as options:

  1. the string specified via --with-malloc-conf
  2. the string pointed to by the global variable malloc_conf
  3. the "name" of the file referenced by the symbolic link named /etc/malloc.conf
  4. the value of the environment variable MALLOC_CONF
⚠️ heads up bestie: malloc_conf may be read before main() is entered 😭 so when you declare malloc_conf, you gotta specify an initializer that already contains the final value to be read by jemalloc. no lazy defaults, she wants the finished look ✨

also, key distinction because iykyk: --with-malloc-conf and malloc_conf are compile-time mechanisms, whereas /etc/malloc.conf and MALLOC_CONF can be safely set any time prior to program invocation 🫢


πŸ¦‹ how to actually write an options string

an options string is just a comma-separated list of option:value pairs. that's it. that's the whole vibe πŸŽ€

there's one key corresponding to each opt.* mallctl (go peek at the MALLCTL NAMESPACE section for the actual options documentation, she spills everything there πŸ‘‘).

like, for example:

abort:true,narenas:1

that lil string sets the opt.abort and opt.narenas options. iconic, right? two birds, one comma 🌈


πŸ’– the value types (she contains multitudes)

not every option wants the same kind of value, and honestly same:

  • some options have boolean values β€” that's true/false, binary queen πŸ‘‘
  • some have integer values β€” and these get read in base 8, 10, or 16, depending on the prefix πŸ”’
  • and yet others have raw string values β€” no math, just vibes 🌸

so yeah, know your type before you type it, bestie πŸ’œ

✨ IMPLEMENTATION NOTES ✨

okay so bestie, let's talk about why this allocator is built the way it is, because the vibes are immaculate and also extremely deliberate πŸ’…

Traditionally, allocators have used sbrk(2) to get memory, and ngl that's kind of a flop for a bunch of reasons: race conditions, more fragmentation, and artificial caps on how much memory you can actually use. So here's the tea β€” if the operating system supports sbrk(2), this allocator uses both mmap(2) and sbrk(2), in that exact order of preference. If not? Only mmap(2). Periodt.

This allocator uses multiple arenas to cut down on lock contention for threaded programs on multi-processor systems. That's a slay for threading scalability, but it does come with a lil cost: there's a small fixed per-arena overhead, and on top of that, arenas manage memory completely independently of each other, which means a small fixed increase in overall memory fragmentation. Honestly? These overheads are not usually a problem given how many arenas get used normally. Also, using substantially more arenas than the default is not likely to improve performance β€” mostly because of reduced cache performance, iykyk. But it can make sense to reduce the number of arenas if your app isn't really using the allocation functions much.

On top of multiple arenas, this allocator supports thread-specific caching, which makes it possible to completely avoid synchronization for most allocation requests πŸŽ€. That caching = very fast allocation in the common case, but it does increase memory usage and fragmentation, since a bounded number of objects can stay allocated in each thread cache. Trade-offs, bestie!

Memory is conceptually broken into extents. Extents are always aligned to multiples of the page size, and that alignment is what makes it possible to find metadata for user objects quickly. User objects split into two categories by size: small and large. Contiguous small objects make up a slab, which lives inside a single extent, whereas large objects each get their own extents backing them.

Small objects are managed in groups by slabs. Each slab keeps a bitmap to track which regions are in use. Allocation requests that are no more than half the quantum (8 or 16, depending on architecture) get rounded up to the nearest power of two that is at least sizeof(double). Every other object size class is a multiple of the quantum, spaced so that there are four size classes for each doubling in size, which limits internal fragmentation to approximately 20% for all but the smallest size classes. Small size classes are smaller than four times the page size, and large size classes extend from four times the page size all the way up to the largest size class that doesn't exceed PTRDIFF_MAX.

Allocations are packed tightly together, and that can be an issue for multi-threaded applications 😭. If you need to make sure your allocations don't suffer from cacheline sharing, round your allocation requests up to the nearest multiple of the cacheline size, or specify cacheline alignment when allocating. Do that.

The realloc(), rallocx(), and xallocx() functions may resize allocations without moving them under limited circumstances. Unlike the *allocx() API, the standard API does not officially round up the usable size of an allocation to the nearest size class, so technically you have to call realloc() to grow, like, a 9-byte allocation to 16 bytes, or shrink a 16-byte allocation to 9 bytes. Growth and shrinkage trivially succeeds in place as long as the pre-size and post-size both round up to the same size class. No other API guarantees are made regarding in-place resizing, BUT the current implementation also tries to resize large allocations in place, as long as the pre-size and post-size are both large. For shrinkage to succeed, the extent allocator must support splitting (see arena.<i>.extent_hooks). Growth only succeeds if the trailing memory is currently available and the extent allocator supports merging.

Assuming 4 KiB pages and a 16-byte quantum on a 64-bit system, the size classes in each category are as shown in Table 1 πŸ“Š

🌸 Table 1. Size classes 🌸

Category: Small

  • Spacing lg β€” Size [8]
  • Spacing 16 β€” Size [16, 32, 48, 64, 80, 96, 112, 128]
  • Spacing 32 β€” Size [160, 192, 224, 256]
  • Spacing 64 β€” Size [320, 384, 448, 512]
  • Spacing 128 β€” Size [640, 768, 896, 1024]
  • Spacing 256 β€” Size [1280, 1536, 1792, 2048]
  • Spacing 512 β€” Size [2560, 3072, 3584, 4096]
  • Spacing 1 KiB β€” Size [5 KiB, 6 KiB, 7 KiB, 8 KiB]
  • Spacing 2 KiB β€” Size [10 KiB, 12 KiB, 14 KiB]

Category: Large

  • Spacing 2 KiB β€” Size [16 KiB]
  • Spacing 4 KiB β€” Size [20 KiB, 24 KiB, 28 KiB, 32 KiB]
  • Spacing 8 KiB β€” Size [40 KiB, 48 KiB, 56 KiB, 64 KiB]
  • Spacing 16 KiB β€” Size [80 KiB, 96 KiB, 112 KiB, 128 KiB]
  • Spacing 32 KiB β€” Size [160 KiB, 192 KiB, 224 KiB, 256 KiB]
  • Spacing 64 KiB β€” Size [320 KiB, 384 KiB, 448 KiB, 512 KiB]
  • Spacing 128 KiB β€” Size [640 KiB, 768 KiB, 896 KiB, 1 MiB]
  • Spacing 256 KiB β€” Size [1280 KiB, 1536 KiB, 1792 KiB, 2 MiB]
  • Spacing 512 KiB β€” Size [2560 KiB, 3 MiB, 3584 KiB, 4 MiB]
  • Spacing 1 MiB β€” Size [5 MiB, 6 MiB, 7 MiB, 8 MiB]
  • Spacing 2 MiB β€” Size [10 MiB, 12 MiB, 14 MiB, 16 MiB]
  • Spacing 4 MiB β€” Size [20 MiB, 24 MiB, 28 MiB, 32 MiB]
  • Spacing 8 MiB β€” Size [40 MiB, 48 MiB, 56 MiB, 64 MiB]
  • Spacing ... β€” Size ...
  • Spacing 512 PiB β€” Size [2560 PiB, 3 EiB, 3584 PiB, 4 EiB]
  • Spacing 1 EiB β€” Size [5 EiB, 6 EiB, 7 EiB]

πŸ¦‹ MALLCTL NAMESPACE πŸ¦‹

These names are defined in the namespace you can reach via the mallctl*() functions 🫢. Value types are given in parentheses, their readable/writable statuses are encoded as rw, r-, -w, or --, and required build configuration flags follow, if any. A name element encoded as <i> or <j> means an integer component, where the integer varies from 0 to some upper value that you have to figure out via introspection, no cap.

In the case of stats.arenas.<i>.* and arena.<i>.{initialized,purge,decay,dss}, <i> equal to MALLCTL_ARENAS_ALL can be used to operate on all arenas or to access the summation of statistics from all arenas πŸ’–. Similarly, <i> equal to MALLCTL_ARENAS_DESTROYED can be used to access the summation of statistics from all destroyed arenas. These constants can be used either via mallctlnametomib() followed by mallctlbymib(), or via code like this:

#define STRINGIFY_HELPER(x) #x
#define STRINGIFY(x) STRINGIFY_HELPER(x)

mallctl("arena." STRINGIFY(MALLCTL_ARENAS_ALL) ".decay",
    NULL, NULL, NULL, 0);
πŸ‘‘ Take special note of the epoch mallctl, which controls refreshing of cached dynamic statistics. It's the main character here, fr fr.
  • version (const char *) r- β€” Return the jemalloc version string πŸ’…
  • epoch (uint64_t) rw β€” okay so if you actually pass a value in, it refreshes all the data that the mallctl*() functions report from, and increments the epoch. Then it returns the current epoch. This is so useful for detecting whether another thread caused a refresh. Detective work, but make it ✨memory✨. πŸ•΅οΈβ€β™€οΈ
  • background_thread (bool) rw β€” turn internal background worker threads on/off. When you set it to true, background threads get created on demand (and the number of them will be no more than the number of CPUs or the number of active arenas, whichever). Those threads run periodically and handle purging asynchronously. When you switch it off, background threads are terminated synchronously. Heads up bestie: after fork(2), the state in the child process will be disabled no matter what the state in the parent process was. See stats.background_thread for the related stats, and opt.background_thread if you wanna set the default option. Also, this one is only available on selected pthread-based platforms β€” iykyk. 🧡
  • max_background_threads (size_t) rw β€” the maximum number of background worker threads that will be created. This value is capped at opt.max_background_threads at startup. So like, don't get any ideas. πŸ’…
  • config.cache_oblivious (bool) r- β€” --enable-cache-oblivious was specified during build configuration.
  • config.debug (bool) r- β€” --enable-debug was specified during build configuration.
  • config.fill (bool) r- β€” --enable-fill was specified during build configuration.
  • config.lazy_lock (bool) r- β€” --enable-lazy-lock was specified during build configuration.
  • config.malloc_conf (const char *) r- β€” the embedded configure-time-specified run-time options string. It's empty unless --with-malloc-conf was specified during build configuration. πŸŽ€
  • config.prof (bool) r- β€” --enable-prof was specified during build configuration.
  • config.prof_libgcc (bool) r- β€” --disable-prof-libgcc was not specified during build configuration.
  • config.prof_libunwind (bool) r- β€” --enable-prof-libunwind was specified during build configuration.
  • config.stats (bool) r- β€” --enable-stats was specified during build configuration.
  • config.utrace (bool) r- β€” --enable-utrace was specified during build configuration.
  • config.xmalloc (bool) r- β€” --enable-xmalloc was specified during build configuration.

okay and now we're in opt.* territory, aka the build-time defaults, aka the "you can only read these, they're not taking suggestions" zone. πŸš«πŸ“

  • opt.abort (bool) r- β€” abort-on-warning, enabled/disabled. If true, most warnings are fatal. Note that runtime option warnings are not included in this (see opt.abort_conf for that one). The process will call abort(3) in these cases. This option is disabled by default β€” unless --enable-debug was specified during configuration, in which case it is enabled by default. 😳
  • opt.confirm_conf (bool) r- β€” confirm-runtime-options-when-program-starts, enabled/disabled. If true, all of these get printed, in order: the string specified via --with-malloc-conf, the string pointed to by the global variable malloc_conf, the β€œname” of the file referenced by the symbolic link named /etc/malloc.conf, and the value of the environment variable MALLOC_CONF. Then, each option being set will be individually printed too. Loud and proud, fr fr. This option is disabled by default.
  • opt.abort_conf (bool) r- β€” abort-on-invalid-configuration, enabled/disabled. If true, invalid runtime options are fatal, and the process will call abort(3) in these cases. This option is disabled by default β€” unless --enable-debug was specified during configuration, in which case it is enabled by default. πŸ’€
  • opt.cache_oblivious (bool) r- β€” enable/disable cache-oblivious large allocation alignment, for large requests with no alignment constraints. If this feature is disabled, all large allocations are page-aligned as an implementation artifact, which can severely harm CPU cache utilization. But here's the trade-off bestie: the cache-oblivious layout comes at the cost of one extra page per large allocation, which in the most extreme case increases physical memory usage for the 16 KiB size class to 20 KiB. This option is enabled by default.
  • opt.metadata_thp (const char *) r- β€” controls whether to allow jemalloc to use transparent huge page (THP) for internal metadata (see stats.metadata). β€œalways” allows such usage. β€œauto” uses no THP initially, but may begin to do so when metadata usage reaches a certain level. The default is β€œdisabled”. 🐘
  • opt.trust_madvise (bool) r- β€” if true, do not perform the runtime check for MADV_DONTNEED, which checks that it actually zeros pages. The default is disabled on Linux and enabled elsewhere. 🌍
  • opt.retain (bool) r- β€” if true, retain unused virtual memory for later reuse rather than discarding it by calling munmap(2) or equivalent (see stats.retained for the related details). It also makes jemalloc use mmap(2) or equivalent in a more greedy way, mapping larger chunks in one go. This option is disabled by default unless discarding virtual memory is known to trigger platform-specific performance problems, namely:
    1. for [64-bit] Linux, which has a quirk in its virtual memory allocation algorithm that causes semi-permanent VM map holes under normal jemalloc operation; and
    2. for [64-bit] Windows, which disallows split / merged regions with MEM_RELEASE.

Although the same issues may present on 32-bit platforms as well, retaining virtual memory for 32-bit Linux and Windows is disabled by default due to the practical possibility of address space exhaustion. 😭

  • opt.dss (const char *) r- β€” dss (sbrk(2)) allocation precedence as related to mmap(2) allocation. The following settings are supported if sbrk(2) is supported by the operating system: β€œdisabled”, β€œprimary”, and β€œsecondary”; otherwise only β€œdisabled” is supported. The default is β€œsecondary” if sbrk(2) is supported by the operating system, and β€œdisabled” otherwise.
  • opt.narenas (unsigned) r- β€” the maximum number of arenas to use for automatic multiplexing of threads and arenas. The default is four times the number of CPUs, or one if there is a single CPU. math is mathing πŸ”’
  • opt.oversize_threshold (size_t) r- β€” the threshold in bytes at which requests are considered oversize. Allocation requests with greater sizes are fulfilled from a dedicated arena (automatically managed, however not within narenas), in order to reduce fragmentation by not mixing huge allocations with small ones. In addition, the decay API guarantees on the extents greater than the specified threshold may be overridden. Note that requests with an arena index specified via MALLOCX_ARENA, or threads associated with explicit arenas, will not be considered. The default threshold is 8MiB. Values not within large size classes disable this feature. πŸ“
  • opt.percpu_arena (const char *) r- β€” per CPU arena mode. Use the β€œpercpu” setting to enable this feature, which uses the number of CPUs to determine the number of arenas, and binds threads to arenas dynamically based on the CPU the thread runs on currently. The β€œphycpu” setting uses one arena per physical CPU, which means the two hyper threads on the same CPU share one arena. Note that no runtime checking regarding the availability of hyper threading is done at the moment. When set to β€œdisabled”, narenas and the thread-to-arena association will not be impacted by this option. The default is β€œdisabled”. πŸ–₯️
  • opt.background_thread (bool) r- β€” internal background worker threads, enabled/disabled. Because of potential circular dependencies, enabling background thread using this option may cause crash or deadlock during initialization ⚠️ β€” so for a reliable way to use this feature, see background_thread for the dynamic control options and details. This option is disabled by default.
  • opt.max_background_threads (size_t) r- β€” the maximum number of background threads that will be created if background_thread is set. Defaults to the number of cpus.
  • opt.dirty_decay_ms (ssize_t) r- β€” the approximate time in milliseconds from the creation of a set of unused dirty pages until an equivalent set of unused dirty pages is purged (i.e. converted to muzzy via e.g. madvise(...MADV_FREE) if supported by the operating system, or converted to clean otherwise) and/or reused. Dirty pages are defined as previously having been potentially written to by the application, and therefore consuming physical memory, yet having no current use. The pages are incrementally purged according to a sigmoidal decay curve that starts and ends with zero purge rate. A decay time of 0 causes all unused dirty pages to be purged immediately upon creation. A decay time of -1 disables purging. The default decay time is 10 seconds. See arenas.dirty_decay_ms and arena.<i>.dirty_decay_ms for the related dynamic control options. See opt.muzzy_decay_ms for a description of muzzy pages. Note that when the oversize_threshold feature is enabled, the arenas reserved for oversize requests may have their own default decay settings. ⏳
  • opt.muzzy_decay_ms (ssize_t) r- β€” the approximate time in milliseconds from the creation of a set of unused muzzy pages until an equivalent set of unused muzzy pages is purged (i.e. converted to clean) and/or reused. Muzzy pages are defined as previously having been unused dirty pages that were subsequently purged in a manner that left them subject to the reclamation whims of the operating system (e.g. madvise(...MADV_FREE)), and therefore in an indeterminate state. The pages are incrementally purged according to a sigmoidal decay curve that starts and ends with zero purge rate. A decay time of 0 causes all unused muzzy pages to be purged immediately upon creation. A decay time of -1 disables purging. The default decay time is 10 seconds. See arenas.muzzy_decay_ms and arena.<i>.muzzy_decay_ms for the related dynamic control options. πŸŒ€
  • opt.lg_extent_max_active_fit (size_t) r- β€” when reusing dirty extents, this determines the (log base 2 of the) maximum ratio between the size of the active extent selected (to split off from) and the size of the requested allocation. This prevents the splitting of large active extents for smaller allocations, which can reduce fragmentation over the long run (especially for non-active extents). A lower value may reduce fragmentation, at the cost of extra active extents. The default value is 6, which gives a maximum ratio of 64 (2^6). βœ‚οΈ
  • opt.stats_print (bool) r- β€” on/off switch for printing stats when the program exits. When it's on, malloc_stats_print() gets called at program exit through an atexit(3) function. You can pair it with opt.stats_print_opts to pick what the output looks like. 🫢
⚠️ okay but hear me out: if --enable-stats was specified during configuration, this has the potential to cause deadlock for a multi-threaded process that exits while one or more threads are executing in the memory allocation functions. And on top of that, atexit() may allocate memory during application initialization and then deadlock internally when jemalloc in turn calls atexit(), so this option is not universally usable (though the application can register its own atexit() function with equivalent functionality). So like… use it with care, fr. It's primarily intended as a performance tuning aid during application development. This option is disabled by default.
  • opt.stats_print_opts (const char *) r- β€” the options (the opts string) you pass to malloc_stats_print() at exit (which is enabled through opt.stats_print). Check the available options over in malloc_stats_print(). ✨ Has no effect unless opt.stats_print is enabled. The default is "".
  • opt.stats_interval (int64_t) r- β€” average interval between statistics outputs, measured in bytes of allocation activity. Heads up: the actual interval may be sporadic because decentralized event counters are used to avoid synchronization bottlenecks. The output may be triggered on any thread, which then calls malloc_stats_print(). You can combine it with opt.stats_interval_opts to specify output options. By default, interval-triggered stats output is disabled (encoded as -1). πŸ’…
  • opt.stats_interval_opts (const char *) r- β€” the options (the opts string) you pass to malloc_stats_print() for interval based statistics printing (enabled through opt.stats_interval). See available options in malloc_stats_print(). Has no effect unless opt.stats_interval is enabled. The default is "".
  • opt.junk (const char *) r- [--enable-fill] β€” junk filling, bestie. πŸ“ If it's set to "alloc", each byte of uninitialized allocated memory will be initialized to 0xa5. If it's set to "free", all deallocated memory will be initialized to 0x5a. If it's set to "true", both allocated and deallocated memory will be initialized, and if it's set to "false", junk filling is disabled entirely. This is intended for debugging and will impact performance negatively. This option is "false" by default unless --enable-debug is specified during configuration, in which case it is "true" by default.
  • opt.zero (bool) r- [--enable-fill] β€” zero filling on/off. When it's on, each byte of uninitialized allocated memory gets initialized to 0. Note that this initialization only happens once for each byte, so realloc() and rallocx() calls do not zero memory that was previously allocated. This is intended for debugging and will impact performance negatively. This option is disabled by default.
  • opt.utrace (bool) r- [--enable-utrace] β€” allocation tracing based on utrace(2), on/off. This option is disabled by default.
  • opt.xmalloc (bool) r- [--enable-xmalloc] β€” abort-on-out-of-memory, on/off. If it's enabled, rather than returning failure for any allocation function, it displays a diagnostic message on STDERR_FILENO and causes the program to drop core (using abort(3)). 😭 If an application is designed to depend on this behavior, set the option at compile time by including the following in the source code:
malloc_conf = "xmalloc:true";

This option is disabled by default.

  • opt.tcache (bool) r- β€” thread-specific caching (tcache), on/off. When there are multiple threads, each thread uses a tcache for objects up to a certain size. Thread-specific caching allows many allocations to be satisfied without performing any thread synchronization, at the cost of increased memory use. 🧚 See the opt.tcache_max option for related tuning information. This option is enabled by default.
  • opt.tcache_max (size_t) r- β€” the maximum size class to cache in the thread-specific cache (tcache). At a minimum, the first size class is cached; and at a maximum, size classes up to 8 MiB can be cached. The default maximum is 32 KiB (2^15). As a convenience, this may also be set by specifying lg_tcache_max, which will be taken to be the base-2 logarithm of the setting of tcache_max. πŸ‘‘
  • opt.thp (const char *) r- β€” transparent hugepage (THP) mode. Settings "always", "never" and "default" are available if THP is supported by the operating system. The "always" setting enables transparent hugepage for all user memory mappings with MADV_HUGEPAGE; "never" ensures no transparent hugepage with MADV_NOHUGEPAGE; the default setting "default" makes no changes. Note that: this option does not affect THP for jemalloc internal metadata (see opt.metadata_thp); in addition, for arenas with customized extent_hooks, this option is bypassed as it is implemented as part of the default extent hooks. 🌈
  • opt.prof (bool) r- [--enable-prof] β€” memory profiling, on/off. If it's enabled, profile memory allocation activity. See the opt.prof_active option for on-the-fly activation/deactivation. See the opt.lg_prof_sample option for probabilistic sampling control. See the opt.prof_accum option for control of cumulative sample reporting. See the opt.lg_prof_interval option for information on interval-triggered profile dumping, the opt.prof_gdump option for information on high-water-triggered profile dumping, and the opt.prof_final option for final profile dumping. Profile output is compatible with the jeprof command, which is based on the pprof that is developed as part of the gperftools package[3]. See HEAP PROFILE FORMAT for heap profile format documentation. ✨
  • opt.prof_prefix (const char *) r- [--enable-prof] β€” filename prefix for profile dumps. If the prefix is set to the empty string, no automatic dumps will occur; this is primarily useful for disabling the automatic final heap dump (which also disables leak reporting, if enabled). The default prefix is jeprof. This prefix value can be overridden by prof.prefix. πŸ’–
  • opt.prof_active (bool) r- [--enable-prof] β€” profiling activated/deactivated. This is a secondary control mechanism that makes it possible to start the application with profiling enabled (see the opt.prof option) but inactive, then toggle profiling at any time during program execution with the prof.active mallctl. This option is enabled by default.
  • opt.prof_thread_active_init (bool) r- [--enable-prof] β€” the initial setting for thread.prof.active in newly created threads. The initial setting for newly created threads can also be changed during execution via the prof.thread_active_init mallctl. This option is enabled by default.
  • opt.lg_prof_sample (size_t) r- [--enable-prof] β€” average interval (log base 2) between allocation samples, as measured in bytes of allocation activity. Increasing the sampling interval decreases profile fidelity, but also decreases the computational overhead. The default sample interval is 512 KiB (2^19 B). πŸ¦‹
  • opt.prof_accum (bool) r- [--enable-prof] β€” reporting of cumulative object/byte counts in profile dumps, on/off. If this option is enabled, every unique backtrace must be stored for the duration of execution. Depending on the application, this can impose a large memory overhead, and the cumulative counts are not always of interest. This option is disabled by default.
  • opt.lg_prof_interval (ssize_t) r- [--enable-prof] β€” average interval (log base 2) between memory profile dumps, as measured in bytes of allocation activity. The actual interval between dumps may be sporadic because decentralized allocation counters are used to avoid synchronization bottlenecks. Profiles are dumped to files named according to the pattern <prefix>.<pid>.<seq>.i<iseq>.heap, where <prefix> is controlled by the opt.prof_prefix and prof.prefix options. By default, interval-triggered profile dumping is disabled (encoded as -1). 🌸
  • opt.prof_gdump (bool) r- [--enable-prof] β€” sets the initial state of prof.gdump, which when enabled triggers a memory profile dump every time the total virtual memory exceeds the previous maximum. This option is disabled by default.
  • opt.prof_final (bool) r- [--enable-prof] β€” use an atexit(3) function to dump final memory usage to a file named according to the pattern <prefix>.<pid>.<seq>.f.heap, where <prefix> is controlled by the opt.prof_prefix and prof.prefix options. Note that atexit() may allocate memory during application initialization and then deadlock internally when jemalloc in turn calls atexit(), so this option is not universally usable (though the application can register its own atexit() function with equivalent functionality). 😭 This option is disabled by default.
  • opt.prof_leak (bool) r- [--enable-prof] β€” leak reporting, on/off. If it's enabled, use an atexit(3) function to report memory leaks detected by allocation sampling. See the opt.prof option for information on analyzing heap profile output. Works only when combined with opt.prof_final, otherwise it does nothing. This option is disabled by default.
  • opt.prof_leak_error (bool) r- [--enable-prof] β€” similar to opt.prof_leak, but makes the process exit with error code 1 if a memory leak is detected. πŸ’€ This option supersedes opt.prof_leak, meaning that if both are specified, this option takes precedence. When enabled, it also enables opt.prof_leak. Works only when combined with opt.prof_final, otherwise it does nothing. This option is disabled by default.
  • opt.zero_realloc (const char *) r- β€” determines the behavior of realloc() when passed a value of zero for the new size. "alloc" treats this as an allocation of size zero (and returns a non-null result except in case of resource exhaustion). "free" treats this as a deallocation of the pointer, and returns NULL without setting errno. "abort" aborts the process if zero is passed. The default is "free" on Linux and Windows, and "alloc" elsewhere. πŸ™ˆ

Okay bestie, so real talk: there's a huge divergence in how different implementations handle this case. A lot of them just do the "free" thing. And that can open up security vulnerabilities, because a NULL return value means failure, and the passed-in pointer stays valid (per POSIX and C11). The "alloc" option is safe, but it can cause leaks in programs that expect the common behavior. So if your program is meant to be portable and leak-free, you literally cannot assume either behavior, and you must therefore never call realloc with a size of 0. The "abort" option lets you test this behavior. πŸ’…


thread.arena (unsigned) rw

Get or set the arena that the calling thread is vibing with. If that arena wasn't initialized yet (peep the arena.i.initialized mallctl), it'll get auto-initialized as a side effect of calling this interface. No extra steps, bestie. ✨

thread.allocated (uint64_t) r- [--enable-stats]

Get the total number of bytes ever allocated by the calling thread. Heads up: this counter can wrap around, so it's on the application to interpret it correctly when that happens. πŸ‘€

thread.allocatedp (uint64_t *) r- [--enable-stats]

Get a pointer to the value that thread.allocated returns. This is a whole hack to avoid the overhead of calling mallctl*() over and over. But bestie, do NOT modify the underlying counter. That's a no-no. 🚫

thread.deallocated (uint64_t) r- [--enable-stats]

Get the total number of bytes ever deallocated by the calling thread. Same wrap-around warning as above: it can wrap, so interpret carefully. πŸ’€

thread.deallocatedp (uint64_t *) r- [--enable-stats]

Get a pointer to what thread.deallocated returns. Again, saves you from repeated mallctl*() calls. And again, don't touch the underlying counter. πŸ™ˆ

thread.peak.read (uint64_t) r- [--enable-stats]

Get an approximation of the peak net bytes: that's max(allocated βˆ’ deallocated) for the calling thread since the last thread.peak.reset, or since the thread was born if it never called thread.peak.reset. No promises on how good the approximation is, but jemalloc is currently trying to keep it accurate within one hundred kilobytes. 🎯

thread.peak.reset (void) -- [--enable-stats]

Resets the net-bytes-allocated counter for the calling thread to zero. This changes what thread.peak.read says next, but it does NOT change thread.allocated or thread.deallocated. Those stay exactly as they were. πŸ”„

thread.tcache.enabled (bool) rw

Turn the calling thread's tcache on or off. If you disable it, the tcache gets implicitly flushed as a side effect (peep thread.tcache.flush for the deets). 🧹

thread.tcache.flush (void) --

Flush the calling thread's tcache. This releases all cached objects and internal data structures tied to that tcache. Usually you don't need to call this, because automatic periodic incremental GC is a thing, and the thread cache gets discarded when the thread exits. BUT: GC is triggered by allocation activity, so if a thread just stops allocating/deallocating, it can hold onto its cache forever. In that case, manual flushing is lowkey useful. 🧼

thread.prof.name (const char *) r- or -w [--enable-prof]

Get or set the cute descriptive name for the calling thread in memory profile dumps. An internal copy of the name is made, so you don't have to keep your input string alive after this call. But if you're reading, copy the output string if you need it non-ephemerally, because implementation details can cause asynchronous string deallocation. Also, each call can only read OR write β€” simultaneous read/write is not supported because of string lifetime limits. And the name must be nil-terminated and only contain characters from the sets recognized by isgraph(3) and isblank(3). ✍️

thread.prof.active (bool) rw [--enable-prof]

Control whether sampling is currently active for the calling thread. This is an extra activation switch on top of prof.active; both have to be active for the calling thread to actually sample. This flag is enabled by default. 🎚️

thread.idle (void) --

Hint to jemalloc that the calling thread is about to be idle for a nontrivial amount of time (like seconds, not milliseconds), and that doing some cleanup might be a good idea. No guarantees on exactly what cleanup happens; currently it flushes the caller's tcache and may (based on some heuristic) purge its associated arena.

This is NOT a general-purpose background activity mechanism, and threads should not wake up multiple times just to call it. Instead, if a thread is waiting for a task, do a timed wait first, call thread.idle if no task shows up in the timeout, then do an untimed wait. For an actual background activity mechanism, see background_thread. 😴

tcache.create (unsigned) r-

Create an explicit tcache and get back an identifier you can pass to the MALLOCX_TCACHE(tc) macro to use that specific cache instead of the automatically managed one. Each explicit cache can only be used by one thread at a time β€” you, the application, have to make sure that constraint holds.

Also, if the space you gave for storing the tcache identifier isn't exactly sizeof(unsigned), then no tcache gets created, no data is written to oldp, and *oldlenp is set to 0. πŸŽ€

tcache.flush (unsigned) -w

Flush the specified tcache. Same considerations as thread.tcache.flush, except this tcache will NEVER be automatically discarded. So you're in charge, bestie. 🧽

tcache.destroy (unsigned) -w

Flush the specified tcache and free up its identifier so it can be used in a future tcache creation. Recycle, slay. ♻️

arena.<i>.initialized (bool) r-

Check whether arena <i>'s statistics are initialized β€” meaning the arena was initialized before the current epoch. You can also nominally use this to query whether the merged stats for MALLCTL_ARENAS_ALL are initialized (spoiler: always true). βœ…

arena.<i>.decay (void) --

Trigger decay-based purging of unused dirty/muzzy pages for arena <i>, or for all arenas if <i> is MALLCTL_ARENAS_ALL. How much gets purged depends on the current time; check opt.dirty_decay_ms and opt.muzy_decay_ms for the deets. ⏳

arena.<i>.purge (void) --

Purge ALL unused dirty pages for arena <i>, or for all arenas if <i> is MALLCTL_ARENAS_ALL. Clean sweep. 🧹

arena.<i>.reset (void) --

Discard every extant allocation in the arena. This only works with arenas explicitly created via arenas.create. After this, none of the arena's discarded/cached allocations may be accessed β€” like, at all. And as part of that requirement, all thread caches that were used to allocate/deallocate with this arena must be flushed beforehand. Don't skip that step. ⚠️

arena.<i>.destroy (void) --

Destroy the arena. This discards all extant allocations the same way arena.<i>.reset does (same constraints, same side effects), merges the arena stats into the ones at arena index MALLCTL_ARENAS_DESTROYED, then completely drops all metadata for the arena. Future arenas.create calls might recycle that arena index. But heads up: destruction will FAIL if any threads are still associated with the arena because of thread.arena calls. So untangle that first. πŸ’€

arena.<i>.dss (const char *) rw

Set the precedence of dss allocation relative to mmap allocation for arena <i>, or for all arenas if <i> is MALLCTL_ARENAS_ALL. Check opt.dss for the supported settings. πŸ—ΊοΈ

arena.<i>.dirty_decay_ms (ssize_t) rw

Current per-arena approximate time in milliseconds from when a set of unused dirty pages is created until an equivalent set gets purged and/or reused. Every time you set this, all currently unused dirty pages are considered fully decayed β€” which means immediate purging of all unused dirty pages, unless the decay time is -1 (aka purging disabled). See opt.dirty_decay_ms for more. ⏱️

arena.<i>.muzzy_decay_ms (ssize_t) rw

Same as above but for muzzy pages. Current per-arena approximate time in milliseconds from creation of a set of unused muzzy pages until an equivalent set gets purged and/or reused. Setting it makes all currently unused muzzy pages fully decayed, so immediate purging happens unless decay time is -1 (purging disabled). See opt.muzzy_decay_ms for more. ⏳

arena.<i>.retain_grow_limit (size_t) rw

Maximum size to grow the retained region. Only relevant when opt.retain is enabled. This controls the max increment for expanding virtual memory, or allocation through arena.<i>extent_hooks. Especially useful if your custom extent hooks reserve physical memory (like 1G huge pages) and you want to control the allocation hook's input size. The default is no limit. πŸ“ˆ

arena.<i>.extent_hooks (extent_hooks_t *) rw

Get or set the extent management hook functions for arena <i>. Those functions have to be able to operate on all extant extents associated with arena <i>, usually by passing unknown extents to the replaced functions. In practice, you can control allocation for arenas explicitly created via arenas.create so that all extents come from an application-supplied extent allocator (just specify your custom extent hook functions when creating the arena). BUT: for automatically created arenas, the API guarantees may be relaxed β€” hooks set there may be called in a "best effort" fashion, and there may be extents created before the application even gets a chance to take over extent allocation. So, y'know, manage your expectations. πŸͺ

typedef extent_hooks_s extent_hooks_t;
struct extent_hooks_s {
     extent_alloc_t      *alloc;
     extent_dalloc_t          *dalloc;
     extent_destroy_t    *destroy;
     extent_commit_t          *commit;
     extent_decommit_t   *decommit;
     extent_purge_t      *purge_lazy;
     extent_purge_t      *purge_forced;
     extent_split_t      *split;
     extent_merge_t      *merge;
};

okay so the extent_hooks_t struct is literally just a lil bundle of function pointers, and we're gonna walk through every single one of them below, no skipping bestie πŸ’– jemalloc uses these functions to manage extent lifetime, which kicks off with allocation of mapped committed memory, and in the simplest case that's followed by deallocation. but β€” and this is the tea β˜• β€” there are performance and platform reasons to hold onto extents for later reuse instead of yeeting them immediately. Cleanup attempts cascade from deallocation ➑️ decommit ➑️ forced purging ➑️ lazy purging, which gives the extent management functions a chance to reject the most permanent cleanup operations in favor of the less permanent (and usually less costly) ones. so polite of them honestly 🫢

All operations except allocation can be universally opted out of by setting the hook pointers to NULL, or selectively opted out of by returning failure. and bestie, listen β€” once the extent hook is set, the structure is accessed directly by the associated arenas, so it must remain valid for the entire lifetime of the arenas. like, it lives there forever now. no takebacks πŸ’€


typedef void *(extent_alloc_t)(extent_hooks_t *extent_hooks,
                               void *new_addr, size_t size,
                               size_t alignment, bool *zero,
                               bool *commit, unsigned arena_ind);

An extent allocation function conforms to the extent_alloc_t type and upon success returns a pointer to size bytes of mapped memory on behalf of arena arena_ind such that the extent's base address is a multiple of alignment, as well as setting *zero to indicate whether the extent is zeroed and *commit to indicate whether the extent is committed. Upon error the function returns NULL and leaves *zero and *commit unmodified. ✨

The size parameter is always a multiple of the page size. The alignment parameter is always a power of two at least as large as the page size. Zeroing is mandatory if *zero is true upon function entry. Committing is mandatory if *commit is true upon function entry. If new_addr is not NULL, the returned pointer must be new_addr on success or NULL on error. 🎯

Committed memory may be committed in absolute terms as on a system that does not overcommit, or in implicit terms as on a system that overcommits and satisfies physical memory needs on demand via soft page faults. And heads up: replacing the default extent allocation function makes the arena's arena.<i>.dss setting irrelevant πŸ’…


typedef bool (extent_dalloc_t)(extent_hooks_t *extent_hooks,
                               void *addr, size_t size,
                               bool committed, unsigned arena_ind);

An extent deallocation function conforms to the extent_dalloc_t type and deallocates an extent at given addr and size with committed/decommited memory as indicated, on behalf of arena arena_ind, returning false upon success. πŸŽ€

If the function returns true, that means opt-out from deallocation β€” the virtual memory mapping associated with the extent remains mapped, in the same commit state, and available for future use, in which case it will be automatically retained for later reuse. so it's not gone, it's just ✨on ice✨


typedef void (extent_destroy_t)(extent_hooks_t *extent_hooks,
                                void *addr, size_t size,
                                bool committed, unsigned arena_ind);

An extent destruction function conforms to the extent_destroy_t type and unconditionally destroys an extent at given addr and size with committed/decommited memory as indicated, on behalf of arena arena_ind. no opt-out here, it's giving final boss energy 😀 This function may be called to destroy retained extents during arena destruction (see arena.<i>.destroy).


typedef bool (extent_commit_t)(extent_hooks_t *extent_hooks,
                               void *addr, size_t size,
                               size_t offset, size_t length,
                               unsigned arena_ind);

An extent commit function conforms to the extent_commit_t type and commits zeroed physical memory to back pages within an extent at given addr and size at offset bytes, extending for length on behalf of arena arena_ind, returning false upon success. πŸ¦‹

Committed memory may be committed in absolute terms as on a system that does not overcommit, or in implicit terms as on a system that overcommits and satisfies physical memory needs on demand via soft page faults. If the function returns true, this indicates insufficient physical memory to satisfy the request β€” like, oops, out of RAM bestie 😭


typedef bool (extent_decommit_t)(extent_hooks_t *extent_hooks,
                                 void *addr, size_t size,
                                 size_t offset, size_t length,
                                 unsigned arena_ind);

An extent decommit function conforms to the extent_decommit_t type and decommits any physical memory that is backing pages within an extent at given addr and size at offset bytes, extending for length on behalf of arena arena_ind, returning false upon success, in which case the pages will be committed via the extent commit function before being reused. πŸ”

If the function returns true, this indicates opt-out from decommit; the memory remains committed and available for future use, in which case it will be automatically retained for later reuse. again: not deleted, just vibing in the background 🌸


typedef bool (extent_purge_t)(extent_hooks_t *extent_hooks,
                              void *addr, size_t size,
                              size_t offset, size_t length,
                              unsigned arena_ind);

An extent purge function conforms to the extent_purge_t type and discards physical pages within the virtual memory mapping associated with an extent at given addr and size at offset bytes, extending for length on behalf of arena arena_ind. 🧹

A lazy extent purge function (e.g. implemented via madvise(...MADV_FREE)) can delay purging indefinitely and leave the pages within the purged virtual memory range in an indeterminite state, whereas a forced extent purge function immediately purges, and the pages within the virtual memory range will be zero-filled the next time they are accessed. If the function returns true, this indicates failure to purge πŸ™ˆ


typedef bool (extent_split_t)(extent_hooks_t *extent_hooks,
                              void *addr, size_t size,
                              size_t size_a, size_t size_b,
                              bool committed, unsigned arena_ind);

An extent split function conforms to the extent_split_t type and optionally splits an extent at given addr and size into two adjacent extents, the first of size_a bytes, and the second of size_b bytes, operating on committed/decommitted memory as indicated, on behalf of arena arena_ind, returning false upon success. βœ‚οΈ

If the function returns true, this indicates that the extent remains unsplit and therefore should continue to be operated on as a whole. it said "no thank you, I'm one entity" πŸ’…


typedef bool (extent_merge_t)(extent_hooks_t *extent_hooks,
                              void *addr_a, size_t size_a,
                              void *addr_b, size_t size_b,
                              bool committed, unsigned arena_ind);

An extent merge function conforms to the extent_merge_t type and optionally merges adjacent extents, at given addr_a and size_a with given addr_b and size_b into one contiguous extent, operating on committed/decommitted memory as indicated, on behalf of arena arena_ind, returning false upon success. 🫢

If the function returns true, this indicates that the extents remain distinct mappings and therefore should continue to be operated on independently. not every friendship is meant to merge, and that's okay πŸ’œ


  • arenas.narenas (unsigned) r- β€” Current limit on number of arenas. πŸ”’
  • arenas.dirty_decay_ms (ssize_t) rw β€” Current default per-arena approximate time in milliseconds from the creation of a set of unused dirty pages until an equivalent set of unused dirty pages is purged and/or reused, used to initialize arena.<i>.dirty_decay_ms during arena creation. See opt.dirty_decay_ms for additional information. ⏳
  • arenas.muzzy_decay_ms (ssize_t) rw β€” Current default per-arena approximate time in milliseconds from the creation of a set of unused muzzy pages until an equivalent set of unused muzzy pages is purged and/or reused, used to initialize arena.<i>.muzzy_decay_ms during arena creation. See opt.muzzy_decay_ms for additional information. 🌫️
  • arenas.quantum (size_t) r- β€” Quantum size. ✨
  • arenas.page (size_t) r- β€” Page size. πŸ“„
  • arenas.tcache_max (size_t) r- β€” Maximum thread-cached size class. 🧡
  • arenas.nbins (unsigned) r- β€” Number of bin size classes. πŸ—‘οΈ
  • arenas.nhbins (unsigned) r- β€” Total number of thread cache bin size classes. πŸ—‘οΈπŸ—‘οΈ
  • arenas.bin.<i>.size (size_t) r- β€” Maximum size supported by size class. πŸ“
  • arenas.bin.<i>.nregs (uint32_t) r- β€” Number of regions per slab. 🧩
  • arenas.bin.<i>.slab_size (size_t) r- β€” Number of bytes per slab. πŸ“
  • arenas.nlextents (unsigned) r- β€” Total number of large size classes. 🐘
  • arenas.lextent.<i>.size (size_t) r- β€” Maximum size supported by this large size class. πŸ“¦
  • arenas.create (unsigned, extent_hooks_t *) rw β€” Explicitly create a new arena outside the range of automatically managed arenas, with optionally specified extent hooks, and return the new arena index. πŸŽ‰
⚠️ careful bestie: If the amount of space supplied for storing the arena index does not equal sizeof(unsigned), no arena will be created, no data will be written to the space pointed by oldp, and *oldlenp will be set to 0. so like, measure twice, allocate once πŸ’€
  • arenas.lookup (unsigned, void*) rw β€” Index of the arena to which an allocation belongs to. πŸ”
  • prof.thread_active_init (bool) rw [--enable-prof] β€” Control the initial setting for thread.prof.active in newly created threads. See the opt.prof_thread_active_init option for additional information. 🧬
  • prof.active (bool) rw [--enable-prof] β€” Control whether sampling is currently active. See the opt.prof_active option for additional information, as well as the interrelated thread.prof.active mallctl. πŸ“Š
  • prof.dump (const char *) -w [--enable-prof] β€” dumps a memory profile to the file you name, bestie πŸ’Ύ and if you pass NULL instead of a filename it'll pick one for you using the pattern <prefix>.<pid>.<seq>.m<mseq>.heap, where <prefix> is whatever you set with the opt.prof_prefix and prof.prefix options. slay.
  • prof.prefix (const char *) -w [--enable-prof] β€” sets the filename prefix for profile dumps. the default lives in opt.prof_prefix if you're curious. super handy for telling your dumps apart, like when you've got forked processes running around doing their own thing πŸ’…
  • prof.gdump (bool) rw [--enable-prof] β€” when you turn this on, it triggers a memory profile dump every single time the total virtual memory exceeds the previous maximum πŸ‘‘ profiles get dumped into files named like <prefix>.<pid>.<seq>.u<useq>.heap, where <prefix> comes from the opt.prof_prefix and prof.prefix options. periodt.
  • prof.reset (size_t) -w [--enable-prof] β€” resets all memory profile statistics, and optionally updates the sample rate too (peep opt.lg_prof_sample and prof.lg_sample for that part) πŸ”„
  • prof.lg_sample (size_t) r- [--enable-prof] β€” reads out the current sample rate. see opt.lg_prof_sample for the deets ✨
  • prof.interval (uint64_t) r- [--enable-prof] β€” the average number of bytes allocated between interval-based profile dumps. opt.lg_prof_interval has extra info if you want it 🫢
  • stats.allocated (size_t) r- [--enable-stats] β€” total bytes allocated by the application, fr fr.
  • stats.active (size_t) r- [--enable-stats] β€” total bytes sitting in active pages allocated by the application. it's a multiple of the page size, and it's greater than or equal to stats.allocated. note that it does not include stats.arenas.<i>.pdirty, stats.arenas.<i>.pmuzzy, or pages that are entirely devoted to allocator metadata πŸ™ˆ
  • stats.metadata (size_t) r- [--enable-stats] β€” total bytes dedicated to metadata, which is made up of base allocations used for bootstrap-sensitive allocator metadata structures (see stats.arenas.<i>.base) plus internal allocations (see stats.arenas.<i>.internal). transparent huge page usage (the kind you enable with opt.metadata_thp) is not counted here πŸŽ€
  • stats.metadata_thp (size_t) r- [--enable-stats] β€” how many transparent huge pages (THP) are being used for metadata. see stats.metadata and opt.metadata_thp for the details πŸ’œ
  • stats.resident (size_t) r- [--enable-stats] β€” the maximum number of bytes in physically resident data pages mapped by the allocator, comprising all pages dedicated to allocator metadata, pages backing active allocations, and unused dirty pages. it's a max and not an exact number because pages might not actually be physically resident if they're demand-zeroed virtual memory that nobody's touched yet. multiple of the page size, and bigger than stats.active πŸ’€
  • stats.mapped (size_t) r- [--enable-stats] β€” total bytes in active extents mapped by the allocator. larger than stats.active. it does not include inactive extents β€” not even ones holding unused dirty pages β€” which means there's no strict ordering between this and stats.resident 😭
  • stats.retained (size_t) r- [--enable-stats] β€” total bytes in virtual memory mappings that got retained instead of being handed back to the operating system via e.g. munmap(2) or similar. retained virtual memory is typically untouched, decommitted, or purged, so it has no strongly associated physical memory (see extent hooks for details). retained memory is excluded from mapped memory statistics, like stats.mapped 🌈
  • stats.zero_reallocs (size_t) r- [--enable-stats] β€” how many times realloc() got called with a non-NULL pointer argument and a 0 size argument. > heads up bestie, this is a fundamentally unsafe pattern in portable programs; opt.zero_realloc has the details. not cute, don't do it πŸ’…
  • stats.background_thread.num_threads (size_t) r- [--enable-stats] β€” how many background threads are running right now 🧡
  • stats.background_thread.num_runs (uint64_t) r- [--enable-stats] β€” total number of runs from all the background threads combined.
  • stats.background_thread.run_interval (uint64_t) r- [--enable-stats] β€” average run interval of background threads, in nanoseconds ⏱️
  • stats.mutexes.ctl.{counter} (counter specific type) r- [--enable-stats] β€” stats on the ctl mutex (global scope; mallctl related). {counter} is one of these counters below, bestie:
    • num_ops (uint64_t): total number of lock acquisition operations on this mutex.
    • num_spin_acq (uint64_t): number of times the mutex was spin-acquired. when the mutex is currently locked and can't be acquired immediately, a short period of spin-retry inside jemalloc happens. acquiring through spin generally means the contention was lightweight and didn't cause context switches ✨
    • num_wait (uint64_t): number of times the mutex was wait-acquired, meaning the contention wasn't solved by spin-retry and a blocking operation was likely involved to grab the mutex. this event generally implies higher cost / longer delay, and you should investigate if it happens a lot πŸ‘€
    • max_wait_time (uint64_t): maximum length of time in nanoseconds spent on a single wait-acquired lock operation. note that to avoid profiling overhead on the common path, this does not consider spin-acquired cases.
    • total_wait_time (uint64_t): cumulative time in nanoseconds spent on wait-acquired lock operations. same deal, spin-acquired cases aren't considered.
    • max_num_thds (uint32_t): maximum number of threads waiting on this mutex simultaneously. also not considering spin-acquired cases.
    • num_owner_switch (uint64_t): number of times the current mutex owner is different from the previous one. this doesn't generally imply an issue β€” it's more of an indicator of how often the protected data gets accessed by different threads πŸ’œ
  • stats.mutexes.background_thread.{counter} (counter specific type) r- [--enable-stats] β€” stats on the background_thread mutex (global scope; background_thread related). {counter} is one of the counters in mutex profiling counters.
  • stats.mutexes.prof.{counter} (counter specific type) r- [--enable-stats] β€” stats on the prof mutex (global scope; profiling related). {counter} is one of the counters in mutex profiling counters.
  • stats.mutexes.prof_thds_data.{counter} (counter specific type) r- [--enable-stats] β€” stats on the prof threads data mutex (global scope; profiling related). {counter} is one of the counters in mutex profiling counters.
  • stats.mutexes.prof_dump.{counter} (counter specific type) r- [--enable-stats] β€” stats on the prof dumping mutex (global scope; profiling related). {counter} is one of the counters in mutex profiling counters.
  • stats.mutexes.reset (void) -- [--enable-stats] β€” resets all mutex profile statistics, including global mutexes, arena mutexes and bin mutexes. fresh start, iconic 🧼

stats.arenas.<i>.* β€” the per-arena zone πŸ¦‹

  • stats.arenas.<i>.dss (const char *) r- β€” the dss (sbrk(2)) allocation precedence as related to mmap(2) allocation. see opt.dss for details πŸ’«
  • stats.arenas.<i>.dirty_decay_ms (ssize_t) r- β€” approximate time in milliseconds from the creation of a set of unused dirty pages until an equivalent set of unused dirty pages is purged and/or reused. see opt.dirty_decay_ms for details.
  • stats.arenas.<i>.muzzy_decay_ms (ssize_t) r- β€” approximate time in milliseconds from the creation of a set of unused muzzy pages until an equivalent set of unused muzzy pages is purged and/or reused. see opt.muzzy_decay_ms for details.
  • stats.arenas.<i>.nthreads (unsigned) r- β€” number of threads currently assigned to this arena 🧡
  • stats.arenas.<i>.uptime (uint64_t) r- β€” time elapsed (in nanoseconds) since the arena was created. if <i> equals 0 or MALLCTL_ARENAS_ALL, this is the uptime since malloc initialization ⏱️
  • stats.arenas.<i>.pactive (size_t) r- β€” number of pages in active extents.
  • stats.arenas.<i>.pdirty (size_t) r- β€” number of pages within unused extents that are potentially dirty, and for which madvise() or similar hasn't been called. opt.dirty_decay_ms has a description of dirty pages if you need it 🫧
  • stats.arenas.<i>.pmuzzy (size_t) r- β€” number of pages within unused extents that are muzzy. opt.muzzy_decay_ms describes muzzy pages 🌫️
  • stats.arenas.<i>.mapped (size_t) r- [--enable-stats] β€” number of mapped bytes.
  • stats.arenas.<i>.retained (size_t) r- [--enable-stats] β€” number of retained bytes. see stats.retained for the details πŸ’–
  • stats.arenas.<i>.extent_avail (size_t) r- [--enable-stats] β€” number of allocated (but unused) extent structs in this arena.
  • stats.arenas.<i>.base (size_t) r- [--enable-stats] β€” number of bytes dedicated to bootstrap-sensitive allocator metadata structures.
  • stats.arenas.<i>.internal (size_t) r- [--enable-stats] β€” number of bytes dedicated to internal allocations. internal allocations are different from application-originated allocations because they're for internal use, and because they get omitted from heap profiles πŸŽ€
  • stats.arenas.<i>.metadata_thp (size_t) r- [--enable-stats] β€” number of transparent huge pages (THP) used for metadata. see opt.metadata_thp for details.
  • stats.arenas.<i>.resident (size_t) r- [--enable-stats] β€” maximum number of bytes in physically resident data pages mapped by the arena, comprising all pages dedicated to allocator metadata, pages backing active allocations, and unused dirty pages. it's a maximum rather than precise because pages might not actually be physically resident if they correspond to demand-zeroed virtual memory that hasn't been touched yet. multiple of the page size πŸ’€
  • stats.arenas.<i>.dirty_npurge (uint64_t) r- [--enable-stats] β€” number of dirty page purge sweeps performed.
  • stats.arenas.<i>.dirty_nmadvise (uint64_t) r- [--enable-stats] β€” number of madvise() or similar calls made to purge dirty pages.
  • stats.arenas.<i>.dirty_purged (uint64_t) r- [--enable-stats] β€” number of dirty pages purged 🧽
  • stats.arenas.<i>.muzzy_npurge (uint64_t) r- [--enable-stats] β€” number of muzzy page purge sweeps performed.
  • stats.arenas.<i>.muzzy_nmadvise (uint64_t) r- [--enable-stats] β€” number of madvise() or similar calls made to purge muzzy pages.
  • stats.arenas.<i>.muzzy_purged (uint64_t) r- [--enable-stats] β€” number of muzzy pages purged 🌫️
  • stats.arenas.<i>.small.allocated (size_t) r- [--enable-stats] β€” number of bytes currently allocated by small objects πŸ“
  • stats.arenas.<i>.small.nmalloc (uint64_t) r- [--enable-stats] β€” how many times, total, someone asked this arena's bins for a small allocation. Whether that was to top up the relevant tcache (when opt.tcache is on) or to just hand out the allocation straight up β€” doesn't matter, it counts either way. ✨
  • stats.arenas.<i>.small.ndalloc (uint64_t) r- [--enable-stats] β€” how many times, total, a small allocation got handed back to this arena's bins. Same deal: whether it was flushing the relevant tcache (if opt.tcache is enabled) or deallocating directly, it's in here. πŸŽ€
  • stats.arenas.<i>.small.nrequests (uint64_t) r- [--enable-stats] β€” total allocation requests that got satisfied across all bin size classes. every single one, bestie.
  • stats.arenas.<i>.small.nfills (uint64_t) r- [--enable-stats] β€” total tcache fills done by all the small size classes combined. πŸ’–
  • stats.arenas.<i>.small.nflushes (uint64_t) r- [--enable-stats] β€” total tcache flushes done by all the small size classes combined. πŸ¦‹
  • stats.arenas.<i>.large.allocated (size_t) r- [--enable-stats] β€” how many bytes are currently allocated by large objects. right now. live. πŸ’…
  • stats.arenas.<i>.large.nmalloc (uint64_t) r- [--enable-stats] β€” total times a large extent got allocated out of the arena. Was it to fill the relevant tcache (if opt.tcache is on and the size class falls within the range being cached)? or to directly satisfy an allocation? Either way it's counted. 🫢
  • stats.arenas.<i>.large.ndalloc (uint64_t) r- [--enable-stats] β€” total times a large extent came back to the arena. Again: flushing the relevant tcache (if opt.tcache is enabled and the size class is within the range being cached), or a direct deallocation β€” both land here. 🌸
  • stats.arenas.<i>.large.nrequests (uint64_t) r- [--enable-stats] β€” total allocation requests satisfied by every large size class. ✨
  • stats.arenas.<i>.large.nfills (uint64_t) r- [--enable-stats] β€” total tcache fills by all large size classes. πŸ”₯
  • stats.arenas.<i>.large.nflushes (uint64_t) r- [--enable-stats] β€” total tcache flushes by all large size classes. 🌈
  • stats.arenas.<i>.bins.<j>.nmalloc (uint64_t) r- [--enable-stats] β€” total times a bin region of that corresponding size class got allocated from the arena β€” whether to fill the relevant tcache (if opt.tcache is enabled) or to just directly satisfy an allocation request. πŸŽ€
  • stats.arenas.<i>.bins.<j>.ndalloc (uint64_t) r- [--enable-stats] β€” total times a bin region of that size class got returned to the arena β€” whether flushing the relevant tcache (if opt.tcache is enabled) or directly deallocating. 😌
  • stats.arenas.<i>.bins.<j>.nrequests (uint64_t) r- [--enable-stats] β€” total allocation requests satisfied by bin regions of that corresponding size class. πŸ‘‘
  • stats.arenas.<i>.bins.<j>.curregs (size_t) r- [--enable-stats] β€” how many regions exist for this size class right now. πŸ’œ
  • stats.arenas.<i>.bins.<j>.nfills (uint64_t) r- β€” total tcache fills. (no [--enable-stats] tag on this one, she's always on ✨)
  • stats.arenas.<i>.bins.<j>.nflushes (uint64_t) r- β€” total tcache flushes. 🌊
  • stats.arenas.<i>.bins.<j>.nslabs (uint64_t) r- [--enable-stats] β€” total slabs created, cumulative. 🧚
  • stats.arenas.<i>.bins.<j>.nreslabs (uint64_t) r- [--enable-stats] β€” total times the current slab you're allocating from switched to a different one. πŸ’€
  • stats.arenas.<i>.bins.<j>.curslabs (size_t) r- [--enable-stats] β€” current number of slabs. as in, right this second.
  • stats.arenas.<i>.bins.<j>.nonfull_slabs (size_t) r- [--enable-stats] β€” current number of nonfull slabs. πŸ‘€
  • stats.arenas.<i>.bins.<j>.mutex.{counter} (counter specific type) r- [--enable-stats] β€” stats on the arena.<i>.bins.<j> mutex (arena bin scope; bin operation related). {counter} is one of the counters in mutex profiling counters. πŸŽ€
  • stats.arenas.<i>.extents.<j>.n{extent_type} (size_t) r- [--enable-stats] β€” how many extents of the given type live in this arena, in the bucket matching page size index <j>. The extent type is one of dirty, muzzy, or retained. πŸ’–
  • stats.arenas.<i>.extents.<j>.{extent_type}_bytes (size_t) r- [--enable-stats] β€” the sum of bytes managed by extents of the given type in this arena, in the bucket matching page size index <j>. Extent type is one of dirty, muzzy, or retained. πŸ¦‹
  • stats.arenas.<i>.lextents.<j>.nmalloc (uint64_t) r- [--enable-stats] β€” total times a large extent of that size class got allocated from the arena β€” whether to fill the relevant tcache (if opt.tcache is enabled and the size class is within the range being cached), or to directly satisfy an allocation request otherwise. ✨
  • stats.arenas.<i>.lextents.<j>.ndalloc (uint64_t) r- [--enable-stats] β€” total times a large extent of that size class got returned to the arena β€” whether flushing the relevant tcache (if opt.tcache is enabled and the size class is within the range being cached), or directly deallocating otherwise. 🌸
  • stats.arenas.<i>.lextents.<j>.nrequests (uint64_t) r- [--enable-stats] β€” total allocation requests satisfied by large extents of that corresponding size class. πŸ’…
  • stats.arenas.<i>.lextents.<j>.curlextents (size_t) r- [--enable-stats] β€” current number of large allocations for this size class. 🌈
  • stats.arenas.<i>.mutexes.large.{counter} (counter specific type) r- [--enable-stats] β€” stats on the arena.<i>.large mutex (arena scope; large allocation related). {counter} is one of the counters in mutex profiling counters. πŸ‘‘
  • stats.arenas.<i>.mutexes.extent_avail.{counter} (counter specific type) r- [--enable-stats] β€” stats on the arena.<i>.extent_avail mutex (arena scope; extent avail related). {counter} is one of the counters in mutex profiling counters. 🫢
  • stats.arenas.<i>.mutexes.extents_dirty.{counter} (counter specific type) r- [--enable-stats] β€” stats on the arena.<i>.extents_dirty mutex (arena scope; dirty extents related). {counter} is one of the counters in mutex profiling counters. 🧚
  • stats.arenas.<i>.mutexes.extents_muzzy.{counter} (counter specific type) r- [--enable-stats] β€” stats on the arena.<i>.extents_muzzy mutex (arena scope; muzzy extents related). {counter} is one of the counters in mutex profiling counters. πŸ’œ
  • stats.arenas.<i>.mutexes.extents_retained.{counter} (counter specific type) r- [--enable-stats] β€” stats on the arena.<i>.extents_retained mutex (arena scope; retained extents related). {counter} is one of the counters in mutex profiling counters. πŸ”₯
  • stats.arenas.<i>.mutexes.decay_dirty.{counter} (counter specific type) r- [--enable-stats] β€” stats on the arena.<i>.decay_dirty mutex (arena scope; decay for dirty pages related). {counter} is one of the counters in mutex profiling counters. 🌸
  • stats.arenas.<i>.mutexes.decay_muzzy.{counter} (counter specific type) r- [--enable-stats] β€” stats on the arena.<i>.decay_muzzy mutex (arena scope; decay for muzzy pages related). {counter} is one of the counters in mutex profiling counters. πŸ¦‹
  • stats.arenas.<i>.mutexes.base.{counter} (counter specific type) r- [--enable-stats] β€” stats on the arena.<i>.base mutex (arena scope; base allocator related). {counter} is one of the counters in mutex profiling counters. πŸŽ€
  • stats.arenas.<i>.mutexes.tcache_list.{counter} (counter specific type) r- [--enable-stats] β€” stats on the arena.<i>.tcache_list mutex (arena scope; tcache to arena association related). This mutex is expected to be accessed less often. {counter} is one of the counters in mutex profiling counters. ✨

🌸 HEAP PROFILE FORMAT 🌸

okay so storytime: heap profiling was originally designed to be compatible with the pprof command that ships as part of the gperftools package<sup>[3]</sup>... but then per-thread heap profiling entered the chat and required a whole different heap profile format. The jeprof command is derived from pprof, with enhancements to support the heap profile format described right here. πŸ’…

In the following hypothetical heap profile, [...] indicates elision for the sake of compactness. (we're keeping it cute and short, bestie)

heap_v2/524288
  t*: 28106: 56637512 [0: 0]
  [...]
  t3: 352: 16777344 [0: 0]
  [...]
  t99: 17754: 29341640 [0: 0]
  [...]
@ 0x5f86da8 0x5f5a1dc [...] 0x29e4d4e 0xa200316 0xabb2988 [...]
  t*: 13: 6688 [0: 0]
  t3: 12: 6496 [0: 0]
  t99: 1: 192 [0: 0]
[...]

MAPPED_LIBRARIES:
[...]

The following matches the above heap profile, but most tokens are replaced with <description> to indicate descriptions of the corresponding fields. 🫢

<heap_profile_format_version>/<mean_sample_interval>
  <aggregate>: <curobjs>: <curbytes> [<cumobjs>: <cumbytes>]
  [...]
  <thread_3_aggregate>: <curobjs>: <curbytes> [<cumobjs>: <cumbytes>]
  [...]
  <thread_99_aggregate>: <curobjs>: <curbytes> [<cumobjs>: <cumbytes>]
  [...]
@ <top_frame> <frame> [...] <frame> <frame> <frame> [...]
  <backtrace_aggregate>: <curobjs>: <curbytes> [<cumobjs>: <cumbytes>]
  <backtrace_thread_3>: <curobjs>: <curbytes> [<cumobjs>: <cumbytes>]
  <backtrace_thread_99>: <curobjs>: <curbytes> [<cumobjs>: <cumbytes>]
[...]

MAPPED_LIBRARIES:
</proc/<pid>/maps>

πŸ’€ DEBUGGING MALLOC PROBLEMS πŸ’€

When you're debugging, it is a good idea to configure/build jemalloc with the --enable-debug and --enable-fill options, and recompile the program with suitable options and symbols for debugger support. When it's configured like that, jemalloc brings a whole wide variety of run-time assertions that catch application errors β€” think double-free, write-after-free, etc. She's basically a bodyguard. πŸ‘‘

Programs often accidentally depend on "uninitialized" memory actually being filled with zero bytes. (not me gaslighting myself into thinking malloc zeroes things 😭) Junk filling (see the opt.junk option) tends to expose such bugs in the form of obviously incorrect results and/or coredumps. Conversely, zero filling (see the opt.zero option) eliminates the symptoms of such bugs. Between these two options, it is usually possible to quickly detect, diagnose, and eliminate such bugs. πŸ¦‹

Heads up bestie: this implementation does not provide much detail about the problems it detects, because the performance impact for storing such information would be prohibitive. πŸ’…

πŸ’– DIAGNOSTIC MESSAGES πŸ’–

okay bestie, so if any of the memory allocation/deallocation functions detect an error or warning condition, a message gets printed to the file descriptor STDERR_FILENO. Errors? They make the process dump core. And if the opt.abort option is set, most warnings are treated as errors too. πŸ’€

The malloc_message variable lets the programmer override the function that emits the text strings forming the errors and warnings, if for some reason the STDERR_FILENO file descriptor is not suitable for this. malloc_message() takes the cbopaque pointer argument that is NULL unless overridden by the arguments in a call to malloc_stats_print(), followed by a string pointer.

🚨 Please note: doing anything which tries to allocate memory in this function is likely to result in a crash or deadlock. no cap, do not do it.

All messages are prefixed by β€œ<jemalloc>: ”. πŸ’…


✨ RETURN VALUES ✨

🫢 Standard API 🫢

The malloc() and calloc() functions return a pointer to the allocated memory if successful; otherwise a NULL pointer is returned and errno is set to ENOMEM.

The posix_memalign() function returns the value 0 if successful; otherwise it returns an error value. The posix_memalign() function will fail if:

  • EINVAL β€” The alignment parameter is not a power of 2 at least as large as sizeof(void *).
  • ENOMEM β€” Memory allocation error.

The aligned_alloc() function returns a pointer to the allocated memory if successful; otherwise a NULL pointer is returned and errno is set. The aligned_alloc() function will fail if:

  • EINVAL β€” The alignment parameter is not a power of 2.
  • ENOMEM β€” Memory allocation error.

The realloc() function returns a pointer, possibly identical to ptr, to the allocated memory if successful; otherwise a NULL pointer is returned, and errno is set to ENOMEM if the error was the result of an allocation failure. The realloc() function always leaves the original buffer intact when an error occurs. 🫢

The free() function returns no value.

🌈 Non-standard API 🌈

The mallocx() and rallocx() functions return a pointer to the allocated memory if successful; otherwise a NULL pointer is returned to indicate insufficient contiguous memory was available to service the allocation request.

The xallocx() function returns the real size of the resulting resized allocation pointed to by ptr, which is a value less than size if the allocation could not be adequately grown in place.

The sallocx() function returns the real size of the allocation pointed to by ptr.

The nallocx() returns the real size that would result from a successful equivalent mallocx() function call, or zero if insufficient memory is available to perform the size computation.

The mallctl(), mallctlnametomib(), and mallctlbymib() functions return 0 on success; otherwise they return an error value. The functions will fail if:

  • EINVAL β€” newp is not NULL, and newlen is too large or too small. Alternatively, *oldlenp is too large or too small; when it happens, except for a very few cases explicitly documented otherwise, as much data as possible are read despite the error, with the amount of data read being recorded in *oldlenp.
  • ENOENT β€” name or mib specifies an unknown/invalid value.
  • EPERM β€” Attempt to read or write void value, or attempt to write read-only value.
  • EAGAIN β€” A memory allocation failure occurred.
  • EFAULT β€” An interface with side effects failed in some way not directly related to mallctl*() read/write processing.

The malloc_usable_size() function returns the usable size of the allocation pointed to by ptr.


🌸 ENVIRONMENT 🌸

The following environment variable affects the execution of the allocation functions:

  • MALLOC_CONF β€” If the environment variable MALLOC_CONF is set, the characters it contains will be interpreted as options. πŸ¦‹

πŸŽ€ EXAMPLES πŸŽ€

To dump core whenever a problem occurs:

ln -s 'abort:true' /etc/malloc.conf

To specify in the source that only one arena should be automatically created:

malloc_conf = "narenas:1";

πŸ¦‹ SEE ALSO πŸ¦‹

madvise(2), mmap(2), sbrk(2), utrace(2), alloca(3), atexit(3), getpagesize(3)


⭐ STANDARDS ⭐

The malloc(), calloc(), realloc(), and free() functions conform to ISO/IEC 9899:1990 (β€œISO C90”).

The posix_memalign() function conforms to IEEE Std 1003.1-2001 (β€œPOSIX.1”).


πŸ‘‘ AUTHOR πŸ‘‘

Jason Evans


πŸ’œ NOTES πŸ’œ

  1. jemalloc website β€” http://jemalloc.net/
  2. JSON format β€” http://www.json.org/
  3. gperftools package β€” http://code.google.com/p/gperftools/

and that's the whole vibe, bestie 🫢✨ this iconic manual page is from jemalloc 5.3.0-0-g54eaed1d8b56, dated 05/06/2022, and it's literally JEMALLOC(3) β€” periodt πŸ’…πŸ’–