Extend purging algorithm with peak demand tracking

Implementation inspired by idea described in "Beyond malloc efficiency
to fleet efficiency: a hugepage-aware memory allocator" paper [1].

Primary idea is to track maximum number (peak) of active pages in use
with sliding window and then use this number to decide how many dirty
pages we would like to keep.

We are trying to estimate maximum amount of active memory we'll need in
the near future. We do so by projecting future active memory demand
(based on peak active memory usage we observed in the past within
sliding window) and adding slack on top of it (an overhead is reasonable
to have in exchange of higher hugepages coverage). When peak demand
tracking is off, projection of future active memory is active memory we
are having right now.

Estimation is essentially the same as `nactive_max * (1 + dirty_mult)`.

Peak demand purging algorithm controlled by two config options. Option
`hpa_peak_demand_window_ms` controls duration of sliding window we track
maximum active memory usage in and option `hpa_dirty_mult` controls
amount of slack we are allowed to have as a percent from maximum active
memory usage. By default `hpa_peak_demand_window_ms == 0` now and we
have same behaviour (ratio based purging) that we had before this
commit.

[1]: https://storage.googleapis.com/gweb-research2023-media/pubtools/6170.pdf
This commit is contained in:
Dmitry Ilvokhin 2025-01-21 07:20:15 -08:00 committed by stanjo74
parent 22440a0207
commit ad108d50f1
20 changed files with 537 additions and 29 deletions

View file

