Skip to content

perf: lock contention on os_abandoned_pages_lock and arena_pages_lock under high concurrency #1372

Description

@houseme

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

  1. Are these optimizations consistent with mimalloc's design philosophy?
  2. Would you accept a PR implementing these optimizations?
  3. Are there any concerns about the lock-free approach for the abandoned pages list?
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions