Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions bench/bench_set_add_dup.c
Original file line number Diff line number Diff line change
@@ -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 <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ttak/container/set.h>

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;
}
32 changes: 32 additions & 0 deletions internal/ttak/table_internal.h
Original file line number Diff line number Diff line change
@@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <ttak/ht/table.h>

/**
* @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
8 changes: 4 additions & 4 deletions src/container/set.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <ttak/container/set.h>
#include "../../internal/ttak/table_internal.h"
#include <stddef.h>

/**
Expand All @@ -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.
Expand All @@ -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);
}

/**
Expand Down
25 changes: 22 additions & 3 deletions src/ht/table.c
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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;
Expand All @@ -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) {
Expand Down
Loading