Summary
Under high-concurrency workloads (64 concurrent S3 PUT requests), we observe significant futex contention from pthread_mutex locks inside mimalloc v3.5.0. The futex syscalls account for 98.64% of total execution time in our small-object PUT path, with an average latency of 1.03ms per futex call.
Environment
- Platform: Linux x86_64 (Azure VM, 32 vCPU)
- mimalloc version: 3.5.0 (
MI_MALLOC_VERSION = 30500)
- Workload: S3-compatible object storage (RustFS), 64 concurrent PUT requests, 1KiB objects
- Allocator usage: Global allocator via
rustfs-mimalloc-sys crate
Profiling Data
strace analysis (5-second window, 64 concurrent 1KiB PUT)
| Syscall |
Calls |
% Time |
Avg (μs) |
| futex |
53,108 |
98.64% |
1,031 |
| statx |
5,930 |
0.24% |
183 |
| close |
933 |
0.03% |
171 |
| openat |
721 |
0.03% |
191 |
futex address distribution
| Address |
Calls |
% |
0x3b152060040 |
103,703 |
84% |
0x3b152060108 |
19,675 |
16% |
perf analysis
The top functions consuming CPU are from mimalloc internals:
_mi_prim_thread_done_auto_done (mimalloc prim/unix/prim.c:1034)
mi_page_map_init_once (mimalloc page-map.c:326)
mi_page_arena_pages (mimalloc arena.c:687)
Root Cause Analysis
Lock 1: heap->os_abandoned_pages_lock
Used in arena.c to manage abandoned pages list:
// arena.c:1345
if (page->memid.memkind != MI_MEM_ARENA) {
mi_lock(&heap->os_abandoned_pages_lock) {
// push in front - short critical section (pointer manipulation only)
page->prev = NULL;
page->next = heap->os_abandoned_pages;
if (page->next != NULL) { page->next->prev = page; }
heap->os_abandoned_pages = page;
}
}
This lock is acquired at 4 locations in arena.c (lines 1345, 1414, 2502, 2623). The critical section is very short (just pointer manipulation), but under high concurrency with 64 threads, the pthread_mutex overhead becomes significant.
Lock 2: heap->arena_pages_lock
Used for lazy initialization of arena pages:
// arena.c:705
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) {
arena_pages = mi_arena_pages_alloc(arena);
mi_atomic_store_ptr_release(mi_arena_pages_t, &heap->arena_pages[arena->arena_idx], arena_pages);
}
}
This lock is acquired at 1 location (line 705). The critical section includes memory allocation (mi_arena_pages_alloc).
Proposed Optimization
Option A: Double-checked locking for arena_pages_lock
The arena_pages_lock is used for lazy initialization. We can use double-checked locking with atomic operations to reduce lock contention:
// Proposed optimization
arena_pages = mi_atomic_load_ptr_acquire(mi_arena_pages_t, &heap->arena_pages[arena->arena_idx]);
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) {
arena_pages = mi_arena_pages_alloc(arena);
mi_atomic_store_ptr_release(mi_arena_pages_t, &heap->arena_pages[arena->arena_idx], arena_pages);
}
}
}
This reduces lock acquisitions from O(n) to O(1) after initialization.
Option B: Lock-free abandoned pages list
The os_abandoned_pages_lock critical section is very short (just pointer manipulation). We can use a lock-free stack with compare-and-swap:
// Proposed optimization
do {
page->next = heap->os_abandoned_pages;
} while (!mi_atomic_cas_ptr_weak(mi_page_t, &heap->os_abandoned_pages, page, page->next));
This eliminates the lock entirely for the common case.
Questions
- Are these optimizations consistent with mimalloc's design philosophy?
- Would you accept a PR implementing these optimizations?
- Are there any concerns about the lock-free approach for the abandoned pages list?
- Is there a preferred approach (double-checked locking vs lock-free)?
Related Issues
Additional Context
We're running RustFS (S3-compatible object storage) with mimalloc as the global allocator. The workload involves many small allocations/deallocations (1KiB objects) with 64 concurrent requests. The futex contention is the primary bottleneck for our small-object PUT performance.
We're willing to contribute a PR with proper benchmarking and testing if the proposed optimizations are acceptable.
Summary
Under high-concurrency workloads (64 concurrent S3 PUT requests), we observe significant futex contention from
pthread_mutexlocks inside mimalloc v3.5.0. The futex syscalls account for 98.64% of total execution time in our small-object PUT path, with an average latency of 1.03ms per futex call.Environment
MI_MALLOC_VERSION = 30500)rustfs-mimalloc-syscrateProfiling Data
strace analysis (5-second window, 64 concurrent 1KiB PUT)
futex address distribution
0x3b1520600400x3b152060108perf analysis
The top functions consuming CPU are from mimalloc internals:
_mi_prim_thread_done_auto_done(mimalloc prim/unix/prim.c:1034)mi_page_map_init_once(mimalloc page-map.c:326)mi_page_arena_pages(mimalloc arena.c:687)Root Cause Analysis
Lock 1:
heap->os_abandoned_pages_lockUsed in
arena.cto manage abandoned pages list:This lock is acquired at 4 locations in
arena.c(lines 1345, 1414, 2502, 2623). The critical section is very short (just pointer manipulation), but under high concurrency with 64 threads, thepthread_mutexoverhead becomes significant.Lock 2:
heap->arena_pages_lockUsed for lazy initialization of arena pages:
This lock is acquired at 1 location (line 705). The critical section includes memory allocation (
mi_arena_pages_alloc).Proposed Optimization
Option A: Double-checked locking for
arena_pages_lockThe
arena_pages_lockis used for lazy initialization. We can use double-checked locking with atomic operations to reduce lock contention:This reduces lock acquisitions from O(n) to O(1) after initialization.
Option B: Lock-free abandoned pages list
The
os_abandoned_pages_lockcritical section is very short (just pointer manipulation). We can use a lock-free stack with compare-and-swap:This eliminates the lock entirely for the common case.
Questions
Related Issues
dev3#1104: Potential Performance Regression withdev3_mi_arenas_page_abandonon macOS arm64 under high multi-thread load #1306: Assertion failed in_mi_arenas_page_abandonunder high multi-thread loadAdditional Context
We're running RustFS (S3-compatible object storage) with mimalloc as the global allocator. The workload involves many small allocations/deallocations (1KiB objects) with 64 concurrent requests. The futex contention is the primary bottleneck for our small-object PUT performance.
We're willing to contribute a PR with proper benchmarking and testing if the proposed optimizations are acceptable.