interpret/src/pool.h

379 lines
13 KiB
C

/** @license 2021 Neil Edelman, distributed under the terms of the
[MIT License](https://opensource.org/licenses/MIT).
@abstract Source <src/pool.h>, depends on <src/heap.h>, and <src/array.h>;
examples <test/test_pool.c>.
@subtitle Stable pool
![Example of Pool](../web/pool.png)
<tag:<P>pool> is a memory pool that stores <typedef:<PP>type>. Pointers to
valid items in the pool are stable, but not generally in any order. When
removal is ongoing and uniformly sampled while reaching a steady-state size,
it will eventually settle in one contiguous region.
@param[POOL_NAME, POOL_TYPE]
`<P>` that satisfies `C` naming conventions when mangled and a valid tag type,
<typedef:<PP>type>, associated therewith; required. `<PP>` is private, whose
names are prefixed in a manner to avoid collisions.
@param[POOL_CHUNK_MIN_CAPACITY]
Default is 8; optional number in
`[2, (SIZE_MAX - sizeof pool_chunk) / sizeof <PP>type]` that the capacity can
not go below.
@param[POOL_EXPECT_TRAIT]
Do not un-define certain variables for subsequent inclusion in a trait.
@param[POOL_TO_STRING_NAME, POOL_TO_STRING]
To string trait contained in <to_string.h>; `<PSZ>` that satisfies `C` naming
conventions when mangled and function implementing <typedef:<PSZ>to_string_fn>.
There can be multiple to string traits, but only one can omit
`POOL_TO_STRING_NAME`. This container is only partially iterable: the values
are only the first chunk, so this is not very useful except for debugging.
@depend [array](https://github.com/neil-edelman/array)
@depend [heap](https://github.com/neil-edelman/heap)
@std C89 */
#if !defined(POOL_NAME) || !defined(POOL_TYPE)
#error Name POOL_NAME undefined or tag type POOL_TYPE undefined.
#endif
#if defined(POOL_TO_STRING_NAME) || defined(POOL_TO_STRING)
#define POOL_TO_STRING_TRAIT 1
#else
#define POOL_TO_STRING_TRAIT 0
#endif
#define POOL_TRAITS POOL_TO_STRING_TRAIT
#if POOL_TRAITS > 1
#error Only one trait per include is allowed; use POOL_EXPECT_TRAIT.
#endif
#if defined(POOL_TO_STRING_NAME) && !defined(POOL_TO_STRING)
#error POOL_TO_STRING_NAME requires POOL_TO_STRING.
#endif
#ifndef POOL_H /* <!-- idempotent */
#define POOL_H
#include <stdlib.h>
#include <assert.h>
#include <errno.h>
#if defined(POOL_CAT_) || defined(POOL_CAT) || defined(P_) || defined(PP_) \
|| defined(POOL_IDLE)
#error Unexpected defines.
#endif
/* <Kernighan and Ritchie, 1988, p. 231>. */
#define POOL_CAT_(n, m) n ## _ ## m
#define POOL_CAT(n, m) POOL_CAT_(n, m)
#define P_(n) POOL_CAT(POOL_NAME, n)
#define PP_(n) POOL_CAT(pool, P_(n))
#define POOL_IDLE { ARRAY_IDLE, HEAP_IDLE, (size_t)0 }
/** @return An order on `a`, `b` which specifies a max-heap. */
static int pool_index_compare(const size_t a, const size_t b) { return a < b; }
#define HEAP_NAME poolfree
#define HEAP_TYPE size_t
#define HEAP_COMPARE &pool_index_compare
#include "heap.h"
#endif /* idempotent --> */
#if POOL_TRAITS == 0 /* <!-- base code */
#ifndef POOL_CHUNK_MIN_CAPACITY /* <!-- !min */
#define POOL_CHUNK_MIN_CAPACITY 8
#endif /* !min --> */
#if POOL_CHUNK_MIN_CAPACITY < 2
#error Pool chunk capacity error.
#endif
/** A valid tag type set by `POOL_TYPE`. */
typedef POOL_TYPE PP_(type);
/* Size and chunk, which goes into a sorted array. */
struct PP_(slot) { size_t size; PP_(type) *chunk; };
#define ARRAY_NAME PP_(slot)
#define ARRAY_TYPE struct PP_(slot)
#include "array.h"
/** Consists of a map of several chunks of increasing size and a free-heap.
Zeroed data is a valid state. To instantiate to an idle state, see
<fn:<P>pool>, `POOL_IDLE`, `{0}` (`C99`,) or being `static`.
![States.](../web/states.png) */
struct P_(pool) {
struct PP_(slot_array) slots;
struct poolfree_heap free0; /* Free-list in chunk-zero. */
size_t capacity0; /* Capacity of chunk-zero. */
};
/** @return Index of sorted chunk that is higher than `x` in `slots`, but
treating zero as special. @order \O(\log `chunks`) */
static size_t PP_(upper)(const struct PP_(slot_array) *const slots,
const PP_(type) *const x) {
const struct PP_(slot) *const base = slots->data;
size_t n, b0, b1;
assert(slots && x);
if(!(n = slots->size)) return 0;
assert(base);
if(!--n) return 1;
/* The last one is a special case: it doesn't have an upper bound. */
for(b0 = 1, --n; n; n /= 2) {
b1 = b0 + n / 2;
if(x < base[b1].chunk) { continue; }
else if(base[b1 + 1].chunk <= x) { b0 = b1 + 1; n--; continue; }
else { return b1 + 1; }
}
return b0 + (x >= base[slots->size - 1].chunk);
}
/** Which chunk is `x` in `pool`?
@order \O(\log `chunks`), \O(\log \log `size`)? */
static size_t PP_(chunk_idx)(const struct P_(pool) *const pool,
const PP_(type) *const x) {
struct PP_(slot) *const base = pool->slots.data;
size_t up;
assert(pool && pool->slots.size && base && x);
/* One chunk, assume it's in that chunk; first chunk is `capacity0`. */
if(pool->slots.size <= 1
|| (x >= base[0].chunk && x < base[0].chunk + pool->capacity0))
return assert(x >= base[0].chunk && x < base[0].chunk + pool->capacity0),
0;
up = PP_(upper)(&pool->slots, x);
return assert(up), up - 1;
}
/** Makes sure there are space for `n` further items in `pool`.
@return Success. */
static int PP_(buffer)(struct P_(pool) *const pool, const size_t n) {
const size_t min_size = POOL_CHUNK_MIN_CAPACITY,
max_size = (size_t)-1 / sizeof(PP_(type));
struct PP_(slot) *base = pool->slots.data, *slot;
PP_(type) *chunk;
size_t c, insert;
int is_recycled = 0;
assert(pool && min_size <= max_size && pool->capacity0 <= max_size &&
(!pool->slots.size && !pool->free0.a.size /* !chunks[0] -> !free0 */
|| pool->slots.size && base
&& base[0].size <= pool->capacity0
&& (!pool->free0.a.size
|| pool->free0.a.size < base[0].size
&& pool->free0.a.data[0] < base[0].size)));
/* Ensure space for new slot. */
if(!n || pool->slots.size && n <= pool->capacity0
- base[0].size + pool->free0.a.size) return 1; /* Already enough. */
if(max_size < n) return errno = ERANGE, 1; /* Request unsatisfiable. */
if(!PP_(slot_array_buffer)(&pool->slots, 1)) return 0;
base = pool->slots.data; /* It may have moved! */
/* Figure out the capacity of the next chunk. */
c = pool->capacity0;
if(pool->slots.size && base[0].size) { /* ~Golden ratio. */
size_t c1 = c + (c >> 1) + (c >> 3);
c = (c1 < c || c1 > max_size) ? max_size : c1;
}
if(c < min_size) c = min_size;
if(c < n) c = n;
/* Allocate it. */
if(pool->slots.size && !base[0].size)
is_recycled = 1, chunk = realloc(base[0].chunk, c * sizeof *chunk);
else chunk = malloc(c * sizeof *chunk);
if(!chunk) { if(!errno) errno = ERANGE; return 0; }
pool->capacity0 = c; /* We only need to store the capacity of chunk 0. */
if(is_recycled) return base[0].size = 0, base[0].chunk = chunk, 1;
/* Evict chunk 0. */
if(!pool->slots.size) insert = 0;
else insert = PP_(upper)(&pool->slots, base[0].chunk);
assert(insert <= pool->slots.size);
slot = PP_(slot_array_insert)(&pool->slots, 1, insert);
assert(slot); /* Made space for it before. */
slot->chunk = base[0].chunk, slot->size = base[0].size;
base[0].chunk = chunk, base[0].size = 0;
return 1;
}
/** Either `data` in `pool` is in a secondary chunk, in which case it
decrements the size, or it's the zero-chunk, where it gets added to the
free-heap.
@return Success. It may fail due to a free-heap memory allocation error.
@order Amortized \O(\log \log `items`) @throws[realloc] */
static int PP_(remove)(struct P_(pool) *const pool,
const PP_(type) *const data) {
size_t c = PP_(chunk_idx)(pool, data);
struct PP_(slot) *slot = pool->slots.data + c;
assert(pool && pool->slots.size && data);
if(!c) { /* It's in the zero-slot, we need to deal with the free-heap. */
const size_t idx = (size_t)(data - slot->chunk);
assert(pool->capacity0 && slot->size <= pool->capacity0
&& idx < slot->size);
if(idx + 1 == slot->size) {
/* Keep shrinking going while item on the free-heap are exposed. */
while(--slot->size && !poolfree_heap_is_empty(&pool->free0)) {
const size_t free = poolfree_heap_peek(&pool->free0);
if(free < slot->size - 1) break;
assert(free == slot->size - 1);
poolfree_heap_pop(&pool->free0);
}
} else if(!poolfree_heap_add(&pool->free0, idx)) return 0;
} else if(assert(slot->size), !--slot->size) {
PP_(type) *const chunk = slot->chunk;
PP_(slot_array_remove)(&pool->slots, pool->slots.data + c);
free(chunk);
}
return 1;
}
/** Initializes `pool` to idle. @order \Theta(1) @allow */
static void P_(pool)(struct P_(pool) *const pool) { assert(pool),
PP_(slot_array)(&pool->slots), poolfree_heap(&pool->free0),
pool->capacity0 = 0; }
/** Destroys `pool` and returns it to idle. @order \O(\log `data`) @allow */
static void P_(pool_)(struct P_(pool) *const pool) {
struct PP_(slot) *s, *s_end;
assert(pool);
for(s = pool->slots.data, s_end = s + pool->slots.size; s < s_end; s++)
assert(s->chunk), free(s->chunk);
PP_(slot_array_)(&pool->slots);
poolfree_heap_(&pool->free0);
P_(pool)(pool);
}
/** Ensure capacity of at least `n` further items in `pool`. Pre-sizing is
better for contiguous blocks, but takes up that memory.
@return Success. @throws[ERANGE, malloc] @allow */
static int P_(pool_buffer)(struct P_(pool) *const pool, const size_t n) {
return assert(pool), PP_(buffer)(pool, n);
}
/** This pointer is constant until it gets <fn:<P>pool_remove>.
@return A pointer to a new uninitialized element from `pool`.
@throws[ERANGE, malloc] @order amortised O(1) @allow */
static PP_(type) *P_(pool_new)(struct P_(pool) *const pool) {
struct PP_(slot) *slot0;
assert(pool);
if(!PP_(buffer)(pool, 1)) return 0;
assert(pool->slots.size && (pool->free0.a.size ||
pool->slots.data[0].size < pool->capacity0));
if(!poolfree_heap_is_empty(&pool->free0)) {
/* Cheating: we prefer the minimum index from a max-heap, but it
doesn't really matter, so take the one off the array used for heap. */
size_t *free;
free = heap_poolfree_node_array_pop(&pool->free0.a);
return assert(free), pool->slots.data[0].chunk + *free;
}
/* The free-heap is empty; guaranteed by <fn:<PP>buffer>. */
slot0 = pool->slots.data + 0;
assert(slot0 && slot0->size < pool->capacity0);
return slot0->chunk + slot0->size++;
}
/** Deletes `data` from `pool`. Do not remove data that is not in `pool`.
@return Success. @order \O(\log \log `items`) @allow */
static int P_(pool_remove)(struct P_(pool) *const pool,
PP_(type) *const data) { return PP_(remove)(pool, data); }
/** Removes all from `pool`, but keeps it's active state, only freeing the
smaller blocks. @order \O(\log `items`) @allow */
static void P_(pool_clear)(struct P_(pool) *const pool) {
struct PP_(slot) *s, *s_end;
assert(pool);
if(!pool->slots.size) { assert(!pool->free0.a.size); return; }
for(s = pool->slots.data + 1, s_end = s - 1 + pool->slots.size;
s < s_end; s++) assert(s->chunk && s->size), free(s->chunk);
pool->slots.data[0].size = 0;
pool->slots.size = 1;
poolfree_heap_clear(&pool->free0);
}
/* <!-- iterate interface: it's not actually possible to iterate though given
the information that we have, but placing it here for testing purposes.
Iterates through the zero-slot, ignoring the free list. Do not call. */
struct PP_(iterator);
struct PP_(iterator) { struct PP_(slot) *slot0; size_t i; };
/** Loads `pool` into `it`. @implements begin */
static void PP_(begin)(struct PP_(iterator) *const it,
const struct P_(pool) *const pool) {
assert(it && pool);
if(pool->slots.size) it->slot0 = pool->slots.data + 0;
else it->slot0 = 0;
it->i = 0;
}
/** Advances `it`. @implements next */
static const PP_(type) *PP_(next)(struct PP_(iterator) *const it) {
assert(it);
return it->slot0 && it->i < it->slot0->size
? it->slot0->chunk + it->i++ : 0;
}
/* iterate --> */
#ifdef POOL_TEST /* <!-- test */
/* Forward-declare. */
static void (*PP_(to_string))(const PP_(type) *, char (*)[12]);
static const char *(*PP_(pool_to_string))(const struct P_(pool) *);
#include "../test/test_pool.h"
#endif /* test --> */
/* <!-- box (multiple traits) */
#define BOX_ PP_
#define BOX_CONTAINER struct P_(pool)
#define BOX_CONTENTS PP_(type)
static void PP_(unused_base_coda)(void);
static void PP_(unused_base)(void) {
P_(pool)(0); P_(pool_)(0); P_(pool_buffer)(0, 0); P_(pool_new)(0);
P_(pool_remove)(0, 0); P_(pool_clear)(0); PP_(begin)(0, 0);
PP_(next)(0); PP_(unused_base_coda)();
}
static void PP_(unused_base_coda)(void) { PP_(unused_base)(); }
#elif defined(POOL_TO_STRING) /* base code --><!-- to string trait */
#ifdef POOL_TO_STRING_NAME /* <!-- name */
#define SZ_(n) POOL_CAT(P_(pool), POOL_CAT(POOL_TO_STRING_NAME, n))
#else /* name --><!-- !name */
#define SZ_(n) POOL_CAT(P_(pool), n)
#endif /* !name --> */
#define TO_STRING POOL_TO_STRING
#include "to_string.h" /** \include */
#ifdef POOL_TEST /* <!-- expect: greedy satisfy forward-declared. */
#undef POOL_TEST
static PSZ_(to_string_fn) PP_(to_string) = PSZ_(to_string);
static const char *(*PP_(pool_to_string))(const struct P_(pool) *)
= &SZ_(to_string);
#endif /* expect --> */
#undef SZ_
#undef POOL_TO_STRING
#ifdef POOL_TO_STRING_NAME
#undef POOL_TO_STRING_NAME
#endif
#endif /* traits --> */
#ifdef POOL_EXPECT_TRAIT /* <!-- trait */
#undef POOL_EXPECT_TRAIT
#else /* trait --><!-- !trait */
#ifdef POOL_TEST
#error No POOL_TO_STRING traits defined for POOL_TEST.
#endif
#undef POOL_NAME
#undef POOL_TYPE
#undef BOX_
#undef BOX_CONTAINER
#undef BOX_CONTENTS
/* box (multiple traits) --> */
#endif /* !trait --> */
#undef POOL_TO_STRING_TRAIT
#undef POOL_TRAITS