diff --git a/.gitignore b/.gitignore index 652de5e..159715e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ build_unix/** libskiplist.so libskiplist.a tests/test -examples/example +examples/skip +examples/mls +examples/slm .direnv/ .idea/ diff --git a/Makefile b/Makefile index 8099e22..17f6899 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,17 @@ -CFLAGS = -Wall -Wextra -Wpedantic -Og -g -std=c99 -Iinclude/ -fPIC -TEST_FLAGS = -Itests/ - #-fsanitize=address,undefined - OBJS = skiplist.o STATIC_LIB = libskiplist.a SHARED_LIB = libskiplist.so +CFLAGS = -Wall -Wextra -Wpedantic -Og -g -std=c99 -Iinclude/ -fPIC +TEST_FLAGS = -Itests/ + #-fsanitize=address,undefined + TESTS = tests/test -EXAMPLES = examples/example +TEST_OBJS = tests/test.o tests/munit.o +EXAMPLES = examples/skip examples/slm -.PHONY: all shared static clean tests examples - -%.o: src/%.c - $(CC) $(CFLAGS) -c -o $@ $^ +.PHONY: all shared static clean test examples mls all: static shared @@ -27,26 +25,38 @@ $(STATIC_LIB): $(OBJS) $(SHARED_LIB): $(OBJS) $(CC) $(CFLAGS) -o $@ $? -shared -tests/%.o: tests/%.c - $(CC) $(CFLAGS) -c -o $@ $^ +examples: $(EXAMPLES) + +mls: examples/mls test: $(TESTS) ./tests/test # env LSAN_OPTIONS=verbosity=1:log_threads=1 ./tests/test -tests/test: tests/test.o tests/munit.o $(STATIC_LIB) - $(CC) $^ -o $@ $(CFLAGS) $(TEST_FLAGS) -pthread - -examples: $(EXAMPLES) - -examples/example: examples/example.c $(STATIC_LIB) +tests/test: $(TEST_OBJS) $(STATIC_LIB) $(CC) $^ -o $@ $(CFLAGS) $(TEST_FLAGS) -pthread clean: rm -f $(OBJS) munit.o test.o + rm -f examples/mls.c rm -f $(STATIC_LIB) rm -f $(TESTS) rm -f $(EXAMPLES) format: clang-format -i include/*.h src/*.c tests/*.c tests/*.h + +%.o: src/%.c + $(CC) $(CFLAGS) -c -o $@ $^ + +tests/%.o: tests/%.c + $(CC) $(CFLAGS) -c -o $@ $^ + +examples/%.o: examples/%.c + $(CC) $(CFLAGS) -c -o $@ $^ + +examples/mls.c: examples/slm.c + $(CC) $(CFLAGS) -C -E examples/slm.c | sed -e '1,7d' -e '/^# [0-9]* "/d' | clang-format > examples/mls.c + +examples/mls: examples/mls.o $(STATIC_LIB) + $(CC) $^ -o $@ $(CFLAGS) $(TEST_FLAGS) -pthread diff --git a/examples/example.c b/examples/skip.c similarity index 100% rename from examples/example.c rename to examples/skip.c diff --git a/examples/slm.c b/examples/slm.c new file mode 100644 index 0000000..c87a71e --- /dev/null +++ b/examples/slm.c @@ -0,0 +1,93 @@ +#include +#include +#include +#include "../include/sl.h" + +/* + * SKIPLIST EXAMPLE: + * + * This example creates a Skiplist keys and values are integers. + */ + +/* Create a type of Skiplist that maps int -> int. */ +struct entry { + int key; + int value; + SKIP_ENTRY(entry) entries; +}; +SKIP_HEAD(skip, entry); + +/* Generate a function to compare keys to order our list. + * + * The function takes three arguments: + * - `a` and `b` of the provided type (`entry` in this case) + * - `aux` an additional auxiliary argument + * and returns: + * a < b : return -1 + * a == b : return 0 + * a > b : return 1 + * + * This function is only called when a and b are not NULL and valid + * pointers to the entry type. + * + * The function will be named: __skip_cmp_ which should be + * passed as the 2nd argument in the SKIP_HEAD_INITIALIZER. + */ + SKIP_COMPARATOR(skip, entry, { + (void)aux; + if (a->key < b->key) + return -1; + if (a->key > b->key) + return 1; + return 0; + }) + +typedef struct skip skip_t; +int main() { +/* Allocate and initialize a Skiplist. */ +#ifdef STATIC_INIT + skip_t _list = SKIP_HEAD_DEFAULT_INITIALIZER(__skip_cmp_entry); + skip_t *list = &_list; +#else /* Dynamic allocation, init. */ + skip_t *list = (skip_t *)malloc(sizeof(skip_t)); + SKIP_DEFAULT_INIT(list, __skip_cmp_entry); +#endif + + /* Insert 10 key/value pairs into the list. */ + for (int i = 0; i < 10; i++) { + struct entry *n; + SKIP_ALLOC_NODE(list, n, entry, entries); + n->key = i; + n->value = i; + SKIP_INSERT(list, entry, n, entries); + } + +#if 0 + /* Delete a specific element in the list. */ + struct entry query; + query.key = 4; + struct entry *removed = SKIP_REMOVE(list, q, entries); + free(removed); + /* Forward traversal. */ + SKIP_FOREACH(np, &head, entries) + np-> ... + /* Reverse traversal. */ + SKIP_FOREACH_REVERSE(np, &head, skiphead, entries) + np-> ... + /* Skiplist Deletion. */ + while (!SKIP_EMPTY(&head)) { + n1 = SKIP_FIRST(&head); + SKIP_REMOVE(&head, n1, entries); + free(n1); + } + /* Faster Skiplist Deletion. */ + n1 = SKIP_FIRST(&head); + while (n1 != NULL) { + n2 = SKIP_NEXT(n1, entries); + free(n1); + n1 = n2; + } + SKIP_INIT(&head); +#endif + return 0; +} diff --git a/include/sl.h b/include/sl.h new file mode 100644 index 0000000..b2eaaeb --- /dev/null +++ b/include/sl.h @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2024 + * Gregory Burd . All rights reserved. + * + * ISC License Permission to use, copy, modify, and/or distribute this software + * for any purpose with or without fee is hereby granted, provided that the + * above copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef _SYS_SKIPLIST_H_ +#define _SYS_SKIPLIST_H_ + +/* + * This file defines a skiplist data structure with a similar API to those + * datastructures defined in FreeBSD's header. + * + * In 1990 William Pugh published: + * - Skiplists: a probabilistic alternative to balanced trees. + * https://www.cl.cam.ac.uk/teaching/2005/Algorithms/skiplists.pdf + * A Skiplist is an ordered data structure providing expected O(Log(n)) lookup, + * insertion, and deletion complexity. + * + * + * For details on the use of these macros, see the queue(3) manual page. + * + * + * SKIP SPLAYED_SKIP + * _HEAD + + + * _HEAD_INITIALIZER + + + * _ENTRY + + + * _INIT + + + * _DISPOSE + + + * _EMPTY + + + * _FIRST + + + * _NEXT + + + * _PREV + + + * _LAST - + + * _FOREACH + + + * _FOREACH_REVERSE + + + * _INSERT + + + * _UPDATE + + + * _BULK_INSERT + + + * _REMOVE + + + * _LOCATE + + + * + */ + +#define SKIPLIST_MACRO_DEBUG 0 +#if SKIPLIST_MACRO_DEBUG +/* Store the last 2 places the element or head was altered */ +struct sl_trace { + char * lastfile; + int lastline; + char * prevfile; + int prevline; +}; + +#define TRACEBUF struct sl_trace trace; +#define TRASHIT(x) do {(x) = (void *)-1;} while (0) + +#define SLD_TRACE_HEAD(head) do { \ + (head)->trace.prevline = (head)->trace.lastline; \ + (head)->trace.prevfile = (head)->trace.lastfile; \ + (head)->trace.lastline = __LINE__; \ + (head)->trace.lastfile = __FILE__; \ +} while (0) + +#define SLD_TRACE_ELEM(elem) do { \ + (elem)->trace.prevline = (elem)->trace.lastline; \ + (elem)->trace.prevfile = (elem)->trace.lastfile; \ + (elem)->trace.lastline = __LINE__; \ + (elem)->trace.lastfile = __FILE__; \ +} while (0) + +#else +#define SLD_TRACE_ELEM(elem) +#define SLD_TRACE_HEAD(head) +#define TRACEBUF +#define TRASHIT(x) +#endif /* QUEUE_MACRO_DEBUG */ + + +/* + * Private, internal API. + */ + +/* NOTE: + * This array is a bit unusual, while values are 0-based the array is allocated + * with space for two more pointers which are used to store max size the array + * can grow to (`array[-2]`) and the length, or number of elements used in the + * array so far (`array[-1]`). + */ +#define ARRAY_ALLOC(var, type, size) do { \ + (size) = (size == 0 ? 254 : size); \ + (var) = (struct type**)calloc(sizeof(struct type*), size + 2); \ + if ((var) != NULL) { \ + *(var)++ = (struct type *)size; \ + *(var)++ = 0; \ + } \ + } while(0) +#define ARRAY_FREE(var) free((var)-2) +#define ARRAY_SIZE(list) (list)[-2] +#define ARRAY_SET_SIZE(list, size) (list)[-2] = (void *)(uintptr_t)(size) +#define ARRAY_LENGTH(list) (list)[-1] +#define ARRAY_SET_LENGTH(list, len) (list)[-1] = (void *)(uintptr_t)(len) +#define ARRAY_FOREACH(var, list) \ + for(unsigned int (var) = 0; (var) < ARRAY_LENGTH(list); (var)++) + + +/* + * Skiplist declarations. + */ +#define SKIP_HEAD(name, type) \ + struct name { \ + size_t level, length, max, fanout; \ + int (*cmp)(struct name *, struct type *, struct type *, void *); \ + void *aux; \ + struct type **slh_head; \ + struct type **slh_tail; \ + TRACEBUF \ + } + +#define SKIP_HEAD_DEFAULT_INITIALIZER(cmp) \ + { 0, 0, 12, 4, cmp, NULL, NULL, NULL } + +#define SKIP_HEAD_INITIALIZER(cmp, max, fanout) \ + { 0, 0, max, fanout, cmp, NULL, NULL, NULL } + +#define SKIP_ENTRY(type) \ + struct { \ + struct type **sle_next; \ + struct type *sle_prev; \ + TRACEBUF \ + } + +/* + * Skip List access methods. + */ +#define SKIP_FIRST(head) ((head)->slh_head) +#define SKIP_NEXT(elm, field) ((elm)->field.sle_next[0]) +#define SKIP_PREV(elm, field) ((elm)->field.sle_prev) +#define SKIP_EMPTY(head) ((head)->slh_head == NULL) + +#if 0 +#define SKIP_FOREACH(var, head, field) \ + for((var) = SKIP_FIRST(head); \ + (var)!= SKIP_END(head); \ + (var) = SKIP_NEXT(var, field)) + +#define SKIP_FOREACH_SAFE(var, head, field, tvar) \ + for ((var) = SKIP_FIRST(head); \ + (var) && ((tvar) = SKIP_NEXT(var, field), 1); \ + (var) = (tvar)) + +#define TAILQ_FOREACH_REVERSE(var, head, headname, field) \ + for((var) = TAILQ_LAST(head, headname); \ + (var) != TAILQ_END(head); \ + (var) = TAILQ_PREV(var, headname, field)) + +#define TAILQ_FOREACH_REVERSE_SAFE(var, head, headname, field, tvar) \ + for ((var) = TAILQ_LAST(head, headname); \ + (var) != TAILQ_END(head) && \ + ((tvar) = TAILQ_PREV(var, headname, field), 1); \ + (var) = (tvar)) +#endif + +/* + * Skip List functions. + */ + +#define SKIP_COMPARATOR(list, type, fn) \ + int __skip_cmp_##type(struct list *head, struct type *a, struct type *b, void *aux) { \ + if (a == b) \ + return 0; \ + if (a == (head)->slh_head || b == (head)->slh_tail) \ + return -1; \ + if (a == (head)->slh_tail || b == (head)->slh_head) \ + return 1; \ + fn } + +#define SKIP_INIT(head, max, fanout, fn) do { \ + (head)->level = 0; \ + (head)->length = 0; \ + (head)->max = max; \ + (head)->fanout = fanout; \ + (head)->cmp = fn; \ + (head)->slh_head = NULL; \ + (head)->slh_tail = NULL; \ + SLD_TRACE_HEAD(head); \ + } while (0) + +#define SKIP_DEFAULT_INIT(head, fn) do { \ + (head)->level = 0; \ + (head)->length = 0; \ + (head)->max = 12; \ + (head)->fanout = 4; \ + (head)->cmp = fn; \ + (head)->slh_head = NULL; \ + (head)->slh_tail = NULL; \ + SLD_TRACE_HEAD(head); \ + } while (0) + + +#define SKIP_ALLOC_NODE(head, var, type, field) do { \ + size_t amt = sizeof(struct type *) * (head)->max; \ + (var) = (struct type *)calloc(1, sizeof(struct type) + amt + 3); \ + if ((var) != NULL) { \ + (var)->field.sle_prev = (struct type *)(var) + sizeof(struct type); \ + (var)->field.sle_next = (struct type **)(var) + sizeof(struct type) + (sizeof(struct type *) * 3); \ + ARRAY_SET_SIZE((var)->field.sle_next, (head)->max); \ + ARRAY_SET_LENGTH((var)->field.sle_next, 0); \ + } \ + } while (0) + +#define __SKIP_TOSS(var, max, fanout) do { \ + (var) = 0; \ + while ((var) + 1 < (max)) { \ + if (rand() % (fanout) == 0) /* NOLINT(*-msc50-cpp) */ \ + (var)++; \ + else \ + break; \ + } \ + } while(0) + +#define SKIP_INSERT(head, type, listelm, field) do { \ + struct type *__elm = SKIP_FIRST(head); \ + unsigned int __i = (head)->level; \ + struct type **__path; \ + if (__elm == NULL) { \ + /* Empty list, setup header and add first element. */ \ + ARRAY_ALLOC((head)->slh_head, type, (head)->max); \ + ARRAY_ALLOC((head)->slh_tail, type, (head)->max); \ + ARRAY_FOREACH(__j, (head)->slh_head) { \ + (head)->slh_head[__j] = (head)->slh_tail; \ + (head)->slh_tail[__j] = (head)->slh_head; \ + } \ + (head)->slh_head[0] = (listelm); \ + (head)->slh_tail[0] = (listelm); \ + (listelm)->field.sle_next[0] = (head)->slh_tail; \ + (listelm)->field.sle_prev = (head)->slh_head; \ + (head)->length = 1; \ + break; \ + } \ + ARRAY_ALLOC(__path, type, (head)->max); \ + if (__path == NULL) break; /* ENOMEM */ \ + /* Find the position in the list where this element belongs. */ \ + do { \ + while(__elm && (head)->cmp((head), __elm, __elm->field.sle_next[__i], (head)->aux) < 0) \ + __elm = __elm->field.sle_next[__i]; \ + __path[__i] = __elm; \ + } while(__i--); \ + __elm = __elm->field.sle_next[0]; \ + if ((head)->cmp((head), __elm, listelm, (head)->aux) == 0) { \ + ARRAY_FREE(__path); \ + break; /* Don't overwrite, to do that use _REPLACE not _INSERT */ \ + } \ + unsigned int __level; \ + __SKIP_TOSS(__level, (head)->max, (head)->fanout); \ + ARRAY_SET_LENGTH((listelm)->field.sle_next, __level); \ + if (__level > (head)->level) { \ + for (__i = (head)->level; __i < __level; __i++) { \ + __path[__i] = (list)->slh_head; \ + } \ + (head)->level = __level; \ + } \ + for (__i = 0; __i < __level; __i++) { \ + (listelm)->field.sle_next[__i] = __path[__i]->field.sle_next[__i];\ + __path[__i]->field.sle_next[__i] = (listelm); \ + } \ + (listelm)->field.sle_prev = __elm->field.sle_prev; \ + ARRAY_FREE(__path); \ + } while (0) + +#if 0 +#define SKIP_REMOVE(head, elm, field) do { \ + if ((elm)->field.le_next != NULL) \ + (elm)->field.le_next->field.le_prev = \ + (elm)->field.le_prev; \ + *(elm)->field.le_prev = (elm)->field.le_next; \ + _Q_INVALIDATE((elm)->field.le_prev); \ + _Q_INVALIDATE((elm)->field.le_next); \ +} while (0) + +#define SKIP_REPLACE(elm, elm2, field) do { \ + if (((elm2)->field.le_next = (elm)->field.le_next) != NULL) \ + (elm2)->field.le_next->field.le_prev = \ + &(elm2)->field.le_next; \ + (elm2)->field.le_prev = (elm)->field.le_prev; \ + *(elm2)->field.le_prev = (elm2); \ + _Q_INVALIDATE((elm)->field.le_prev); \ + _Q_INVALIDATE((elm)->field.le_next); \ +} while (0) +#endif + +#endif /* _SYS_SKIPLIST_H_ */ diff --git a/tests/test.c b/tests/test.c index bf0aaf9..4962c8c 100644 --- a/tests/test.c +++ b/tests/test.c @@ -121,6 +121,29 @@ uint32_key_cmp(sl_node *a, sl_node *b, void *aux) return 0; } +static size_t +__populate_slist(ex_sl_t *slist){ + size_t inserted = 0; + uint32_t n, key; + ex_node_t *node; + + n = munit_rand_int_range(1024, 4196); + while (n--) { + key = munit_rand_int_range(0, (((uint32_t)0) - 1) / 10); + node = (ex_node_t *)calloc(sizeof(ex_node_t), 1); + if (node == NULL) + return MUNIT_ERROR; + sl_init_node(&node->snode); + node->key = key; + node->value = key; + if (sl_insert_nodup(slist, &node->snode) == -1) + continue; /* a random duplicate appeared */ + else + inserted++; + } + return inserted; +} + static void * test_api_setup(const MunitParameter params[], void *user_data) { @@ -157,29 +180,6 @@ test_api_tear_down(void *fixture) free(fixture); } -static size_t -__populate_slist(ex_sl_t *slist){ - size_t inserted = 0; - uint32_t n, key; - ex_node_t *node; - - n = munit_rand_int_range(1024, 4196); - while (n--) { - key = munit_rand_int_range(0, (((uint32_t)0) - 1) / 10); - node = (ex_node_t *)calloc(sizeof(ex_node_t), 1); - if (node == NULL) - return MUNIT_ERROR; - sl_init_node(&node->snode); - node->key = key; - node->value = key; - if (sl_insert_nodup(slist, &node->snode) == -1) - continue; /* a random duplicate appeared */ - else - inserted++; - } - return inserted; -} - static void * test_api_insert_setup(const MunitParameter params[], void *user_data) { @@ -269,7 +269,19 @@ test_api_remove(const MunitParameter params[], void *data) static void * test_api_find_setup(const MunitParameter params[], void *user_data) { - return test_api_setup(params, user_data); + sl_raw *slist = (sl_raw *)test_api_setup(params, user_data); + ex_node_t *node; + for (int i = 1; i <= 100; ++i) { + node = calloc(sizeof(ex_node_t), 1); + if (node == NULL) + return NULL; + node = (ex_node_t *)calloc(sizeof(ex_node_t), 1); + sl_init_node(&node->snode); + node->key = i; + node->value = i; + sl_insert(slist, &node->snode); + } + return (void *)slist; } static void test_api_find_tear_down(void *fixture) @@ -281,7 +293,19 @@ test_api_find(const MunitParameter params[], void *data) { sl_raw *slist = (sl_raw *)data; (void)params; - (void)slist; + + /* find equal every value */ + assert_ptr_not_null(data); + for (int i = 1; i <= 100; i++) { + ex_node_t query; + query.key = i; + sl_node *cursor = sl_find(slist, &query.snode); + assert_ptr_not_null(cursor); + ex_node_t *entry = sl_get_entry(cursor, ex_node_t, snode); + assert_uint32(entry->key, ==, i); + } + + /* */ return MUNIT_OK; } @@ -420,3 +444,5 @@ main(int argc, char *argv[MUNIT_ARRAY_PARAM(argc + 1)]) struct user_data info; return munit_suite_main(&main_test_suite, (void *)&info, argc, argv); } + +/* ARGS: --no-fork --seed 8675309 */