From 68c35f655792f40243ba5f4bc9577e03cc7cc49d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Mar=C3=ADn?= Date: Thu, 16 Jul 2026 23:53:25 +0200 Subject: [PATCH] Allow resuming per-CPU arena selection via thread.arena (#2957) * Allow resuming per-CPU arena selection via thread.arena With percpu_arena enabled, thread.arena control is one-directional. A thread can be bound to a manually created arena (an index at or above the per-CPU auto range) to route a bounded region of work to a dedicated, long-lived arena, but there is no way back: thread_arena_ctl returns EPERM for any index within the auto range, and arena_choose_impl only re-selects a per-CPU arena for threads whose current arena is already in that range. So once a thread is bound to a manual arena it stays pinned there forever, and its later allocations land there instead of following the CPU. Treat setting thread.arena to an index within the per-CPU range as a request to resume automatic per-CPU selection: hand the thread back to percpu management (rebinding it to the current CPU's arena via percpu_arena_update) and return 0 instead of EPERM. Binding to a manual arena is unchanged. The requested index is advisory; under percpu the thread is governed by its current CPU, so it resumes on the current CPU's arena regardless of the value passed. Add test/unit/percpu_arena_resume covering the manual-arena to resume round trip with allocs & dallocs happening meanwhile, and update test_thread_arena, which asserted the old EPERM. Co-Authored-By: Claude Opus 4.8 (1M context) --- Makefile.in | 1 + src/ctl.c | 14 ++++-- test/unit/mallctl.c | 11 +++-- test/unit/percpu_arena_resume.c | 81 ++++++++++++++++++++++++++++++++ test/unit/percpu_arena_resume.sh | 3 ++ 5 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 test/unit/percpu_arena_resume.c create mode 100644 test/unit/percpu_arena_resume.sh diff --git a/Makefile.in b/Makefile.in index c7bab1a2..75cc184a 100644 --- a/Makefile.in +++ b/Makefile.in @@ -276,6 +276,7 @@ TESTS_UNIT := \ $(srcroot)test/unit/pack.c \ $(srcroot)test/unit/pages.c \ $(srcroot)test/unit/peak.c \ + $(srcroot)test/unit/percpu_arena_resume.c \ $(srcroot)test/unit/ph.c \ $(srcroot)test/unit/prng.c \ $(srcroot)test/unit/prof_accum.c \ diff --git a/src/ctl.c b/src/ctl.c index 193e055a..6a12e957 100644 --- a/src/ctl.c +++ b/src/ctl.c @@ -2374,10 +2374,18 @@ thread_arena_ctl(tsd_t *tsd, const size_t *mib, size_t miblen, void *oldp, if (have_percpu_arena && PERCPU_ARENA_ENABLED(opt_percpu_arena)) { if (newind < percpu_arena_ind_limit(opt_percpu_arena)) { /* - * If perCPU arena is enabled, thread_arena control is - * not allowed for the auto arena range. + * Setting thread.arena to an arena in the auto range + * means "resume automatic per-CPU selection" rather than + * pinning to a specific per-CPU arena. This lets a caller + * that temporarily switched to a manually managed arena + * (e.g. a scoped guard) hand the thread back to per-CPU + * management. It is otherwise impossible: a thread bound + * to a manual arena is never reclaimed by percpu (see + * arena_choose_impl), so without this it would stay + * pinned forever. */ - return EPERM; + percpu_arena_update(tsd, percpu_arena_choose()); + return 0; } } diff --git a/test/unit/mallctl.c b/test/unit/mallctl.c index bee7d75a..4452d3cb 100644 --- a/test/unit/mallctl.c +++ b/test/unit/mallctl.c @@ -764,12 +764,17 @@ TEST_BEGIN(test_thread_arena) { 0, "Unexpected mallctl() failure"); new_arena_ind = percpu_arena_ind_limit(opt_percpu_arena) - 1; if (old_arena_ind != new_arena_ind) { + /* + * Setting thread.arena to an index within the per-CPU + * range resumes automatic per-CPU selection rather than + * failing (see test/unit/percpu_arena_resume.c). + */ expect_d_eq( mallctl("thread.arena", (void *)&old_arena_ind, &sz, (void *)&new_arena_ind, sizeof(unsigned)), - EPERM, - "thread.arena ctl " - "should not be allowed with percpu arena"); + 0, + "thread.arena within the per-CPU range should " + "resume per-CPU selection"); } } } diff --git a/test/unit/percpu_arena_resume.c b/test/unit/percpu_arena_resume.c new file mode 100644 index 00000000..6023d3fa --- /dev/null +++ b/test/unit/percpu_arena_resume.c @@ -0,0 +1,81 @@ +#include "test/jemalloc_test.h" + +/* + * Under percpu_arena, binding a thread to a manual arena (an index at or above + * the per-CPU auto range) is one-way: percpu never reclaims it (see + * arena_choose_impl). Setting thread.arena back to an index in the auto range + * resumes per-CPU selection instead of failing with EPERM. + */ +TEST_BEGIN(test_thread_arena_resume_percpu) { + test_skip_if(!have_percpu_arena + || !PERCPU_ARENA_ENABLED(opt_percpu_arena)); + + unsigned limit = percpu_arena_ind_limit(opt_percpu_arena); + /* Bypass the tcache so every allocation and free hits the arena. */ + const int flags = MALLOCX_TCACHE_NONE; + + void *warm = mallocx(1, 0); + expect_ptr_not_null(warm, "Unexpected mallocx() failure"); + dallocx(warm, 0); + + unsigned cur; + size_t sz = sizeof(cur); + expect_d_eq(mallctl("thread.arena", (void *)&cur, &sz, NULL, 0), 0, + "Unexpected mallctl() failure"); + expect_u_lt(cur, limit, "Thread should start on a per-CPU arena"); + + unsigned manual; + sz = sizeof(manual); + expect_d_eq(mallctl("arenas.create", (void *)&manual, &sz, NULL, 0), 0, + "Unexpected arenas.create() failure"); + expect_u_ge(manual, limit, "A manual arena is outside the per-CPU range"); + + unsigned old; + sz = sizeof(old); + expect_d_eq(mallctl("thread.arena", (void *)&old, &sz, (void *)&manual, + sizeof(manual)), 0, "Binding to a manual arena should be allowed"); + sz = sizeof(cur); + expect_d_eq(mallctl("thread.arena", (void *)&cur, &sz, NULL, 0), 0, + "Unexpected mallctl() failure"); + expect_u_eq(cur, manual, "Thread should be bound to the manual arena"); + + void *p_manual = mallocx(1024, flags); + expect_ptr_not_null(p_manual, "Unexpected mallocx() failure"); + unsigned found; + sz = sizeof(found); + expect_d_eq(mallctl("arenas.lookup", (void *)&found, &sz, + (void *)&p_manual, sizeof(p_manual)), 0, + "Unexpected arenas.lookup() failure"); + expect_u_eq(found, manual, "Allocation should come from the manual arena"); + + void *scratch = mallocx(1024, flags); + expect_ptr_not_null(scratch, "Unexpected mallocx() failure"); + dallocx(scratch, flags); + + unsigned resume = 0; + expect_d_eq(mallctl("thread.arena", NULL, NULL, (void *)&resume, + sizeof(resume)), 0, "Should resume per-CPU selection, not fail"); + sz = sizeof(cur); + expect_d_eq(mallctl("thread.arena", (void *)&cur, &sz, NULL, 0), 0, + "Unexpected mallctl() failure"); + expect_u_lt(cur, limit, "Thread should be back on a per-CPU arena"); + + void *p_percpu = mallocx(1024, flags); + expect_ptr_not_null(p_percpu, "Unexpected mallocx() failure"); + sz = sizeof(found); + expect_d_eq(mallctl("arenas.lookup", (void *)&found, &sz, + (void *)&p_percpu, sizeof(p_percpu)), 0, + "Unexpected arenas.lookup() failure"); + expect_u_lt(found, limit, "Allocation should come from a per-CPU arena"); + dallocx(p_percpu, flags); + + /* Free the manual-arena region while bound to a different arena. */ + dallocx(p_manual, flags); +} +TEST_END + +int +main(void) { + return test( + test_thread_arena_resume_percpu); +} diff --git a/test/unit/percpu_arena_resume.sh b/test/unit/percpu_arena_resume.sh new file mode 100644 index 00000000..94d13b97 --- /dev/null +++ b/test/unit/percpu_arena_resume.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +export MALLOC_CONF="percpu_arena:percpu"