Skip to content

Commit d359148

Browse files
committed
perf(pqueue): grow geometrically so Size and Belady can stop reserving 64MB
Size_init and Belady_init each called pqueue_init(8e6), reserving 8 million pointers -- 64MB -- before a single object was cached. MINISIM builds one cache per profile point and holds them all at once, so at the default --size=0.01,1,100 that reservation was paid a hundred times over. Measured on cloudPhysicsIO.oracleGeneral, peak virtual memory for the same curve: lru 859 MiB size 6976 MiB belady 6970 MiB about 6.1GB of address space neither run had any use for. It never appeared in RSS -- the block is malloc'd and never touched, so the pages stay unmapped -- which is why this was easy to miss. It is not free though: under `ulimit -v 2g`, the shape a scheduler or container commonly imposes, size and belady abort where lru completes. The reservation could not simply be shrunk, because pqueue grew by adding a fixed q->step, and step is the initial capacity. Starting small and reaching 8M entries that way would have cost thousands of reallocs and hundreds of GB of memcpy -- from 1024, 7803 reallocs and roughly 250GB. Doubling instead makes the same growth 13 reallocs and 67MB, so the initial guess only has to cover the common case rather than the worst one, and both caches now reserve 512KB. Doubling does not cost memory in the steady state either. The old policy reserved the full step whether it was needed or not; doubling lands just above what the queue actually holds -- 67MB for one that ends at 8M entries against the 64MB reserved unconditionally, and far less for every queue that stays smaller than its initial guess. q->step is left as the record of the initial capacity, which pqueue_duplicate still copies. After: size 914 MiB and belady 913 MiB against lru's 859 -- the 100 caches add the 51MB the reservations account for -- and both complete under `ulimit -v 2g`. Miss ratios are byte-identical with and without the change (size 0.5017/0.5923, belady 0.4301/0.4647 at 1GB), the PG prefetcher -- the third pqueue user, which inits with 2 and relied on that linear growth -- still runs, and the suite passes 10/10 with 185 CLI checks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YUq1vM4g82TkaX2jvLmuQc
1 parent 5b4dd39 commit d359148

3 files changed

Lines changed: 38 additions & 3 deletions

File tree

libCacheSim/cache/eviction/Belady.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ static void Belady_remove_obj(cache_t *cache, cache_obj_t *obj);
5353
* @param ccache_params some common cache parameters
5454
* @param cache_specific_params Belady specific parameters, should be NULL
5555
*/
56+
/* 512KB of pointers. The queue grows geometrically, so the initial reservation
57+
* only has to cover the common case rather than the worst one. Reserving 8e6
58+
* entries -- 64MB -- up front was paid by every cache: MINISIM builds one per
59+
* profile point and holds them all at once, so the default --size=0.01,1,100
60+
* reserved about 6.1GB of address space beyond what the same curve costs for
61+
* lru. That never showed up in RSS, because the reservation is malloc'd and
62+
* never touched, but it is not free: under `ulimit -v 2g` the run aborts where
63+
* lru completes. */
64+
static const unsigned long kInitialPQCapacity = (512UL << 10) / sizeof(void *);
65+
5666
cache_t *Belady_init(const common_cache_params_t ccache_params,
5767

5868
const char *cache_specific_params) {
@@ -70,7 +80,7 @@ cache_t *Belady_init(const common_cache_params_t ccache_params,
7080
Belady_params_t *params = my_malloc(Belady_params_t);
7181
cache->eviction_params = params;
7282

73-
params->pq = pqueue_init((unsigned long)8e6);
83+
params->pq = pqueue_init(kInitialPQCapacity);
7484
return cache;
7585
}
7686

libCacheSim/cache/eviction/Size.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ static void Size_remove_obj(cache_t *cache, cache_obj_t *obj);
5151
* @param ccache_params some common cache parameters
5252
* @param cache_specific_params Size specific parameters, should be NULL
5353
*/
54+
/* 512KB of pointers. The queue grows geometrically, so the initial reservation
55+
* only has to cover the common case rather than the worst one. Reserving 8e6
56+
* entries -- 64MB -- up front was paid by every cache: MINISIM builds one per
57+
* profile point and holds them all at once, so the default --size=0.01,1,100
58+
* reserved about 6.1GB of address space beyond what the same curve costs for
59+
* lru. That never showed up in RSS, because the reservation is malloc'd and
60+
* never touched, but it is not free: under `ulimit -v 2g` the run aborts where
61+
* lru completes. */
62+
static const unsigned long kInitialPQCapacity = (512UL << 10) / sizeof(void *);
63+
5464
cache_t *Size_init(const common_cache_params_t ccache_params,
5565

5666
const char *cache_specific_params) {
@@ -68,7 +78,7 @@ cache_t *Size_init(const common_cache_params_t ccache_params,
6878
Size_params_t *params = my_malloc(Size_params_t);
6979
cache->eviction_params = params;
7080

71-
params->pq = pqueue_init((unsigned long)8e6);
81+
params->pq = pqueue_init(kInitialPQCapacity);
7282
return cache;
7383
}
7484

libCacheSim/dataStructure/pqueue.c

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,22 @@ int pqueue_insert(pqueue_t *q, void *d) {
132132

133133
/* allocate more memory if necessary */
134134
if (q->size >= q->avail) {
135-
newsize = q->size + q->step;
135+
/* Double, rather than adding the fixed q->step the queue was built with.
136+
* The step is the initial capacity, so a queue that starts small paid
137+
* O(N/step) reallocs and O(N^2/step) copied pointers to reach N entries:
138+
* from a 1024-entry start, 7803 reallocs and about 250GB of memcpy to
139+
* reach 8M. Doubling makes the same growth 13 reallocs and 67MB, which is
140+
* what lets Size and Belady stop reserving 64MB apiece up front.
141+
*
142+
* This does not cost memory in the steady state: the old policy reserved
143+
* the full step whether or not it was needed, while doubling lands just
144+
* above what the queue actually holds -- 67MB for a queue that ends at 8M
145+
* entries against the 64MB that was reserved unconditionally, and far less
146+
* for every queue that stays smaller than its initial guess.
147+
*
148+
* q->step is left as the record of the initial capacity, which
149+
* pqueue_duplicate still copies. */
150+
newsize = q->avail * 2;
136151
if (!(tmp = realloc(q->d, sizeof(void *) * newsize))) return 1;
137152
q->d = tmp;
138153
q->avail = newsize;

0 commit comments

Comments
 (0)