From 33df484912f8ac4af759a1cf865a2cebbdecbf34 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 Feb 2023 22:56:37 +0100 Subject: [PATCH] Introduce VecDeque Introduce a double-ended queue implemented with a growable ring buffer. Inspired from the Rust VecDeque type: --- app/meson.build | 4 + app/src/util/vecdeque.h | 379 ++++++++++++++++++++++++++++++++++++++ app/tests/test_vecdeque.c | 197 ++++++++++++++++++++ 3 files changed, 580 insertions(+) create mode 100644 app/src/util/vecdeque.h create mode 100644 app/tests/test_vecdeque.c diff --git a/app/meson.build b/app/meson.build index c24a17de..a238eb8f 100644 --- a/app/meson.build +++ b/app/meson.build @@ -300,6 +300,10 @@ if get_option('buildtype') == 'debug' 'src/util/str.c', 'src/util/strbuf.c', ]], + ['test_vecdeque', [ + 'tests/test_vecdeque.c', + 'src/util/memory.c', + ]], ['test_vector', [ 'tests/test_vector.c', ]], diff --git a/app/src/util/vecdeque.h b/app/src/util/vecdeque.h new file mode 100644 index 00000000..e5372e02 --- /dev/null +++ b/app/src/util/vecdeque.h @@ -0,0 +1,379 @@ +#ifndef SC_VECDEQUE_H +#define SC_VECDEQUE_H + +#include "common.h" + +#include +#include +#include +#include +#include + +#include "util/memory.h" + +/** + * A double-ended queue implemented with a growable ring buffer. + * + * Inspired from the Rust VecDeque type: + * + */ + +/** + * VecDeque struct body + * + * A VecDeque is a dynamic ring-buffer, managed by the sc_vecdeque_* helpers. + * + * It is generic over the type of its items, so it is implemented via macros. + * + * To use a VecDeque, a new type must be defined: + * + * struct vecdeque_int SC_VECDEQUE(int); + * + * The struct may be anonymous: + * + * struct SC_VECDEQUE(const char *) names; + * + * Functions and macros having name ending with '_' are private. + */ +#define SC_VECDEQUE(type) { \ + size_t cap; \ + size_t origin; \ + size_t size; \ + type *data; \ +} + +/** + * Static initializer for a VecDeque + */ +#define SC_VECDEQUE_INITIALIZER { 0, 0, 0, NULL } + +/** + * Initialize an empty VecDeque + */ +#define sc_vecdeque_init(pv) \ +({ \ + (pv)->cap = 0; \ + (pv)->origin = 0; \ + (pv)->size = 0; \ + (pv)->data = NULL; \ +}) + +/** + * Destroy a VecDeque + */ +#define sc_vecdeque_destroy(pv) \ + free((pv)->data) + +/** + * Clear a VecDeque + * + * Remove all items. + */ +#define sc_vecdeque_clear(pv) \ +(void) ({ \ + sc_vecdeque_destroy(pv); \ + sc_vecdeque_init(pv); \ +}) + +/** + * Returns the content size + */ +#define sc_vecdeque_size(pv) \ + (pv)->size + +/** + * Return whether the VecDeque is empty (i.e. its size is 0) + */ +#define sc_vecdeque_is_empty(pv) \ + ((pv)->size == 0) + +/** + * Return whether the VecDeque is full + * + * A VecDeque is full when its size equals its current capacity. However, it + * does not prevent to push a new item (with sc_vecdeque_push()), since this + * will increase its capacity. + */ +#define sc_vecdeque_is_full(pv) \ + ((pv)->size == (pv)->cap) + +/** + * The minimal allocation size, in number of items + * + * Private. + */ +#define SC_VECDEQUE_MINCAP_ ((size_t) 10) + +/** + * The maximal allocation size, in number of items + * + * Use SIZE_MAX/2 to fit in ssize_t, and so that cap*1.5 does not overflow. + * + * Private. + */ +#define sc_vecdeque_max_cap_(pv) (SIZE_MAX / 2 / sizeof(*(pv)->data)) + +/** + * Realloc the internal array to a specific capacity + * + * On reallocation success, update the VecDeque capacity (`*pcap`) and origin + * (`*porigin`), and return the reallocated data. + * + * On reallocation failure, return NULL without any change. + * + * Private. + * + * \param ptr the current `data` field of the SC_VECDEQUE to realloc + * \param newcap the requested capacity, in number of items + * \param item_size the size of one item (the generic type is unknown from this + * function) + * \param pcap a pointer to the `cap` field of the SC_VECDEQUE [IN/OUT] + * \param porigin a pointer to pv->origin [IN/OUT] + * \param size the `size` field of the SC_VECDEQUE + * \return the new array to assign to the `data` field of the SC_VECDEQUE (if + * not NULL) + */ +static inline void * +sc_vecdeque_reallocdata_(void *ptr, size_t newcap, size_t item_size, + size_t *pcap, size_t *porigin, size_t size) { + + size_t oldcap = *pcap; + size_t oldorigin = *porigin; + + assert(newcap > oldcap); // Could only grow + + if (oldorigin + size <= oldcap) { + // The current content will stay in place, just realloc + // + // As an example, here is the content of a ring-buffer (oldcap=10) + // before the realloc: + // + // _ _ 2 3 4 5 6 7 _ _ + // ^ + // origin + // + // It is resized (newcap=15), e.g. with sc_vecdeque_reserve(): + // + // _ _ 2 3 4 5 6 7 _ _ _ _ _ _ _ + // ^ + // origin + + void *newptr = reallocarray(ptr, newcap, item_size); + if (!newptr) { + return NULL; + } + + *pcap = newcap; + return newptr; + } + + // Copy the current content to the new array + // + // As an example, here is the content of a ring-buffer (oldcap=10) before + // the realloc: + // + // 5 6 7 _ _ 0 1 2 3 4 + // ^ + // origin + // + // It is resized (newcap=15), e.g. with sc_vecdeque_reserve(): + // + // 0 1 2 3 4 5 6 7 _ _ _ _ _ _ _ + // ^ + // origin + + assert(size); + void *newptr = sc_allocarray(newcap, item_size); + if (!newptr) { + return NULL; + } + + size_t right_len = MIN(size, oldcap - oldorigin); + assert(right_len); + memcpy(newptr, ptr + (oldorigin * item_size), right_len * item_size); + + if (size > right_len) { + memcpy(newptr + (right_len * item_size), ptr, + (size - right_len) * item_size); + } + + free(ptr); + + *pcap = newcap; + *porigin = 0; + return newptr; +} + +/** + * Macro to realloc the internal data to a new capacity + * + * Private. + * + * \retval true on success + * \retval false on allocation failure (the VecDeque is left untouched) + */ +#define sc_vecdeque_realloc_(pv, newcap) \ +({ \ + void *p = sc_vecdeque_reallocdata_((pv)->data, newcap, \ + sizeof(*(pv)->data), &(pv)->cap, \ + &(pv)->origin, (pv)->size); \ + if (p) { \ + (pv)->data = p; \ + } \ + (bool) p; \ +}); + +static inline size_t +sc_vecdeque_growsize_(size_t value) +{ + /* integer multiplication by 1.5 */ + return value + (value >> 1); +} + +/** + * Increase the capacity of the VecDeque to at least `mincap` + * + * \param pv a pointer to the VecDeque + * \param mincap (`size_t`) the requested capacity + * \retval true on success + * \retval false on allocation failure (the VecDeque is left untouched) + */ +#define sc_vecdeque_reserve(pv, mincap) \ +({ \ + assert(mincap <= sc_vecdeque_max_cap_(pv)); \ + bool ok; \ + /* avoid to allocate tiny arrays (< SC_VECDEQUE_MINCAP_) */ \ + size_t mincap_ = MAX(mincap, SC_VECDEQUE_MINCAP_); \ + if (mincap_ <= (pv)->cap) { \ + /* nothing to do */ \ + ok = true; \ + } else if (mincap_ <= sc_vecdeque_max_cap_(pv)) { \ + /* not too big */ \ + size_t newsize = sc_vecdeque_growsize_((pv)->cap); \ + newsize = CLAMP(newsize, mincap_, sc_vecdeque_max_cap_(pv)); \ + ok = sc_vecdeque_realloc_(pv, newsize); \ + } else { \ + ok = false; \ + } \ + ok; \ +}) + +/** + * Automatically grow the VecDeque capacity + * + * Private. + * + * \retval true on success + * \retval false on allocation failure (the VecDeque is left untouched) + */ +#define sc_vecdeque_grow_(pv) \ +({ \ + bool ok; \ + if ((pv)->cap < sc_vecdeque_max_cap_(pv)) { \ + size_t newsize = sc_vecdeque_growsize_((pv)->cap); \ + newsize = CLAMP(newsize, SC_VECDEQUE_MINCAP_, \ + sc_vecdeque_max_cap_(pv)); \ + ok = sc_vecdeque_realloc_(pv, newsize); \ + } else { \ + ok = false; \ + } \ + ok; \ +}) + +/** + * Grow the VecDeque capacity if it is full + * + * Private. + * + * \retval true on success + * \retval false on allocation failure (the VecDeque is left untouched) + */ +#define sc_vecdeque_grow_if_needed_(pv) \ + (!sc_vecdeque_is_full(pv) || sc_vecdeque_grow_(pv)) + +/** + * Push an uninitialized item, and return a pointer to it + * + * It does not attempt to resize the VecDeque. It is an error to this function + * if the VecDeque is full. + * + * This function may not fail. It returns a valid non-NULL pointer to the + * uninitialized item just pushed. + */ +#define sc_vecdeque_push_hole_noresize(pv) \ +({ \ + assert(!sc_vecdeque_is_full(pv)); \ + ++(pv)->size; \ + &(pv)->data[((pv)->origin + (pv)->size - 1) % (pv)->cap]; \ +}) + +/** + * Push an uninitialized item, and return a pointer to it + * + * If the VecDeque is full, it is resized. + * + * This function returns either a valid non-NULL pointer to the uninitialized + * item just pushed, or NULL on reallocation failure. + */ +#define sc_vecdeque_push_hole(pv) \ + (sc_vecdeque_grow_if_needed_(pv) ? \ + sc_vecdeque_push_hole_noresize(pv) : NULL) + +/** + * Push an item + * + * It does not attempt to resize the VecDeque. It is an error to this function + * if the VecDeque is full. + * + * This function may not fail. + */ +#define sc_vecdeque_push_noresize(pv, item) \ +(void) ({ \ + assert(!sc_vecdeque_is_full(pv)); \ + ++(pv)->size; \ + (pv)->data[((pv)->origin + (pv)->size - 1) % (pv)->cap] = item; \ +}) + +/** + * Push an item + * + * If the VecDeque is full, it is resized. + * + * \retval true on success + * \retval false on allocation failure (the VecDeque is left untouched) + */ +#define sc_vecdeque_push(pv, item) \ +({ \ + bool ok = sc_vecdeque_grow_if_needed_(pv); \ + if (ok) { \ + sc_vecdeque_push_noresize(pv, item); \ + } \ + ok; \ +}) + +/** + * Pop an item and return a pointer to it (still in the VecDeque) + * + * Returning a pointer allows the caller to destroy it in place without copy + * (especially if the item type is big). + * + * It is an error to call this function if the VecDeque is empty. + */ +#define sc_vecdeque_popref(pv) \ +({ \ + assert(!sc_vecdeque_is_empty(pv)); \ + size_t pos = (pv)->origin; \ + (pv)->origin = ((pv)->origin + 1) % (pv)->cap; \ + --(pv)->size; \ + &(pv)->data[pos]; \ +}) + +/** + * Pop an item and return it + * + * It is an error to call this function if the VecDeque is empty. + */ +#define sc_vecdeque_pop(pv) \ + (*sc_vecdeque_popref(pv)) + +#endif diff --git a/app/tests/test_vecdeque.c b/app/tests/test_vecdeque.c new file mode 100644 index 00000000..fa3ba963 --- /dev/null +++ b/app/tests/test_vecdeque.c @@ -0,0 +1,197 @@ +#include "common.h" + +#include + +#include "util/vecdeque.h" + +#define pr(pv) \ +({ \ + fprintf(stderr, "cap=%lu origin=%lu size=%lu\n", (pv)->cap, (pv)->origin, (pv)->size); \ + for (size_t i = 0; i < (pv)->cap; ++i) \ + fprintf(stderr, "%d ", (pv)->data[i]); \ + fprintf(stderr, "\n"); \ +}) + +static void test_vecdeque_push_pop(void) { + struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; + + assert(sc_vecdeque_is_empty(&vdq)); + assert(sc_vecdeque_size(&vdq) == 0); + + bool ok = sc_vecdeque_push(&vdq, 5); + assert(ok); + assert(sc_vecdeque_size(&vdq) == 1); + + ok = sc_vecdeque_push(&vdq, 12); + assert(ok); + assert(sc_vecdeque_size(&vdq) == 2); + + int v = sc_vecdeque_pop(&vdq); + assert(v == 5); + assert(sc_vecdeque_size(&vdq) == 1); + + ok = sc_vecdeque_push(&vdq, 7); + assert(ok); + assert(sc_vecdeque_size(&vdq) == 2); + + int *p = sc_vecdeque_popref(&vdq); + assert(p); + assert(*p == 12); + assert(sc_vecdeque_size(&vdq) == 1); + + v = sc_vecdeque_pop(&vdq); + assert(v == 7); + assert(sc_vecdeque_size(&vdq) == 0); + assert(sc_vecdeque_is_empty(&vdq)); + + sc_vecdeque_destroy(&vdq); +} + +static void test_vecdeque_reserve(void) { + struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; + + bool ok = sc_vecdeque_reserve(&vdq, 20); + assert(ok); + assert(vdq.cap == 20); + + assert(sc_vecdeque_size(&vdq) == 0); + + for (size_t i = 0; i < 20; ++i) { + ok = sc_vecdeque_push(&vdq, i); + assert(ok); + } + + assert(sc_vecdeque_size(&vdq) == 20); + + // It is now full + + for (int i = 0; i < 5; ++i) { + int v = sc_vecdeque_pop(&vdq); + assert(v == i); + } + assert(sc_vecdeque_size(&vdq) == 15); + + for (int i = 20; i < 25; ++i) { + ok = sc_vecdeque_push(&vdq, i); + assert(ok); + } + + assert(sc_vecdeque_size(&vdq) == 20); + assert(vdq.cap == 20); + + // Now, the content wraps around the ring buffer: + // 20 21 22 23 24 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + // ^ + // origin + + // It is now full, let's reserve some space + ok = sc_vecdeque_reserve(&vdq, 30); + assert(ok); + assert(vdq.cap == 30); + + assert(sc_vecdeque_size(&vdq) == 20); + + for (int i = 0; i < 20; ++i) { + // We should retrieve the items we inserted in order + int v = sc_vecdeque_pop(&vdq); + assert(v == i + 5); + } + + assert(sc_vecdeque_size(&vdq) == 0); + + sc_vecdeque_destroy(&vdq); +} + +static void test_vecdeque_grow() { + struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; + + bool ok = sc_vecdeque_reserve(&vdq, 20); + assert(ok); + assert(vdq.cap == 20); + + assert(sc_vecdeque_size(&vdq) == 0); + + for (int i = 0; i < 500; ++i) { + ok = sc_vecdeque_push(&vdq, i); + assert(ok); + } + + assert(sc_vecdeque_size(&vdq) == 500); + + for (int i = 0; i < 100; ++i) { + int v = sc_vecdeque_pop(&vdq); + assert(v == i); + } + + assert(sc_vecdeque_size(&vdq) == 400); + + for (int i = 500; i < 1000; ++i) { + ok = sc_vecdeque_push(&vdq, i); + assert(ok); + } + + assert(sc_vecdeque_size(&vdq) == 900); + + for (int i = 100; i < 1000; ++i) { + int v = sc_vecdeque_pop(&vdq); + assert(v == i); + } + + assert(sc_vecdeque_size(&vdq) == 0); + + sc_vecdeque_destroy(&vdq); +} + +static void test_vecdeque_push_hole() { + struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; + + bool ok = sc_vecdeque_reserve(&vdq, 20); + assert(ok); + assert(vdq.cap == 20); + + assert(sc_vecdeque_size(&vdq) == 0); + + for (int i = 0; i < 20; ++i) { + int *p = sc_vecdeque_push_hole(&vdq); + assert(p); + *p = i * 10; + } + + assert(sc_vecdeque_size(&vdq) == 20); + + for (int i = 0; i < 10; ++i) { + int v = sc_vecdeque_pop(&vdq); + assert(v == i * 10); + } + + assert(sc_vecdeque_size(&vdq) == 10); + + for (int i = 20; i < 30; ++i) { + int *p = sc_vecdeque_push_hole(&vdq); + assert(p); + *p = i * 10; + } + + assert(sc_vecdeque_size(&vdq) == 20); + + for (int i = 10; i < 30; ++i) { + int v = sc_vecdeque_pop(&vdq); + assert(v == i * 10); + } + + assert(sc_vecdeque_size(&vdq) == 0); + + sc_vecdeque_destroy(&vdq); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_vecdeque_push_pop(); + test_vecdeque_reserve(); + test_vecdeque_grow(); + test_vecdeque_push_hole(); + + return 0; +}