@ -106,6 +106,7 @@ CTL_PROTO(opt_hpa_hugify_delay_ms)
CTL_PROTO(opt_hpa_hugify_sync)
CTL_PROTO(opt_hpa_min_purge_interval_ms)
CTL_PROTO(opt_experimental_hpa_max_purge_nhp)
CTL_PROTO(opt_hpa_peak_demand_window_ms)
CTL_PROTO(opt_hpa_dirty_mult)
CTL_PROTO(opt_hpa_sec_nshards)
CTL_PROTO(opt_hpa_sec_max_alloc)
@ -487,6 +488,8 @@ static const ctl_named_node_t opt_node[] = {
{NAME("hpa_min_purge_interval_ms"), CTL(opt_hpa_min_purge_interval_ms)},
{NAME("experimental_hpa_max_purge_nhp"),
CTL(opt_experimental_hpa_max_purge_nhp)},
{NAME("hpa_peak_demand_window_ms"),
CTL(opt_hpa_peak_demand_window_ms)},
{NAME("hpa_dirty_mult"), CTL(opt_hpa_dirty_mult)},
{NAME("hpa_sec_nshards"), CTL(opt_hpa_sec_nshards)},
{NAME("hpa_sec_max_alloc"), CTL(opt_hpa_sec_max_alloc)},
@ -2255,6 +2258,8 @@ CTL_RO_NL_GEN(opt_hpa_min_purge_interval_ms, opt_hpa_opts.min_purge_interval_ms,
uint64_t)
CTL_RO_NL_GEN(opt_experimental_hpa_max_purge_nhp,
opt_hpa_opts.experimental_max_purge_nhp, ssize_t)
CTL_RO_NL_GEN(opt_hpa_peak_demand_window_ms,
opt_hpa_opts.peak_demand_window_ms, uint64_t)
/*
* This will have to change before we publicly document this option; fxp_t and

View file

@ -63,6 +63,11 @@ hpa_supported(void) {
return true;
}
static bool
hpa_peak_demand_tracking_enabled(hpa_shard_t *shard) {
return shard->opts.peak_demand_window_ms > 0;
}
static void
hpa_do_consistency_checks(hpa_shard_t *shard) {
assert(shard->base != NULL);
@ -217,6 +222,11 @@ hpa_shard_init(hpa_shard_t *shard, hpa_central_t *central, emap_t *emap,
shard->stats.nhugify_failures = 0;
shard->stats.ndehugifies = 0;
if (hpa_peak_demand_tracking_enabled(shard)) {
peak_demand_init(&shard->peak_demand,
shard->opts.peak_demand_window_ms);
}
/*
* Fill these in last, so that if an hpa_shard gets used despite
* initialization failing, we'll at least crash instead of just
@ -294,8 +304,37 @@ hpa_ndirty_max(tsdn_t *tsdn, hpa_shard_t *shard) {
if (shard->opts.dirty_mult == (fxp_t)-1) {
return (size_t)-1;
}
return fxp_mul_frac(psset_nactive(&shard->psset),
shard->opts.dirty_mult);
/*
* We are trying to estimate maximum amount of active memory we'll
* need in the near future. We do so by projecting future active
* memory demand (based on peak active memory usage we observed in the
* past within sliding window) and adding slack on top of it (an
* overhead is reasonable to have in exchange of higher hugepages
* coverage). When peak demand tracking is off, projection of future
* active memory is active memory we are having right now.
*
* Estimation is essentially the same as nactive_max * (1 +
* dirty_mult), but expressed differently to factor in necessary
* implementation details.
*/
size_t nactive = psset_nactive(&shard->psset);
size_t nactive_max = nactive;
if (hpa_peak_demand_tracking_enabled(shard)) {
/*
* We release shard->mtx, when we do a syscall to purge dirty
* memory, so someone might grab shard->mtx, allocate memory
* from this shard and update psset's nactive counter, before
* peak_demand_update(...) was called and we'll get
* peak_demand_nactive_max(...) <= nactive as a result.
*/
size_t peak = peak_demand_nactive_max(&shard->peak_demand);
if (peak > nactive_max) {
nactive_max = peak;
}
}
size_t slack = fxp_mul_frac(nactive_max, shard->opts.dirty_mult);
size_t estimation = nactive_max + slack;
return estimation - nactive;
}
static bool
@ -548,6 +587,16 @@ static void
hpa_shard_maybe_do_deferred_work(tsdn_t *tsdn, hpa_shard_t *shard,
bool forced) {
malloc_mutex_assert_owner(tsdn, &shard->mtx);
/* Update active memory demand statistics. */
if (hpa_peak_demand_tracking_enabled(shard)) {
nstime_t now;
shard->central->hooks.curtime(&now,
/* first_reading */ true);
peak_demand_update(&shard->peak_demand, &now,
psset_nactive(&shard->psset));
}
if (!forced && shard->opts.deferral_allowed) {
return;
}

View file

@ -1568,6 +1568,11 @@ malloc_conf_init_helper(sc_data_t *sc_data, unsigned bin_shard_sizes[SC_NBINS],
opt_hpa_opts.experimental_max_purge_nhp,
"experimental_hpa_max_purge_nhp", -1, SSIZE_MAX);
CONF_HANDLE_UINT64_T(
opt_hpa_opts.peak_demand_window_ms,
"hpa_peak_demand_window_ms", 0, 0,
CONF_DONT_CHECK_MIN, CONF_DONT_CHECK_MAX, false);
if (CONF_MATCH("hpa_dirty_mult")) {
if (CONF_MATCH_VALUE("-1")) {
opt_hpa_opts.dirty_mult = (fxp_t)-1;

74
src/peak_demand.c Normal file
View file

@ -0,0 +1,74 @@
#include "jemalloc/internal/jemalloc_preamble.h"
#include "jemalloc/internal/jemalloc_internal_includes.h"
#include "jemalloc/internal/peak_demand.h"
void
peak_demand_init(peak_demand_t *peak_demand, uint64_t interval_ms) {
assert(interval_ms > 0);
peak_demand->epoch = 0;
uint64_t interval_ns = interval_ms * 1000 * 1000;
peak_demand->epoch_interval_ns = interval_ns / PEAK_DEMAND_NBUCKETS;
memset(peak_demand->nactive_max, 0, sizeof(peak_demand->nactive_max));
}
static uint64_t
peak_demand_epoch_ind(peak_demand_t *peak_demand) {
return peak_demand->epoch % PEAK_DEMAND_NBUCKETS;
}
static nstime_t
peak_demand_next_epoch_advance(peak_demand_t *peak_demand) {
uint64_t epoch = peak_demand->epoch;
uint64_t ns = (epoch + 1) * peak_demand->epoch_interval_ns;
nstime_t next;
nstime_init(&next, ns);
return next;
}
static uint64_t
peak_demand_maybe_advance_epoch(peak_demand_t *peak_demand,
const nstime_t *now) {
nstime_t next_epoch_advance =
peak_demand_next_epoch_advance(peak_demand);
if (nstime_compare(now, &next_epoch_advance) < 0) {
return peak_demand_epoch_ind(peak_demand);
}
uint64_t next_epoch = nstime_ns(now) / peak_demand->epoch_interval_ns;
assert(next_epoch > peak_demand->epoch);
/*
* If we missed more epochs, than capacity of circular buffer
* (PEAK_DEMAND_NBUCKETS), re-write no more than PEAK_DEMAND_NBUCKETS
* items as we don't want to zero out same item multiple times.
*/
if (peak_demand->epoch + PEAK_DEMAND_NBUCKETS < next_epoch) {
peak_demand->epoch = next_epoch - PEAK_DEMAND_NBUCKETS;
}
while (peak_demand->epoch < next_epoch) {
++peak_demand->epoch;
uint64_t ind = peak_demand_epoch_ind(peak_demand);
peak_demand->nactive_max[ind] = 0;
}
return peak_demand_epoch_ind(peak_demand);
}
void
peak_demand_update(peak_demand_t *peak_demand, const nstime_t *now,
size_t nactive) {
uint64_t ind = peak_demand_maybe_advance_epoch(peak_demand, now);
size_t *epoch_nactive = &peak_demand->nactive_max[ind];
if (nactive > *epoch_nactive) {
*epoch_nactive = nactive;
}
}
size_t
peak_demand_nactive_max(peak_demand_t *peak_demand) {
size_t nactive_max = peak_demand->nactive_max[0];
for (int i = 1; i < PEAK_DEMAND_NBUCKETS; ++i) {
if (peak_demand->nactive_max[i] > nactive_max) {
nactive_max = peak_demand->nactive_max[i];
}
}
return nactive_max;
}

View file

@ -1657,6 +1657,7 @@ stats_general_print(emitter_t *emitter) {
OPT_WRITE_BOOL("hpa_hugify_sync")
OPT_WRITE_UINT64("hpa_min_purge_interval_ms")
OPT_WRITE_SSIZE_T("experimental_hpa_max_purge_nhp")
OPT_WRITE_UINT64("hpa_peak_demand_window_ms")
if (je_mallctl("opt.hpa_dirty_mult", (void *)&u32v, &u32sz, NULL, 0)
== 0) {
/*