diff --git a/bench/bench_set_add_dup.c b/bench/bench_set_add_dup.c new file mode 100644 index 00000000..36de6973 --- /dev/null +++ b/bench/bench_set_add_dup.c @@ -0,0 +1,65 @@ +/** + * A/B benchmark for ttak_set_add duplicate-heavy insert throughput. + * + * Prefills a set with PREFILL distinct keys, then performs ITERS add + * attempts where 90% of the keys already exist (duplicates) and 10% are + * rotating fresh keys. Old ttak_set_add performs a get probe plus a put + * probe per attempt; the new implementation resolves each attempt with a + * single probe. + */ +#define _XOPEN_SOURCE 700 +#include +#include +#include +#include +#include +#include + +static double now_ns(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec * 1e9 + (double)ts.tv_nsec; +} + +#define PREFILL 100000u +#define ITERS 1000000u +#define REPEAT 5 + +static int cmp_keys(const void *a, const void *b) { + return strcmp((const char *)a, (const char *)b); +} + +int main(void) { + char **pool = malloc((PREFILL + PREFILL / 4) * sizeof(char *)); + for (size_t i = 0; i < PREFILL + PREFILL / 4; i++) { + pool[i] = malloc(24); + snprintf(pool[i], 24, "key-%zu", i); + } + + double best = 1e30; + for (int r = 0; r < REPEAT; r++) { + uint64_t now = 0; + ttak_set_t set; + ttak_set_init(&set, PREFILL * 2, NULL, cmp_keys, NULL); + + for (size_t i = 0; i < PREFILL; i++) + ttak_table_put(&set.table, pool[i], strlen(pool[i]) + 1, (void *)1, now); + + double t0 = now_ns(); + for (size_t i = 0; i < ITERS; i++) { + /* 90% duplicates from the prefilled range, 10% rotating fresh keys */ + size_t idx = (i % 10 == 9) ? PREFILL + ((i / 10) % (PREFILL / 4)) : (i % PREFILL); + ttak_set_add(&set, pool[idx], strlen(pool[idx]) + 1, now); + } + double t1 = now_ns(); + + double ns = t1 - t0; + if (ns < best) best = ns; + ttak_set_destroy(&set, now); + } + + printf("best of %d: %.0f ns total, %.2f ns/add\n", REPEAT, best, best / ITERS); + for (size_t i = 0; i < PREFILL + PREFILL / 4; i++) free(pool[i]); + free(pool); + return 0; +} diff --git a/internal/ttak/table_internal.h b/internal/ttak/table_internal.h new file mode 100644 index 00000000..b6e924a7 --- /dev/null +++ b/internal/ttak/table_internal.h @@ -0,0 +1,32 @@ +/** + * @file table_internal.h + * @brief Internal hash table entry points not exposed in the public API. + */ + +#ifndef TTAK_TABLE_INTERNAL_H +#define TTAK_TABLE_INTERNAL_H + +#include +#include +#include +#include + +/** + * @brief Insert a key/value pair only if the key is not already present. + * + * Performs a single probe: on a key match the table is left untouched and + * false is returned; on an empty slot the pair is inserted and true is + * returned. Unlike ttak_table_put(), an existing value is never replaced + * (and its val_free destructor is never invoked). + * + * @param table Table instance to update. + * @param key Key to store. + * @param key_len Length of the key payload. + * @param value Value associated with the key. + * @param now Timestamp for safe memory access. + * @return true if the pair was inserted, false if the key already existed. + */ +bool ttak_table_put_if_absent(ttak_table_t *table, void *key, size_t key_len, + void *value, uint64_t now); + +#endif // TTAK_TABLE_INTERNAL_H diff --git a/src/container/set.c b/src/container/set.c index c2a4e4a5..d5b29209 100644 --- a/src/container/set.c +++ b/src/container/set.c @@ -1,4 +1,5 @@ #include +#include "../../internal/ttak/table_internal.h" #include /** @@ -22,7 +23,8 @@ void ttak_set_init(ttak_set_t *set, size_t capacity, * @brief Insert a key into the set. * * Stores a placeholder value so lookups can distinguish between "missing" and - * "present with NULL value". + * "present with NULL value". Duplicate adds are no-ops resolved with a single + * hash-table probe. * * @param set Set to update. * @param key Key to store. @@ -31,9 +33,7 @@ void ttak_set_init(ttak_set_t *set, size_t capacity, */ void ttak_set_add(ttak_set_t *set, void *key, size_t key_len, uint64_t now) { if (!set) return; - if (ttak_table_get(&set->table, key, key_len, now) != NULL) { - ttak_table_put(&set->table, key, key_len, (void*)1, now); - } + ttak_table_put_if_absent(&set->table, key, key_len, (void*)1, now); } /** diff --git a/src/ht/table.c b/src/ht/table.c index 2f26e564..95ed703f 100644 --- a/src/ht/table.c +++ b/src/ht/table.c @@ -80,8 +80,15 @@ static void ttak_table_rehash(ttak_table_t *table, uint64_t now, size_t new_cap) table->tombstones = 0; } -void ttak_table_put(ttak_table_t *table, void *key, size_t key_len, void *value, uint64_t now) { - if (!table || !table->ctrls) return; +/** + * @brief Single-probe insert core shared by ttak_table_put() and + * ttak_table_put_if_absent(). + * + * @param replace When true an existing key has its value replaced; when + * false the table is left untouched on a key match. + * @return true if a new slot was occupied, false on an existing-key hit. + */ +static bool table_insert(ttak_table_t *table, void *key, size_t key_len, void *value, uint64_t now, bool replace) { /* Tombstones degrade probe chains just like live entries, so they count * toward the load factor. If live entries alone are under 70% the table * is mostly tombstones: rehash at the same capacity to bound memory. */ @@ -98,10 +105,11 @@ void ttak_table_put(ttak_table_t *table, void *key, size_t key_len, void *value, if (table->ctrls[idx] == DELETED) { if (first_tomb == SIZE_MAX) first_tomb = idx; } else if (table->key_cmp(table->entries[idx].key, key) == 0) { + if (!replace) return false; if (table->val_free && table->entries[idx].value) table->val_free(table->entries[idx].value); table->entries[idx].value = value; table->entries[idx].key_len = key_len; - return; + return false; } idx++; if (idx >= table->capacity + MAX_PROBE - 1) idx = 0; @@ -117,6 +125,17 @@ void ttak_table_put(ttak_table_t *table, void *key, size_t key_len, void *value, table->entries[idx].value = value; table->entries[idx].key_len = key_len; table->size++; + return true; +} + +void ttak_table_put(ttak_table_t *table, void *key, size_t key_len, void *value, uint64_t now) { + if (!table || !table->ctrls) return; + (void)table_insert(table, key, key_len, value, now, true); +} + +bool ttak_table_put_if_absent(ttak_table_t *table, void *key, size_t key_len, void *value, uint64_t now) { + if (!table || !table->ctrls) return false; + return table_insert(table, key, key_len, value, now, false); } void *ttak_table_get(ttak_table_t *table, const void *key, size_t key_len, uint64_t now) {