Download
225 lines 6.8 KiBCModified 2022-12-23 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
// Code for interning chunks of data in a way compatible with the Boehm garbage collector
#include <assert.h>
#include <gc.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/random.h>

#include "intern.h"

typedef struct intern_entry_s {
    char *mem; // Pointer to interned memory, may be zeroed out by GC
    size_t len;
    // Memoized hash value (we can't rehash if GC collects the memory)
    int home; // hash value & (intern_capacity-1)
    struct intern_entry_s *next;
} intern_entry_t;

static intern_entry_t *interned = NULL, *lastfree = NULL;
static size_t intern_capacity = 0, intern_count = 0;

// Cache most recently used strings to prevent GC churn
#ifndef N_RECENTLY_USED
#define N_RECENTLY_USED 256
#endif
static const char **recently_used = NULL;
static int recently_used_i = 0;

// Special case for zero-width interns, no need to track it in the hash map:
static char empty = '\0';
static void intern_insert(char *mem, size_t len);
static void rehash(void);

#ifdef NOSIPHASH
static size_t initial_hash = 0;
void randomize_hash(void) {
    getrandom(&initial_hash, sizeof(initial_hash), 0);
    rehash();
}

static inline size_t hash_mem(const char *mem, size_t len)
{
    if (__builtin_expect(len == 0, 0)) return 0;
    register unsigned char *p = (unsigned char *)mem;
    register size_t h = (size_t)(*p << 7) ^ len ^ initial_hash;
    register size_t i = len > 128 ? 128 : len;
    while (i--)
        h = (1000003*h) ^ *p++;
    if (h == 0) h = 1234567;
    return h;
}
#else
#include "SipHash/halfsiphash.h"
#include "SipHash/halfsiphash.c"

static uint8_t hash_random_vector[16] = {42,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
void randomize_hash(void) {
    getrandom(hash_random_vector, sizeof(hash_random_vector), 0);
    rehash();
}

static size_t hash_mem(const char *mem, size_t len)
{
    size_t hash;
    halfsiphash(mem, len, hash_random_vector, (uint8_t*)&hash, sizeof(hash));
    return hash;
}
#endif

static void rehash(void)
{
    // Calculate new size to be min(16, next_highest_power_of_2(2 * num_entries))
    size_t new_size = 0;
    for (size_t i = 0; i < intern_capacity; i++)
        if (interned[i].mem)
            new_size += 2;
    if (new_size < 16) new_size = 16;
    size_t pow2 = 1;
    while (pow2 < new_size) pow2 <<= 1;
    new_size = pow2;

    intern_entry_t *old = interned;
    size_t old_capacity = intern_capacity;
    interned = GC_MALLOC_UNCOLLECTABLE(new_size*sizeof(intern_entry_t));
    intern_capacity = new_size;
    intern_count = 0;
    lastfree = &interned[new_size - 1];
    // Rehash:
    if (old) {
        for (size_t i = 0; i < old_capacity; i++) {
            if (old[i].mem)
                GC_unregister_disappearing_link((void**)&old[i].mem);
            if (old[i].mem)
                intern_insert(GC_REVEAL_POINTER(old[i].mem), old[i].len);
        }
        GC_FREE(old);
    }
}

static char *lookup(const char *mem, size_t len)
{
    if (intern_capacity == 0 || !mem) return NULL;
    int i = (int)(hash_mem(mem, len) & (size_t)(intern_capacity-1));
    for (intern_entry_t *e = &interned[i]; e; e = e->next) {
        if (e->mem && e->len == len && memcmp(mem, GC_REVEAL_POINTER(e->mem), len) == 0)
            return GC_REVEAL_POINTER(e->mem);
    }
    return NULL;
}

static void intern_insert(char *mem, size_t len)
{
    if (!mem || len == 0) return;

    // Grow the storage if necessary
    if ((intern_count + 1) >= intern_capacity)
        rehash();

    int i = (int)(hash_mem(mem, len) & (size_t)(intern_capacity-1));
    intern_entry_t *collision = &interned[i];
    if (!collision->mem && !collision->next) { // No collision
        collision->mem = (char*)GC_HIDE_POINTER(mem);
        GC_general_register_disappearing_link((void**)&collision->mem, mem);
        collision->len = len;
        collision->home = i;
        ++intern_count;
        return;
    }

    while (lastfree >= interned && lastfree->len)
        --lastfree;

    if (collision->home == i) { // Collision with element in its main position
        lastfree->mem = (char*)GC_HIDE_POINTER(mem);
        GC_general_register_disappearing_link((void**)&lastfree->mem, mem);
        lastfree->len = len;
        lastfree->home = i;
        lastfree->next = collision->next;
        collision->next = lastfree;
    } else { // Collision with element in a makeshift spot
        // Find the previous linked list node:
        intern_entry_t *prev = &interned[collision->home];
        while (prev->next != collision)
            prev = prev->next;
        memcpy(lastfree, collision, sizeof(intern_entry_t));
        GC_move_disappearing_link((void**)&collision->mem, (void**)&lastfree->mem);
        prev->next = lastfree;
        collision->mem = (char*)GC_HIDE_POINTER(mem);
        GC_general_register_disappearing_link((void**)&collision->mem, mem);
        collision->len = len;
        collision->home = i;
        collision->next = NULL;
    }
    ++intern_count;
}

const void *intern_bytes(const void *bytes, size_t len)
{
    if (!bytes) return NULL;
    if (len == 0) return &empty;
    const char *intern = lookup(bytes, len);
    if (!intern) {
        // GC_MALLOC() means this may contain pointers to other stuff to keep alive in GC
        char *tmp = GC_MALLOC(sizeof(size_t) + len + 1);
        *(size_t*)tmp = len;
        tmp += sizeof(size_t);
        memcpy(tmp, bytes, len);
        tmp[len] = '\0';
        intern_insert(tmp, len);
        intern = tmp;
    }
    if (!recently_used)
        recently_used = GC_MALLOC_UNCOLLECTABLE(sizeof(char*)*N_RECENTLY_USED);
    recently_used[recently_used_i] = intern;
    recently_used_i = (recently_used_i + 1) & (N_RECENTLY_USED-1);
    return intern;
}

size_t intern_len(const char *str)
{
    istr_t istr = intern_str(str);
    return ((size_t*)istr)[-1];
}

istr_t intern_str(const char *str)
{
    return intern_strn(str, strlen(str));
}

istr_t intern_strn(const char *str, size_t len)
{
    if (!str) return NULL;
    if (len == 0) return &empty;
    istr_t intern = lookup(str, len);
    if (!intern) {
        // GC_MALLOC_ATOMIC() means this memory doesn't need to be scanned by the GC
        char *tmp = GC_MALLOC_ATOMIC(sizeof(size_t) + len + 1);
        *(size_t*)tmp = len;
        tmp += sizeof(size_t);
        memcpy(tmp, str, len);
        tmp[len] = '\0';
        intern_insert(tmp, len);
        intern = tmp;
    }
    if (!recently_used)
        recently_used = GC_MALLOC_UNCOLLECTABLE(sizeof(char*)*N_RECENTLY_USED);
    recently_used[recently_used_i] = intern;
    recently_used_i = (recently_used_i + 1) & (N_RECENTLY_USED-1);
    return intern;
}

istr_t intern_strf(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    char *tmp = NULL;
    int len = vasprintf(&tmp, fmt, args);
    if (len < 0) return NULL;
    va_end(args);
    istr_t ret = intern_strn(tmp, (size_t)len);
    free(tmp);
    return ret;
}