/** * \file * A mostly concurrent hashtable * * Author: * Rodrigo Kumpera (kumpera@gmail.com) * * (C) 2014 Xamarin */ #include "mono-conc-hashtable.h" #include /* Configuration knobs. */ #define INITIAL_SIZE 32 #define LOAD_FACTOR 0.75f #define TOMBSTONE ((gpointer)(ssize_t)-1) typedef struct { gpointer key; gpointer value; } key_value_pair; typedef struct { int table_size; key_value_pair *kvs; } conc_table; struct _MonoConcurrentHashTable { volatile conc_table *table; /* goes to HP0 */ GHashFunc hash_func; GEqualFunc equal_func; int element_count; int overflow_count; GDestroyNotify key_destroy_func; GDestroyNotify value_destroy_func; }; static conc_table* conc_table_new (int size) { conc_table *res = g_new (conc_table, 1); res->table_size = size; res->kvs = g_new0 (key_value_pair, size); return res; } static void conc_table_free (gpointer ptr) { conc_table *table = (conc_table *)ptr; g_free (table->kvs); g_free (table); } static void conc_table_lf_free (conc_table *table) { mono_thread_hazardous_try_free (table, conc_table_free); } /* A common problem with power of two hashtables is that it leads of bad clustering when dealing with aligned numbers. The solution here is to mix the bits from two primes plus the hash itself, it produces a better spread than just the numbers. */ static MONO_ALWAYS_INLINE int mix_hash (int hash) { return ((hash * 215497) >> 16) ^ (hash * 1823231 + hash); } static MONO_ALWAYS_INLINE void insert_one_local (conc_table *table, GHashFunc hash_func, gpointer key, gpointer value) { key_value_pair *kvs = table->kvs; int table_mask = table->table_size - 1; int hash = mix_hash (hash_func (key)); int i = hash & table_mask; while (table->kvs [i].key) i = (i + 1) & table_mask; kvs [i].key = key; kvs [i].value = value; } /* LOCKING: Must be called holding hash_table->mutex */ static void expand_table (MonoConcurrentHashTable *hash_table) { conc_table *old_table = (conc_table*)hash_table->table; conc_table *new_table = conc_table_new (old_table->table_size * 2); key_value_pair *kvs = old_table->kvs; int i; for (i = 0; i < old_table->table_size; ++i) { if (kvs [i].key && kvs [i].key != TOMBSTONE) insert_one_local (new_table, hash_table->hash_func, kvs [i].key, kvs [i].value); } mono_memory_barrier (); hash_table->table = new_table; hash_table->overflow_count = (int)(new_table->table_size * LOAD_FACTOR); conc_table_lf_free (old_table); } MonoConcurrentHashTable* mono_conc_hashtable_new (GHashFunc hash_func, GEqualFunc key_equal_func) { MonoConcurrentHashTable *res = g_new0 (MonoConcurrentHashTable, 1); res->hash_func = hash_func ? hash_func : g_direct_hash; res->equal_func = key_equal_func; // res->equal_func = g_direct_equal; res->table = conc_table_new (INITIAL_SIZE); res->element_count = 0; res->overflow_count = (int)(INITIAL_SIZE * LOAD_FACTOR); return res; } MonoConcurrentHashTable* mono_conc_hashtable_new_full (GHashFunc hash_func, GEqualFunc key_equal_func, GDestroyNotify key_destroy_func, GDestroyNotify value_destroy_func) { MonoConcurrentHashTable *res = mono_conc_hashtable_new (hash_func, key_equal_func); res->key_destroy_func = key_destroy_func; res->value_destroy_func = value_destroy_func; return res; } void mono_conc_hashtable_destroy (MonoConcurrentHashTable *hash_table) { if (hash_table->key_destroy_func || hash_table->value_destroy_func) { int i; conc_table *table = (conc_table*)hash_table->table; key_value_pair *kvs = table->kvs; for (i = 0; i < table->table_size; ++i) { if (kvs [i].key && kvs [i].key != TOMBSTONE) { if (hash_table->key_destroy_func) (hash_table->key_destroy_func) (kvs [i].key); if (hash_table->value_destroy_func) (hash_table->value_destroy_func) (kvs [i].value); } } } conc_table_free ((gpointer)hash_table->table); g_free (hash_table); } gpointer mono_conc_hashtable_lookup (MonoConcurrentHashTable *hash_table, gpointer key) { MonoThreadHazardPointers* hp; conc_table *table; int hash, i, table_mask; key_value_pair *kvs; hash = mix_hash (hash_table->hash_func (key)); hp = mono_hazard_pointer_get (); retry: table = (conc_table *)mono_get_hazardous_pointer ((gpointer volatile*)&hash_table->table, hp, 0); table_mask = table->table_size - 1; kvs = table->kvs; i = hash & table_mask; if (G_LIKELY (!hash_table->equal_func)) { while (kvs [i].key) { if (key == kvs [i].key) { gpointer value; /* The read of keys must happen before the read of values */ mono_memory_barrier (); value = kvs [i].value; /* FIXME check for NULL if we add suppport for removal */ mono_hazard_pointer_clear (hp, 0); return value; } i = (i + 1) & table_mask; } } else { GEqualFunc equal = hash_table->equal_func; while (kvs [i].key) { if (kvs [i].key != TOMBSTONE && equal (key, kvs [i].key)) { gpointer value; /* The read of keys must happen before the read of values */ mono_memory_barrier (); value = kvs [i].value; /* We just read a value been deleted, try again. */ if (G_UNLIKELY (!value)) goto retry; mono_hazard_pointer_clear (hp, 0); return value; } i = (i + 1) & table_mask; } } /* The table might have expanded and the value is now on the newer table */ mono_memory_barrier (); if (hash_table->table != table) goto retry; mono_hazard_pointer_clear (hp, 0); return NULL; } /** * mono_conc_hashtable_remove: * Remove a value from the hashtable. Requires external locking * \returns the old value if \p key is already present or NULL */ gpointer mono_conc_hashtable_remove (MonoConcurrentHashTable *hash_table, gpointer key) { conc_table *table; key_value_pair *kvs; int hash, i, table_mask; g_assert (key != NULL && key != TOMBSTONE); hash = mix_hash (hash_table->hash_func (key)); table = (conc_table*)hash_table->table; kvs = table->kvs; table_mask = table->table_size - 1; i = hash & table_mask; if (!hash_table->equal_func) { for (;;) { if (!kvs [i].key) { return NULL; /*key not found*/ } if (key == kvs [i].key) { gpointer value = kvs [i].value; kvs [i].value = NULL; mono_memory_barrier (); kvs [i].key = TOMBSTONE; --hash_table->element_count; if (hash_table->key_destroy_func != NULL) (*hash_table->key_destroy_func) (key); if (hash_table->value_destroy_func != NULL) (*hash_table->value_destroy_func) (value); return value; } i = (i + 1) & table_mask; } } else { GEqualFunc equal = hash_table->equal_func; for (;;) { if (!kvs [i].key) { return NULL; /*key not found*/ } if (kvs [i].key != TOMBSTONE && equal (key, kvs [i].key)) { gpointer old_key = kvs [i].key; gpointer value = kvs [i].value; kvs [i].value = NULL; mono_memory_barrier (); kvs [i].key = TOMBSTONE; if (hash_table->key_destroy_func != NULL) (*hash_table->key_destroy_func) (old_key); if (hash_table->value_destroy_func != NULL) (*hash_table->value_destroy_func) (value); return value; } i = (i + 1) & table_mask; } } } /** * mono_conc_hashtable_insert: * Insert a value into the hashtable. Requires external locking. * \returns the old value if \p key is already present or NULL */ gpointer mono_conc_hashtable_insert (MonoConcurrentHashTable *hash_table, gpointer key, gpointer value) { conc_table *table; key_value_pair *kvs; int hash, i, table_mask; g_assert (key != NULL && key != TOMBSTONE); g_assert (value != NULL); hash = mix_hash (hash_table->hash_func (key)); if (hash_table->element_count >= hash_table->overflow_count) expand_table (hash_table); table = (conc_table*)hash_table->table; kvs = table->kvs; table_mask = table->table_size - 1; i = hash & table_mask; if (!hash_table->equal_func) { for (;;) { if (!kvs [i].key || kvs [i].key == TOMBSTONE) { kvs [i].value = value; /* The write to values must happen after the write to keys */ mono_memory_barrier (); kvs [i].key = key; ++hash_table->element_count; return NULL; } if (key == kvs [i].key) { gpointer value = kvs [i].value; return value; } i = (i + 1) & table_mask; } } else { GEqualFunc equal = hash_table->equal_func; for (;;) { if (!kvs [i].key || kvs [i].key == TOMBSTONE) { kvs [i].value = value; /* The write to values must happen after the write to keys */ mono_memory_barrier (); kvs [i].key = key; ++hash_table->element_count; return NULL; } if (equal (key, kvs [i].key)) { gpointer value = kvs [i].value; return value; } i = (i + 1) & table_mask; } } } /** * mono_conc_hashtable_foreach: * Calls \p func for each value in the hashtable. Requires external locking. */ void mono_conc_hashtable_foreach (MonoConcurrentHashTable *hash_table, GHFunc func, gpointer userdata) { int i; conc_table *table = (conc_table*)hash_table->table; key_value_pair *kvs = table->kvs; for (i = 0; i < table->table_size; ++i) { if (kvs [i].key && kvs [i].key != TOMBSTONE) { func (kvs [i].key, kvs [i].value, userdata); } } } /** * mono_conc_hashtable_foreach_steal: * * Calls @func for each entry in the hashtable, if @func returns true, remove from the hashtable. Requires external locking. * Same semantics as g_hash_table_foreach_steal. */ void mono_conc_hashtable_foreach_steal (MonoConcurrentHashTable *hash_table, GHRFunc func, gpointer userdata) { int i; conc_table *table = (conc_table*)hash_table->table; key_value_pair *kvs = table->kvs; for (i = 0; i < table->table_size; ++i) { if (kvs [i].key && kvs [i].key != TOMBSTONE) { if (func (kvs [i].key, kvs [i].value, userdata)) { kvs [i].value = NULL; mono_memory_barrier (); kvs [i].key = TOMBSTONE; --hash_table->element_count; } } } }