From 95f94ae25852cd9bbc1bafd23234cf3ff4e02ffa Mon Sep 17 00:00:00 2001 From: Anton Angelov <16073265+antonangeloff@users.noreply.github.com> Date: Mon, 2 Mar 2026 06:31:35 +0200 Subject: [PATCH] refactor: SwissTable implementation for `ht` (#5860) * Add ht benchmarks * Initial implementation * Finalize native per-group lookup support * Lookup SSE2 implementation * Improve hashing * Add support for custom elem_size * Avoid double h2 hashing when reserving slot * Make custom elem_size support conditional * Fix issues with bitwise and default lookup implementations * Implement deletion trick optimization * Track growth_size instead of deleted_slots * Refactor SDB to access ht via API instead of internals * Modify SDB tests which rely on hashtable order * Fix SDB build warnings * Fix bug with finding next power of two * foreach_kv to return a bool result * Change SDB diff order expected by serialize_analysis unit test * Fix bug in the bitwise lookup implementation * Remove second call to rz_core_init() which causes memory leaks * Update some regression tests to accept reordered output * Adapt ht clear to new implementation * Use fini_kv_pair and fix 1 potential leak on malloc failure * Fix cmd/types test after merge * Avoid second call to calsize_key and avoid iter leaks on malloc failure * Improve hash distribution * Extend benchmark suite * Fix bug with string hashing * Branchless write to mirrored ctrl bytes * Simplify string hash and remove potential UB * Move RZ_PREFETCH macro to rz_types.h * Add SSE2 discovery in Meson * Forward SDB string hash function to ht string hash * Try to revert test_cpu_profiles() to avoid relying on a baked SDB file * Revert SDB/CDB hash function change * Fix SDB reference to HT hash function instead of CDB hash * Change calloc to malloc * Avoid storing/checking key_len and key_value if they are ut64 * Improve string hash function * Rename default hash functions * Improve bench code * linter.yml: set clang-path to point to llvm-18 --- .github/workflows/linter.yml | 4 +- librz/include/rz_types.h | 12 + librz/include/rz_userconf.h.in | 1 + librz/include/rz_util/ht_inc.h | 230 ++++-- librz/util/ht/ht_inc.c | 1015 +++++++++++++++++++-------- librz/util/ht/ht_sp.c | 4 +- librz/util/ht/ht_ss.c | 4 +- librz/util/ht/ht_su.c | 4 +- librz/util/ht/ht_up.c | 2 +- librz/util/sdb/src/sdb.c | 63 +- librz/util/sdb/src/sdb.h | 2 +- librz/util/sdb/src/sdbht.c | 33 + librz/util/sdb/src/sdbht.h | 4 + meson.build | 7 + test/bench/bench_ht.c | 443 ++++++++++++ test/bench/bench_utils.h | 12 + test/bench/meson.build | 1 + test/db/analysis/x86_64 | 4 +- test/db/archos/linux-x64/debuginfod | 2 +- test/db/cmd/cmd_ac | 4 +- test/db/cmd/cmd_avD | 20 +- test/db/cmd/cmd_avx | 8 +- test/db/cmd/cmd_ax | 2 +- test/db/cmd/cmd_egg | 2 +- test/db/cmd/cmd_flags | 46 +- test/db/cmd/cmd_graph | 6 +- test/db/cmd/cmd_list | 8 +- test/db/cmd/types | 58 +- test/db/formats/le | 1 + test/db/formats/pe/pe | 10 +- test/db/tools/rz_hash | 32 +- test/unit/test_annotated_code.c | 4 +- test/unit/test_ht.c | 95 +++ test/unit/test_sdb_diff.c | 24 +- test/unit/test_sdb_sdb.c | 2 +- test/unit/test_serialize_analysis.c | 2 +- 36 files changed, 1617 insertions(+), 554 deletions(-) create mode 100644 test/bench/bench_ht.c diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 1a0ab51f1a..10de030c7e 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -89,7 +89,7 @@ jobs: run: | sudo pip install meson ninja sudo apt update - sudo apt install libclang-14-dev + sudo apt install libclang-18-dev - name: Build rizin working-directory: rizin run: | @@ -98,7 +98,7 @@ jobs: - name: Run rz-bindgen linter run: | python3 rz-bindgen/src/lint.py \ - --clang-path "/usr/lib/llvm-14/lib" \ + --clang-path "/usr/lib/llvm-18/lib" \ --clang-args "-resource-dir=$(clang -print-resource-dir) " \ --rizin-path rizin diff --git a/librz/include/rz_types.h b/librz/include/rz_types.h index 3b9a513bd9..7e02adf118 100644 --- a/librz/include/rz_types.h +++ b/librz/include/rz_types.h @@ -611,6 +611,18 @@ typedef enum { } #endif +#if defined(__GNUC__) || defined(__clang__) +#define RZ_PREFETCH(addr) __builtin_prefetch((addr), 0, 3) +#elif defined(_MSC_VER) +#include +/** + * \def Prefetch data from a certain memory address to memory cache + */ +#define RZ_PREFETCH(addr) _mm_prefetch((const char *)(addr), _MM_HINT_T0) +#else +#define RZ_PREFETCH(addr) ((void)0) +#endif + static inline void rz_run_call1(void *fcn, void *arg1) { ((void (*)(void *))(fcn))(arg1); } diff --git a/librz/include/rz_userconf.h.in b/librz/include/rz_userconf.h.in index 04213f301c..c456719e36 100644 --- a/librz/include/rz_userconf.h.in +++ b/librz/include/rz_userconf.h.in @@ -58,6 +58,7 @@ #define HAVE___BUILTIN_CTZLL @HAVE___BUILTIN_CTZLL@ #define HAVE_POSIX_MEMALIGN @HAVE_POSIX_MEMALIGN@ #define HAVE__ALIGNED_MALLOC @HAVE__ALIGNED_MALLOC@ +#define HAVE_SSE2 @HAVE_SSE2@ #define HAVE_HEADER_LINUX_ASHMEM_H @HAVE_HEADER_LINUX_ASHMEM_H@ #define HAVE_HEADER_SYS_SHM_H @HAVE_HEADER_SYS_SHM_H@ diff --git a/librz/include/rz_util/ht_inc.h b/librz/include/rz_util/ht_inc.h index c022f619bb..196bcc6c9d 100644 --- a/librz/include/rz_util/ht_inc.h +++ b/librz/include/rz_util/ht_inc.h @@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: 2016-2018 pancake // SPDX-FileCopyrightText: 2016-2018 ret2libc // SPDX-FileCopyrightText: 2024 pelijah +// SPDX-FileCopyrightText: 2026 Anton Angelov // SPDX-License-Identifier: BSD-3-Clause #include @@ -17,70 +18,154 @@ #undef VALUE_TYPE #undef KEY_TO_HASH #undef HT_NULL_VALUE +#undef VARIABLE_KEY_LEN +#undef VARIABLE_VALUE_LEN #if HT_TYPE == 1 // Hash table HtPP that has void* as key and void* as value -#define HtName_(name) name##PP -#define Ht_(name) ht_pp_##name -#define HT_(name) HtPP##name -#define KEY_TYPE void * -#define VALUE_TYPE void * -#define KEY_TO_HASH(x) ((ut32)(uintptr_t)(x)) -#define HT_NULL_VALUE NULL +#define HtName_(name) name##PP +#define Ht_(name) ht_pp_##name +#define HT_(name) HtPP##name +#define KEY_TYPE void * +#define VALUE_TYPE void * +#define KEY_TO_HASH(key, key_size) (ht_default_hash_ut64((uintptr_t)(key))) +#define HT_NULL_VALUE NULL +#define VARIABLE_KEY_LEN +#define VARIABLE_VALUE_LEN #elif HT_TYPE == 2 -// Hash table HtPU that has void* as key and ut64 as value -#define HtName_(name) name##UP -#define Ht_(name) ht_up_##name -#define HT_(name) HtUP##name -#define KEY_TYPE ut64 -#define VALUE_TYPE void * -#define KEY_TO_HASH(x) ((ut32)(x)) -#define HT_NULL_VALUE 0 +// Hash table HtUP that has void* as key and ut64 as value +#define HtName_(name) name##UP +#define Ht_(name) ht_up_##name +#define HT_(name) HtUP##name +#define KEY_TYPE ut64 +#define VALUE_TYPE void * +#define KEY_TO_HASH(key, key_size) (ht_default_hash_ut64((ut64)(key))) +#define HT_NULL_VALUE 0 +#define VARIABLE_VALUE_LEN #elif HT_TYPE == 3 // Hash table HtUU that has ut64 as key and ut64 as value -#define HtName_(name) name##UU -#define Ht_(name) ht_uu_##name -#define HT_(name) HtUU##name -#define KEY_TYPE ut64 -#define VALUE_TYPE ut64 -#define KEY_TO_HASH(x) ((ut32)(x)) -#define HT_NULL_VALUE 0 +#define HtName_(name) name##UU +#define Ht_(name) ht_uu_##name +#define HT_(name) HtUU##name +#define KEY_TYPE ut64 +#define VALUE_TYPE ut64 +#define KEY_TO_HASH(key, key_size) (ht_default_hash_ut64((ut64)(key))) +#define HT_NULL_VALUE 0 #elif HT_TYPE == 4 // Hash table HtPU that has void* as key and ut64 as value -#define HtName_(name) name##PU -#define Ht_(name) ht_pu_##name -#define HT_(name) HtPU##name -#define KEY_TYPE void * -#define VALUE_TYPE ut64 -#define KEY_TO_HASH(x) ((ut32)(uintptr_t)(x)) -#define HT_NULL_VALUE 0 +#define HtName_(name) name##PU +#define Ht_(name) ht_pu_##name +#define HT_(name) HtPU##name +#define KEY_TYPE void * +#define VALUE_TYPE ut64 +#define KEY_TO_HASH(key, key_size) (ht_default_hash_ut64((uintptr_t)(key))) +#define HT_NULL_VALUE 0 +#define VARIABLE_KEY_LEN #elif HT_TYPE == 5 // Hash table HtSP that has C-string as key and void* as value -#define HtName_(name) name##SP -#define Ht_(name) ht_sp_##name -#define HT_(name) HtSP##name -#define KEY_TYPE char * -#define VALUE_TYPE void * -#define KEY_TO_HASH(x) ((ut32)(uintptr_t)(x)) -#define HT_NULL_VALUE NULL +#define HtName_(name) name##SP +#define Ht_(name) ht_sp_##name +#define HT_(name) HtSP##name +#define KEY_TYPE char * +#define VALUE_TYPE void * +#define KEY_TO_HASH(key, key_size) (ht_default_hash_string(key, key_size)) +#define HT_NULL_VALUE NULL +#define VARIABLE_KEY_LEN +#define VARIABLE_VALUE_LEN #elif HT_TYPE == 6 // Hash table HtSS that has C-string as key and C-string as value -#define HtName_(name) name##SS -#define Ht_(name) ht_ss_##name -#define HT_(name) HtSS##name -#define KEY_TYPE char * -#define VALUE_TYPE char * -#define KEY_TO_HASH(x) ((ut32)(uintptr_t)(x)) -#define HT_NULL_VALUE NULL +#define HtName_(name) name##SS +#define Ht_(name) ht_ss_##name +#define HT_(name) HtSS##name +#define KEY_TYPE char * +#define VALUE_TYPE char * +#define KEY_TO_HASH(key, key_size) (ht_default_hash_string(key, key_size)) +#define HT_NULL_VALUE NULL +#define VARIABLE_KEY_LEN +#define VARIABLE_VALUE_LEN #elif HT_TYPE == 7 // Hash table HtSU that has C-string as key and ut64 as value -#define HtName_(name) name##SU -#define Ht_(name) ht_su_##name -#define HT_(name) HtSU##name -#define KEY_TYPE char * -#define VALUE_TYPE ut64 -#define KEY_TO_HASH(x) ((ut32)(uintptr_t)(x)) -#define HT_NULL_VALUE 0 +#define HtName_(name) name##SU +#define Ht_(name) ht_su_##name +#define HT_(name) HtSU##name +#define KEY_TYPE char * +#define VALUE_TYPE ut64 +#define KEY_TO_HASH(key, key_size) (ht_default_hash_string(key, key_size)) +#define HT_NULL_VALUE 0 +#define VARIABLE_KEY_LEN +#endif + +// Uncomment to enable support for custom element size (opt.elem_size != sizeof(VALUE_TYPE)). This could +// slow down lookup/insert/iteration with about 5-10%. +// #ifndef HT_ENABLE_CUSTOM_ELEM_SIZE +// #define HT_ENABLE_CUSTOM_ELEM_SIZE +// #endif + +#ifndef HT_HASH_FUNCTIONS +#define HT_HASH_FUNCTIONS + +/** + * \brief A Murmur3-like hash function which reduces a 64-bit key to a 32-bit non-cryptographic hash. + * + * It's simpler than MurmurHash3 which makes it run faster at the cost of slightly worse hash distribution. + * This hash function is used for HtPX and HtUX, if no custom hash function is specified. + */ +static inline ut32 ht_default_hash_ut64(ut64 key) { + key ^= key >> 33; + key *= 0xff51afd7ed558ccdULL; + key ^= key >> 33; + return (uint32_t)(key ^ (key >> 32)); +} + +/** + * \brief Computes 32-bit hash for a byte buffer (string). + * + * The function uses Murmur3 mixing constants and tries to process the buffer in 16, 8 or 4-byte blocks when possible. + * This hash function is used for HtSX, if no custom hash function is specified. + */ +static inline ut32 ht_default_hash_string(const char *key, ut32 len) { + const uint64_t prime1 = 0xff51afd7ed558ccdULL; /* Murmur3 consts */ + const uint64_t prime2 = 0xc4ceb9fe1a85ec53ULL; + ut64 result = 0xff51afd7ed558ccdULL; + + while (len >= 16) { + ut64 blocks[2]; + memcpy(blocks, key, sizeof(ut64) * 2); + result += (result << 5) ^ (blocks[0] * prime1); + result += (result << 5) ^ (blocks[1] * prime1); + len -= 16; + key += 16; + } + + while (len >= 8) { + ut64 block = 0; + memcpy(&block, key, sizeof(ut64)); + result += (result << 5) ^ (block * prime1); + len -= 8; + key += 8; + } + + while (len >= 4) { + ut32 block = 0; + memcpy(&block, key, sizeof(ut32)); + result += (result << 5) ^ (block * prime1); + len -= 4; + key += 4; + } + + while (len > 0) { + result += (result << 5) ^ (*key * prime1); + len -= 1; + key += 1; + } + + // Finalize + result ^= result >> 33; + result *= prime2; + + // Fold the result + return (ut32)(result ^ (result >> 32)); +} #endif #ifndef HT_ENUM_DEFINED @@ -107,12 +192,18 @@ typedef enum { #include -/* Kv represents a single key/value element in the hashtable */ +/** + * \brief Kv represents a single key/value element in the hashtable + */ typedef struct Ht_(kv) { KEY_TYPE key; VALUE_TYPE value; - ut32 key_len; - ut32 value_len; +#ifdef VARIABLE_KEY_LEN + ut32 key_len; ///< Size of the key. Used only for pointer or string keys. +#endif +#ifdef VARIABLE_VALUE_LEN + ut32 value_len; ///< Size of the value. Used only for pointer or string values. +#endif } HT_(Kv); typedef void (*HT_(FiniKv))(HT_(Kv) *kv, void *user); @@ -124,50 +215,48 @@ typedef ut32 (*HT_(CalcSizeV))(const VALUE_TYPE); typedef ut32 (*HT_(HashFunction))(const KEY_TYPE); typedef int (*HT_(Comparator))(const KEY_TYPE, const KEY_TYPE); typedef bool (*HT_(ForeachCallback))(void *user, const KEY_TYPE, const VALUE_TYPE); - -typedef struct Ht_(bucket_t) { - HT_(Kv) *arr; - ut32 count; -} HT_(Bucket); +typedef bool (*HT_(ForeachKvCallback))(void *user, const HT_(Kv) *kv); /** * Options contain all the settings of the hashtable. */ typedef struct Ht_(options_t) { + size_t elem_size; ///< Size of each HtKv element (useful for subclassing like SdbKv). + ///< Zero value means to use default size of HtKv. HT_(Comparator) cmp; ///< RZ_NULLABLE. Function for comparing keys. ///< Returns 0 if keys are equal. ///< Function is invoked only if == operator applied to keys returns false. HT_(HashFunction) hashfn; ///< RZ_NULLABLE. Function for hashing items in the hash table. ///< If NULL KEY_TO_HASH macro is used. - HT_(DupKey) dupkey; ///< RZ_NULLABLE. Function for making a copy of key. - ///< If NULL simple assignment operator is used. - HT_(DupValue) dupvalue; ///< RZ_NULLABLE. Function for making a copy of value. - ///< If NULL simple assignment operator is used. HT_(CalcSizeK) calcsizeK; ///< RZ_NULLABLE. Function to determine the key's size. ///< If NULL zero value is used as a size. ///< Key sizes are checked on equality during keys comparsion as a pre-check. HT_(CalcSizeV) calcsizeV; ///< RZ_NULLABLE. Function to determine the value's size. ///< If NULL zero value is used as a size. ///< Not required for common scenarios. Could be used in subclasses. + HT_(DupKey) dupkey; ///< RZ_NULLABLE. Function for making a copy of key. + ///< If NULL simple assignment operator is used. + HT_(DupValue) dupvalue; ///< RZ_NULLABLE. Function for making a copy of value. + ///< If NULL simple assignment operator is used. HT_(FiniKv) finiKV; ///< RZ_NULLABLE. Function to clean up the key-value store. void *finiKV_user; ///< RZ_NULLABLE. User data which is passed into finiKV. - size_t elem_size; ///< Size of each HtKv element (useful for subclassing like SdbKv). - ///< Zero value means to use default size of HtKv. } HT_(Options); /* Ht is the hashtable structure */ typedef struct Ht_(t) { - ut32 size; ///< Size of the hash table in buckets. - ut32 count; ///< Number of stored elements. - HT_(Bucket) *table; ///< Actual table. - ut32 prime_idx; - HT_(Options) opt; + ut32 capacity; ///< Capacity of the main array. + ut32 capacity_mask; ///< Set to `capacity - 1` and used for bucket/index modulo. + ut32 size; ///< Number of stored elements. + ut32 growth_left; ///< Number of empty slots. + RZ_BORROW ut8 *ctrl; ///< Control bytes (metadata) - point to the beginning of the `data` pointer. + RZ_BORROW HT_(Kv) *slots; ///< Main array (no buckets) - points to an offset after the `data` pointer. + HT_(Options) opt; ///< Methods + ut8 *data; ///< Single allocation for `ctrl` and `slots` arrays. } HtName_(Ht); typedef struct Ht_(iter_mut_t) { HtName_(Ht) *ht; ///< The hash table to iterate over. ut32 ti; ///< Table index - ut32 bi; ///< Bucket index HT_(Kv) *kv; ///< Current Key-Value-pair. } HT_(IterMutState); @@ -202,6 +291,7 @@ RZ_API VALUE_TYPE Ht_(find)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, RZ_N // cb should not modify the hashtable. // NOTE: cb can delete the current element, but it should be avoided RZ_API void Ht_(foreach)(RZ_NONNULL HtName_(Ht) *ht, RZ_NONNULL HT_(ForeachCallback) cb, RZ_NULLABLE void *user); +RZ_API bool Ht_(foreach_kv)(RZ_NONNULL HtName_(Ht) *ht, RZ_NONNULL HT_(ForeachKvCallback) cb, RZ_NULLABLE void *user); RZ_API ut32 Ht_(size)(const RZ_NONNULL HtName_(Ht) *ht); diff --git a/librz/util/ht/ht_inc.c b/librz/util/ht/ht_inc.c index edd6435ea1..f15374faba 100644 --- a/librz/util/ht/ht_inc.c +++ b/librz/util/ht/ht_inc.c @@ -2,35 +2,189 @@ // SPDX-FileCopyrightText: 2016-2018 pancake // SPDX-FileCopyrightText: 2016-2018 ret2libc // SPDX-FileCopyrightText: 2024 pelijah +// SPDX-FileCopyrightText: 2026 Anton Angelov // SPDX-License-Identifier: BSD-3-Clause +#include +#include #include #include #include #include +#include -#define LOAD_FACTOR 1 -#define S_ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) +/** + * \file ht_inc.c + * \brief "SwissTable" hash table implementation. + * + * References: + * - https://abseil.io/about/design/swisstables + * - https://en.wikipedia.org/wiki/Open_addressing + */ -// Sizes of the ht. -static const ut32 ht_primes_sizes[] = { - 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, - 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, - 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, - 5839, 7013, 8419, 10103, 12143, 14591, 17519, 21023, - 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, - 108631, 130363, 156437, 187751, 225307, 270371, 324449, - 389357, 467237, 560689, 672827, 807403, 968897, 1162687, - 1395263, 1674319, 2009191, 2411033, 2893249, 3471899, - 4166287, 4999559, 5999471, 7199369 -}; +// Load factor thershold of 87.5% (after that the table grows) +#define LOAD_FACTOR_NUM 7 +#define LOAD_FACTOR_DEN 8 /* should be power of 2; also GROUP_WIDTH should be multiple of LOAD_FACTOR_DEN */ -static inline ut32 hashfn(HtName_(Ht) *ht, const KEY_TYPE k) { - return ht->opt.hashfn ? ht->opt.hashfn(k) : KEY_TO_HASH(k); +// Helper macros for H1/H2 hash components +#define H1(HASH) ((HASH) >> 7) +#define H2_HASH_FRAGMENT(HASH) ((HASH) & 0x7F) +#define H2_STATUS_DELETED 0b11111110 +#define H2_STATUS_EMPTY 0b11111111 +#define H2_IS_EMPTY_OR_DELETED(CTRL) ((CTRL) >> 7) +#define H2_IS_EMPTY(CTRL) ((CTRL) == H2_STATUS_EMPTY) +#define H2_IS_DELETED(CTRL) ((CTRL) == H2_STATUS_DELETED) +#define INDEX_TYPE ut32 +#define INVALID_INDEX UT32_MAX + +// Select lookup implementation +#if HAVE_SSE2 +#include +#define LOOKUP_METHOD_SSE2 +#define GROUP_WIDTH 16 +typedef ut16 group_mask_t; +typedef __m128i group_t; +#elif RZ_SYS_BITS == RZ_SYS_BITS_64 +#define LOOKUP_METHOD_BITWISE_64 +typedef ut64 group_t; +typedef ut64 group_mask_t; +#define GROUP_WIDTH sizeof(group_t) +#else +// Default lookup implementation +typedef ut64 group_t; +#define GROUP_WIDTH sizeof(group_t) +#endif + +// Minimal capacity of a hash table +#define MIN_CAPACITY GROUP_WIDTH + +// Slot addressing is different depending on whether custom elem_size is used +#ifdef HT_ENABLE_CUSTOM_ELEM_SIZE +#define HT_SLOT_AT(ht, index) \ + ((HT_(Kv) *)((ut8 *)(ht->slots) + index * ht->opt.elem_size)) +#else +#define HT_SLOT_AT(ht, index) \ + (&(ht)->slots[(index)]) +#endif + +// Helper macro for implementing an unrolled foreach loop +#define HT_FOREACH_UNROLL(ht, kv, idx, body) \ + if (!H2_IS_EMPTY_OR_DELETED((ht)->ctrl[idx])) { \ + HT_(Kv) *kv = HT_SLOT_AT((ht), (idx)); \ + body \ + } + +// Helper function for the different lookup implementations +#if defined(LOOKUP_METHOD_SSE2) +static inline group_t group_load(const void *addr) { + return _mm_loadu_si128((const __m128i *)addr); } -static inline ut32 bucketfn(HtName_(Ht) *ht, const KEY_TYPE k) { - return hashfn(ht, k) % ht->size; +static inline group_mask_t group_match_hash_fragment(group_t group, ut8 ctrl) { + __m128i ctrl_vec = _mm_set1_epi8((char)ctrl); + __m128i diff = _mm_cmpeq_epi8(group, ctrl_vec); + return (group_mask_t)_mm_movemask_epi8(diff); +} + +static inline group_mask_t group_match_empty(group_t group) { + __m128i empty_vec = _mm_set1_epi8(H2_STATUS_EMPTY); + __m128i diff = _mm_cmpeq_epi8(group, empty_vec); + return (group_mask_t)_mm_movemask_epi8(diff); +} + +static inline group_mask_t group_match_deleted(group_t group) { + __m128i deleted_vec = _mm_set1_epi8(H2_STATUS_DELETED); + __m128i diff = _mm_cmpeq_epi8(group, deleted_vec); + return (group_mask_t)_mm_movemask_epi8(diff); +} + +static inline ut8 group_lowest_bit(group_mask_t mask) { + return rz_bits_trailing_zeros(mask); +} + +#define HT_FOREACH(ht, kv, body) \ + for (INDEX_TYPE i = 0; i < (ht)->capacity; i += GROUP_WIDTH) { \ + RZ_PREFETCH(&(ht)->ctrl[i + GROUP_WIDTH]); \ + RZ_PREFETCH(HT_SLOT_AT((ht), i + GROUP_WIDTH)); \ + HT_FOREACH_UNROLL(ht, kv, i + 0, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 1, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 2, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 3, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 4, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 5, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 6, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 7, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 8, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 9, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 10, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 11, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 12, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 13, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 14, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 15, body); \ + } +#elif defined(LOOKUP_METHOD_BITWISE_64) +static inline group_t group_load(const void *addr) { + return rz_read_le64(addr); +} + +// Construct a ut64 by repeating a single byte (e.g. 0xF0 -> 0xF0F0F0F0F0F0F0F0) +static inline group_t group_repeat(ut8 byte) { + return byte * 0x0101010101010101ull; +} + +static inline group_t group_match_hash_fragment(group_t group, ut8 ctrl) { + group_t diff = group ^ group_repeat(ctrl); + return (diff - group_repeat(0x01)) & ~diff & group_repeat(0x80); +} + +static inline group_mask_t group_match_empty(group_t group) { + group_mask_t xor = group ^ group_repeat(0xFF); + return (xor - group_repeat(0x01)) & ~xor & group_repeat(0x80); +} + +static inline group_mask_t group_match_deleted(group_t group) { + return group & ~(group << 1) & group_repeat(0x80); +} + +static inline ut8 group_lowest_bit(group_mask_t mask) { + return mask ? rz_bits_trailing_zeros(mask) / 8 : UT8_MAX; +} + +#define HT_FOREACH(ht, kv, body) \ + for (INDEX_TYPE i = 0; i < (ht)->capacity; i += GROUP_WIDTH) { \ + RZ_PREFETCH(&(ht)->ctrl[i + GROUP_WIDTH]); \ + RZ_PREFETCH(HT_SLOT_AT((ht), i + GROUP_WIDTH)); \ + HT_FOREACH_UNROLL(ht, kv, i + 0, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 1, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 2, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 3, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 4, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 5, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 6, body); \ + HT_FOREACH_UNROLL(ht, kv, i + 7, body); \ + } +#else +// Default implementation +#define HT_FOREACH(ht, kv, body) \ + for (INDEX_TYPE i = 0; i < (ht)->capacity; i++) { \ + if (!H2_IS_EMPTY_OR_DELETED((ht)->ctrl[i])) { \ + HT_(Kv) *kv = HT_SLOT_AT((ht), i); \ + body \ + } \ + } +#endif + +static inline ut32 kv_key_len(const HT_(Kv) *kv) { +#ifdef VARIABLE_KEY_LEN + return kv->key_len; +#else + return 0; +#endif +} + +static inline ut32 hashfn(HtName_(Ht) *ht, const KEY_TYPE k, ut32 key_size) { + return ht->opt.hashfn ? ht->opt.hashfn(k) : KEY_TO_HASH(k, key_size); } static inline KEY_TYPE dupkey(HtName_(Ht) *ht, const KEY_TYPE k) { @@ -42,11 +196,19 @@ static inline VALUE_TYPE dupval(HtName_(Ht) *ht, const VALUE_TYPE v) { } static inline ut32 calcsize_key(HtName_(Ht) *ht, const KEY_TYPE k) { +#ifdef VARIABLE_KEY_LEN return ht->opt.calcsizeK ? ht->opt.calcsizeK(k) : 0; +#else + return 0; +#endif } static inline ut32 calcsize_val(HtName_(Ht) *ht, const VALUE_TYPE v) { +#ifdef VARIABLE_VALUE_LEN return ht->opt.calcsizeV ? ht->opt.calcsizeV(v) : 0; +#else + return 0; +#endif } static inline void fini_kv_pair(HtName_(Ht) *ht, HT_(Kv) *kv) { @@ -55,70 +217,112 @@ static inline void fini_kv_pair(HtName_(Ht) *ht, HT_(Kv) *kv) { } } -static inline ut32 next_idx(ut32 idx) { - if (idx != UT32_MAX && idx < S_ARRAY_SIZE(ht_primes_sizes) - 1) { - return idx + 1; - } - return UT32_MAX; -} - -static inline ut32 compute_size(ut32 idx, ut32 sz) { - // when possible, use the precomputed prime numbers which help with - // collisions, otherwise, at least make the number odd with |1 - return idx != UT32_MAX && idx < S_ARRAY_SIZE(ht_primes_sizes) ? ht_primes_sizes[idx] : (sz | 1); -} - -static inline bool is_kv_equal(HtName_(Ht) *ht, const KEY_TYPE key, const ut32 key_len, const HT_(Kv) *kv) { - if (key_len != kv->key_len) { +static inline bool is_key_equal(HtName_(Ht) *ht, const KEY_TYPE key, const ut32 key_len, const HT_(Kv) *kv) { +#ifdef VARIABLE_KEY_LEN + if (key_len != kv_key_len(kv)) { return false; } +#endif - bool res = key == kv->key; - if (!res && ht->opt.cmp) { - res = !ht->opt.cmp(key, kv->key); + if (key == kv->key) { + return true; } - return res; + + return ht->opt.cmp && !ht->opt.cmp(key, kv->key); } -static inline HT_(Kv) *kv_at(HtName_(Ht) *ht, HT_(Bucket) *bt, ut32 i) { - return (HT_(Kv) *)((char *)bt->arr + i * ht->opt.elem_size); +static void ctrl_table_set(HtName_(Ht) *ht, INDEX_TYPE idx, ut8 value) { + // Branchless copy to mirrored bytes: if idx < GROUP_WIDTH the code below will set `mirror_idx` + // to `ht->capacity + idx` and otherwise to `idx` (resulting in harmless duplicate write). + ut32 mirror_idx = ((idx - (GROUP_WIDTH - 1)) & ht->capacity_mask) + ((GROUP_WIDTH - 1) & ht->capacity_mask); + ht->ctrl[idx] = value; + ht->ctrl[mirror_idx] = value; } -static inline HT_(Kv) *next_kv(HtName_(Ht) *ht, HT_(Kv) *kv) { - return (HT_(Kv) *)((char *)kv + ht->opt.elem_size); +static ut32 next_power_of_two(ut32 n) { + if (n <= 1) { + return 1; + } + + if ((n & (n - 1)) == 0) { + return n; + } + + ut8 shift = 64 - rz_bits_leading_zeros(n); + + if (shift > 31) { + // ut32 overflow + rz_warn_if_reached(); + return 0x80000000; + } + + return 1ul << shift; } -#define BUCKET_FOREACH(ht, bt, j, kv) \ - if ((bt)->arr) \ - for ((j) = 0, (kv) = (bt)->arr; (j) < (bt)->count; (j)++, (kv) = next_kv(ht, kv)) - -#define BUCKET_FOREACH_SAFE(ht, bt, j, count, kv) \ - if ((bt)->arr) \ - for ((j) = 0, (kv) = (bt)->arr, (count) = (ht)->count; \ - (j) < (bt)->count; \ - (j) = (count) == (ht)->count ? j + 1 : j, (kv) = (count) == (ht)->count ? next_kv(ht, kv) : kv, (count) = (ht)->count) - -// Create a new hashtable and return a pointer to it. -// size - number of buckets in the hashtable -static RZ_OWN HtName_(Ht) *internal_ht_new(ut32 size, ut32 prime_idx, HT_(Options) *opt) { +/** + * \brief Create a new hashtable and return a pointer to it. + */ +static RZ_OWN HtName_(Ht) *internal_ht_new(ut32 requested_capacity, HT_(Options) *opt) { HtName_(Ht) *ht = RZ_NEW0(HtName_(Ht)); if (!ht) { return NULL; } - ht->size = size; - ht->count = 0; - ht->prime_idx = prime_idx; - ht->table = calloc(ht->size, sizeof(*ht->table)); - if (!ht->table) { - free(ht); - return NULL; - } + + // Use minimum capacity of group size in order to avoid edge cases (related to ctrl byte mirroring and deletion slot placement) + // No maximum capacity enforcement at the moment.. + ht->capacity = next_power_of_two(RZ_MAX(requested_capacity, 16)); + ht->capacity_mask = ht->capacity - 1; + ht->growth_left = (ht->capacity / LOAD_FACTOR_DEN) * LOAD_FACTOR_NUM; + ht->size = 0; ht->opt = *opt; - // if not provided, assume we are dealing with a regular HtName_(Ht), with - // HT_(Kv) as elements + + // If not provided, assume we are dealing with a regular HtName_(Ht), with HT_(Kv) as elements if (ht->opt.elem_size == 0) { ht->opt.elem_size = sizeof(HT_(Kv)); } + + // Allocate additional space for the mirrored bytes at the end of the control array + ut32 ctrl_size = (ht->capacity + GROUP_WIDTH) * sizeof(*ht->ctrl); + ut32 slots_size = ht->capacity * ht->opt.elem_size; + +#ifndef HT_ENABLE_CUSTOM_ELEM_SIZE + if (ht->opt.elem_size != sizeof(HT_(Kv))) { + // Custom elem_size support can be enabled by uncommenting the respective define + rz_warn_if_reached(); + free(ht); + return NULL; + } +#endif + +#ifndef VARIABLE_KEY_LEN + if (ht->opt.calcsizeK) { + // Key type is expected to be fixed sized (i.e. ut64) + rz_warn_if_reached(); + free(ht); + return NULL; + } +#endif + +#ifndef VARIABLE_VALUE_LEN + if (ht->opt.calcsizeV) { + // Value type is expected to be fixed sized (i.e. ut64) + rz_warn_if_reached(); + free(ht); + return NULL; + } +#endif + + // Allocate single heap block for both control and slot arrays + if ((ht->data = malloc(ctrl_size + slots_size)) == NULL) { + free(ht); + return NULL; + } + + ht->ctrl = ht->data; + ht->slots = (HT_(Kv) *)(ht->data + ctrl_size); + + // Initialize all slots as empty + memset(ht->ctrl, H2_STATUS_EMPTY, ctrl_size); return ht; } @@ -129,7 +333,7 @@ static RZ_OWN HtName_(Ht) *internal_ht_new(ut32 size, ut32 prime_idx, HT_(Option */ RZ_API RZ_OWN HtName_(Ht) *Ht_(new_opt)(RZ_NONNULL HT_(Options) *opt) { rz_return_val_if_fail(opt, NULL); - return internal_ht_new(ht_primes_sizes[0], 0, opt); + return internal_ht_new(0, opt); } /** @@ -140,16 +344,7 @@ RZ_API RZ_OWN HtName_(Ht) *Ht_(new_opt)(RZ_NONNULL HT_(Options) *opt) { */ RZ_API RZ_OWN HtName_(Ht) *Ht_(new_opt_size)(RZ_NONNULL HT_(Options) *opt, ut32 initial_size) { rz_return_val_if_fail(opt, NULL); - ut32 idx = 0; - while (idx < S_ARRAY_SIZE(ht_primes_sizes) && - ht_primes_sizes[idx] * LOAD_FACTOR < initial_size) { - idx++; - } - if (idx == S_ARRAY_SIZE(ht_primes_sizes)) { - idx = UT32_MAX; - } - ut32 sz = compute_size(idx, (ut32)(initial_size * (2 - LOAD_FACTOR))); - return internal_ht_new(sz, idx, opt); + return internal_ht_new(initial_size, opt); } RZ_API void Ht_(free)(RZ_NULLABLE HtName_(Ht) *ht) { @@ -157,75 +352,55 @@ RZ_API void Ht_(free)(RZ_NULLABLE HtName_(Ht) *ht) { return; } - Ht_(clear)(ht); - for (size_t i = 0; i < ht->size; i++) { - HT_(Bucket) *bt = &ht->table[i]; - free(bt->arr); + if (ht->opt.finiKV) { + HT_FOREACH(ht, kv, { + fini_kv_pair(ht, kv); + }); } - free(ht->table); + + free(ht->data); free(ht); } /** * \brief Remove all entries in the hash table. * - * \param ht The hash table to clean. + * \param ht The hash table to clear. */ RZ_API void Ht_(clear)(RZ_NONNULL HtName_(Ht) *ht) { rz_return_if_fail(ht); - ut32 i; - for (i = 0; i < ht->size; i++) { - HT_(Bucket) *bt = &ht->table[i]; - HT_(Kv) *kv; - ut32 j; - - if (ht->opt.finiKV) { - BUCKET_FOREACH(ht, bt, j, kv) { - ht->opt.finiKV(kv, ht->opt.finiKV_user); - } - } - bt->count = 0; + if (ht->opt.finiKV) { + HT_FOREACH(ht, kv, { + fini_kv_pair(ht, kv); + }); } - ht->count = 0; + + // Reset control byte array + memset(ht->ctrl, H2_STATUS_EMPTY, (ht->capacity + GROUP_WIDTH) * sizeof(*ht->ctrl)); + ht->growth_left = (ht->capacity / LOAD_FACTOR_DEN) * LOAD_FACTOR_NUM; + ht->size = 0; } /** - * Increases the size of the hashtable by 2. - * Tracks change of KV \p tracked position. + * \brief Creates a new hash table with requested size, copies existing elements and swaps with \p ht. */ -static HT_(Kv) *internal_ht_grow(HtName_(Ht) *ht, HT_(Kv) *tracked) { - ut32 idx = next_idx(ht->prime_idx); - ut32 sz = compute_size(idx, ht->size * 2); - - HtName_(Ht) *ht2 = internal_ht_new(sz, idx, &ht->opt); +static bool internal_ht_resize(HtName_(Ht) *ht, ut32 new_size) { + // Create a new hash table + HtName_(Ht) *ht2 = internal_ht_new(new_size, &ht->opt); if (!ht2) { // we can't grow the ht anymore. Never mind, we'll be slower, // but everything can continue to work - return tracked; + return false; } - for (ut32 i = 0; i < ht->size; i++) { - HT_(Bucket) *bt = &ht->table[i]; - HT_(Kv) *kv; - ut32 j; - - BUCKET_FOREACH(ht, bt, j, kv) { - if (kv == tracked) { - continue; - } - if (Ht_(insert_kv_ex)(ht2, kv, false, NULL) < 0) { - ht2->opt.finiKV = NULL; - Ht_(free)(ht2); - return tracked; - } + HT_FOREACH(ht, kv, { + if (Ht_(insert_kv_ex)(ht2, kv, false, NULL) < 0) { + ht2->opt.finiKV = NULL; + Ht_(free)(ht2); + return false; } - } - if (Ht_(insert_kv_ex)(ht2, tracked, false, &tracked) < 0) { - ht2->opt.finiKV = NULL; - Ht_(free)(ht2); - return tracked; - } + }); // And now swap the internals. HtName_(Ht) swap = *ht; @@ -234,47 +409,243 @@ static HT_(Kv) *internal_ht_grow(HtName_(Ht) *ht, HT_(Kv) *tracked) { ht2->opt.finiKV = NULL; Ht_(free)(ht2); - return tracked; -} - -static HT_(Kv) *check_growing(HtName_(Ht) *ht, HT_(Kv) *tracked) { - if (ht->count >= LOAD_FACTOR * ht->size) { - return internal_ht_grow(ht, tracked); - } - return tracked; + return true; } /** - * \brief Get an existing KV with key \p key or allocate a new KV otherwise + * \brief Checks if the hash table needs to grow (if load factor limit is reached) or rehash (if there are too many slots marked as "deleted") */ -static RZ_BORROW HT_(Kv) *reserve_kv(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, const ut32 key_len, bool update, RZ_NONNULL HtRetCode *code) { - HT_(Bucket) *bt = &ht->table[bucketfn(ht, key)]; - HT_(Kv) *kvtmp; - ut32 j; - - BUCKET_FOREACH(ht, bt, j, kvtmp) { - if (is_kv_equal(ht, key, key_len, kvtmp)) { - if (update) { - fini_kv_pair(ht, kvtmp); - *code = HT_RC_UPDATED; - } else { - *code = HT_RC_EXISTING; - } - return kvtmp; - } +static bool internal_ht_rehash_if_needed(HtName_(Ht) *ht) { + if (ht->growth_left) { + return true; } - HT_(Kv) *newkvarr = realloc(bt->arr, (bt->count + 1) * ht->opt.elem_size); - if (!newkvarr) { + ut32 capacity_used = (ht->capacity / LOAD_FACTOR_DEN) * LOAD_FACTOR_NUM - ht->growth_left; + ut32 num_deleted = capacity_used - ht->size; + + if (num_deleted > ht->capacity / 4) { + // 25% of the elements are deleted slots; rehash with the same size + return internal_ht_resize(ht, ht->capacity); + } + + // Grow + return internal_ht_resize(ht, ht->capacity + 1); +} + +/** + * \brief Looks up an existing \p key, or reserves an unused slot otherwise. + */ +static INDEX_TYPE ctrl_table_lookup_or_reserve(HtName_(Ht) *ht, const KEY_TYPE key, const ut32 key_len, ut32 hash, ut8 hash_fragment, ut8 *previous_ctrl, bool *existing) { + ut32 probe_step = GROUP_WIDTH; + INDEX_TYPE index = H1(hash) & ht->capacity_mask; + INDEX_TYPE first_deleted = INVALID_INDEX; + + while (true) { + // Probe one group at a time +#if defined(LOOKUP_METHOD_SSE2) || defined(LOOKUP_METHOD_BITWISE_64) + group_mask_t deleted_match; + group_mask_t empty_match; + group_t group = group_load(&ht->ctrl[index]); + + // Match all control group bytes with the hash fragment of `key` + for (group_mask_t ctrl_match = group_match_hash_fragment(group, hash_fragment); ctrl_match != 0; ctrl_match &= ctrl_match - 1) { + INDEX_TYPE i = (index + group_lowest_bit(ctrl_match)) & ht->capacity_mask; + + if (is_key_equal(ht, key, key_len, HT_SLOT_AT(ht, i))) { + *existing = true; + *previous_ctrl = 0; // not empty or deleted; we don't need the actual value + return i; + } + } + + // If we reach a "deleted" slot, save it's index and return later + if (first_deleted == INVALID_INDEX && (deleted_match = group_match_deleted(group))) { + first_deleted = (index + group_lowest_bit(deleted_match)) & ht->capacity_mask; + } + + // Check if there is at least 1 empty slot in the group + if ((empty_match = group_match_empty(group))) { + *existing = false; + *previous_ctrl = first_deleted == INVALID_INDEX ? H2_STATUS_EMPTY : H2_STATUS_DELETED; + return *previous_ctrl == H2_STATUS_EMPTY ? (index + group_lowest_bit(empty_match)) & ht->capacity_mask : first_deleted; + } +#else + INDEX_TYPE first_empty = INVALID_INDEX; + + for (INDEX_TYPE i = index; i < index + GROUP_WIDTH; i++) { + INDEX_TYPE normalized_i = i & ht->capacity_mask; + + if (H2_IS_EMPTY(ht->ctrl[i])) { + if (first_empty == INVALID_INDEX) { + first_empty = normalized_i; + } + continue; + } + + // If we visit a deleted slot, save it's index for potential later use + if (H2_IS_DELETED(ht->ctrl[i])) { + if (first_deleted == INVALID_INDEX) { + first_deleted = normalized_i; + } + continue; + } + + if (ht->ctrl[i] == hash_fragment && is_key_equal(ht, key, key_len, HT_SLOT_AT(ht, normalized_i))) { + *existing = true; + *previous_ctrl = 0; // not empty; we don't need the value + return normalized_i; + } + } + + if (first_empty != INVALID_INDEX) { + *existing = false; + *previous_ctrl = first_deleted == INVALID_INDEX ? H2_STATUS_EMPTY : H2_STATUS_DELETED; + return *previous_ctrl == H2_STATUS_EMPTY ? first_empty : first_deleted; + } +#endif + + // Warn if we reach past the probing sequence (unexpected since we shouldn't get above load factor > 85.4%) + if (probe_step >= ht->capacity) { + rz_warn_if_reached(); + *existing = false; + return INVALID_INDEX; + } + + // Triangular probing + index = (index + probe_step) & ht->capacity_mask; + probe_step += GROUP_WIDTH; + } +} + +/** + * \brief Looks up a \p key and returns it's slot index. + */ +static INDEX_TYPE ctrl_table_lookup(HtName_(Ht) *ht, const KEY_TYPE key, const ut32 key_len) { + ut32 probe_step = GROUP_WIDTH; + ut32 hash = hashfn(ht, key, key_len); + ut8 hash_fragment = H2_HASH_FRAGMENT(hash); + INDEX_TYPE index = H1(hash) & ht->capacity_mask; + + RZ_PREFETCH(HT_SLOT_AT(ht, index)); + + while (true) { + // Probe one group at a time +#if defined(LOOKUP_METHOD_SSE2) || defined(LOOKUP_METHOD_BITWISE_64) + group_t group = group_load(&ht->ctrl[index]); + + // Match all group control bytes with the H2 (hash fragment) of `key` + for (group_mask_t ctrl_match = group_match_hash_fragment(group, hash_fragment); ctrl_match != 0; ctrl_match &= ctrl_match - 1) { + INDEX_TYPE i = (index + group_lowest_bit(ctrl_match)) & ht->capacity_mask; + + if (is_key_equal(ht, key, key_len, HT_SLOT_AT(ht, i))) { + return i; + } + } + + // Check if there is at least 1 empty slot in the group + if (group_match_empty(group)) { + return INVALID_INDEX; + } +#else + bool empty_found = false; + + for (ut32 i = index; i < index + GROUP_WIDTH; i++) { + INDEX_TYPE normalized_i = i & ht->capacity_mask; + + if (H2_IS_EMPTY(ht->ctrl[i])) { + empty_found = true; + continue; + } + + if (ht->ctrl[i] == hash_fragment && is_key_equal(ht, key, key_len, HT_SLOT_AT(ht, normalized_i))) { + return normalized_i; + } + } + + if (empty_found) { + return INVALID_INDEX; + } +#endif + + // Warn if we reach past the probing sequence (unexpected) + if (probe_step >= ht->capacity) { + rz_warn_if_reached(); + return INVALID_INDEX; + } + + // Triangular probing + index = (index + probe_step) & ht->capacity_mask; + probe_step += GROUP_WIDTH; + } +} + +/** + * \brief Get an existing KV with key \p key or allocate a new KV otherwise. + */ +static RZ_BORROW HT_(Kv) *reserve_kv(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, const ut32 key_len, bool update, RZ_NONNULL HtRetCode *code) { + if (!internal_ht_rehash_if_needed(ht)) { *code = HT_RC_ERROR; return NULL; } - bt->arr = newkvarr; - bt->count++; - ht->count++; + ut32 hash = hashfn(ht, key, key_len); + ut8 hash_fragment = H2_HASH_FRAGMENT(hash); + ut8 previous_ctrl = 0; + bool existing = false; + INDEX_TYPE idx = ctrl_table_lookup_or_reserve(ht, key, key_len, hash, hash_fragment, &previous_ctrl, &existing); + + if (idx == INVALID_INDEX) { + *code = HT_RC_ERROR; + return NULL; + } + + RZ_PREFETCH(HT_SLOT_AT(ht, idx)); + + if (existing) { + if (update) { + fini_kv_pair(ht, HT_SLOT_AT(ht, idx)); + *code = HT_RC_UPDATED; + } else { + *code = HT_RC_EXISTING; + } + return HT_SLOT_AT(ht, idx); + } + + // Writing over an empty or a deleted slot *code = HT_RC_INSERTED; - return kv_at(ht, bt, bt->count - 1); + + // Decrease `growth_left` if writing over an empty slot + if (previous_ctrl == H2_STATUS_EMPTY) { + ht->growth_left--; + } + + ht->size++; + ctrl_table_set(ht, idx, hash_fragment); + return HT_SLOT_AT(ht, idx); +} + +// + +static inline HtRetCode internal_insert_kv_ex(RZ_NONNULL HtName_(Ht) *ht, RZ_NONNULL HT_(Kv) *kv, bool update, RZ_OUT RZ_NULLABLE HT_(Kv) **out_kv) { + rz_return_val_if_fail(ht && kv, HT_RC_ERROR); + + HtRetCode rc; + HT_(Kv) *kv_dst = reserve_kv(ht, kv->key, kv_key_len(kv), update, &rc); + + if (rc <= 0) { + if (out_kv) { + *out_kv = kv_dst; + } + return rc; + } + + memcpy(kv_dst, kv, ht->opt.elem_size); + + if (out_kv) { + *out_kv = kv_dst; + } + + return rc; } /** @@ -286,7 +657,7 @@ static RZ_BORROW HT_(Kv) *reserve_kv(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE * \return Returns true if insertion/replacement took place */ RZ_API bool Ht_(insert_kv)(RZ_NONNULL HtName_(Ht) *ht, RZ_NONNULL HT_(Kv) *kv, bool update) { - return Ht_(insert_kv_ex)(ht, kv, update, NULL) > 0; + return internal_insert_kv_ex(ht, kv, update, NULL) > 0; } /** @@ -303,27 +674,13 @@ RZ_API bool Ht_(insert_kv)(RZ_NONNULL HtName_(Ht) *ht, RZ_NONNULL HT_(Kv) *kv, b * returns HT_RC_ERROR if out of memory. */ RZ_API HtRetCode Ht_(insert_kv_ex)(RZ_NONNULL HtName_(Ht) *ht, RZ_NONNULL HT_(Kv) *kv, bool update, RZ_OUT RZ_NULLABLE HT_(Kv) **out_kv) { - rz_return_val_if_fail(ht && kv, HT_RC_ERROR); - - HtRetCode rc; - HT_(Kv) *kv_dst = reserve_kv(ht, kv->key, kv->key_len, update, &rc); - if (rc <= 0) { - if (out_kv) { - *out_kv = kv_dst; - } - return rc; - } - memcpy(kv_dst, kv, ht->opt.elem_size); - kv_dst = check_growing(ht, kv_dst); - if (out_kv) { - *out_kv = kv_dst; - } - return rc; + return internal_insert_kv_ex(ht, kv, update, NULL); } -static int insert_update(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, VALUE_TYPE value, bool update, RZ_OUT RZ_NULLABLE HT_(Kv) **out_kv) { +static HtRetCode insert_update(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, VALUE_TYPE value, bool update, RZ_OUT RZ_NULLABLE HT_(Kv) **out_kv) { ut32 key_len = calcsize_key(ht, key); HtRetCode rc; + HT_(Kv) *kv_dst = reserve_kv(ht, key, key_len, update, &rc); if (rc <= 0) { if (out_kv) { @@ -331,14 +688,21 @@ static int insert_update(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, VALUE_T } return rc; } + kv_dst->key = dupkey(ht, key); - kv_dst->key_len = key_len; kv_dst->value = dupval(ht, value); + +#ifdef VARIABLE_KEY_LEN + kv_dst->key_len = key_len; +#endif +#ifdef VARIABLE_VALUE_LEN kv_dst->value_len = calcsize_val(ht, value); - kv_dst = check_growing(ht, kv_dst); +#endif + if (out_kv) { *out_kv = kv_dst; } + return rc; } @@ -403,143 +767,196 @@ RZ_API HtRetCode Ht_(update_ex)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, } /** - * Update the key of an element that has \p old_key as key and replace it with \p new_key + * \brief This function decides if a slot should be marked as "empty" or "deleted" based on a heuristic. + * + * If the distance between the previous and following empty slots is < GROUP_WIDTH it is safe to mark the slot + * as "empty", otherwise we need to mark it as "deleted" (see references above for the difference between these two markers). + */ +static ut8 select_slot_type_for_deletion(RZ_NONNULL HtName_(Ht) *ht, INDEX_TYPE idx) { + // Decide if we should mark the slot as empty or deleted + INDEX_TYPE nearest_empty_before = 0; + INDEX_TYPE nearest_empty_after = 0; + + // Check up to `GROUP_WIDTH` preceeding control bytes + for (INDEX_TYPE i = 0; i < GROUP_WIDTH; i++) { + if (ht->ctrl[(idx - i - 1) & ht->capacity_mask] == H2_STATUS_EMPTY) { + break; + } + nearest_empty_before++; + } + + // Check up to `GROUP_WIDTH` following control bytes + for (INDEX_TYPE i = 0; i < GROUP_WIDTH; i++) { + if (ht->ctrl[(idx + i) & ht->capacity_mask] == H2_STATUS_EMPTY) { + break; + } + nearest_empty_after++; + } + + return nearest_empty_before + nearest_empty_after < GROUP_WIDTH ? H2_STATUS_EMPTY : H2_STATUS_DELETED; +} + +static bool internal_ht_delete(RZ_NONNULL HtName_(Ht) *ht, INDEX_TYPE idx) { + ut8 ctrl = select_slot_type_for_deletion(ht, idx); + + ht->size--; + ht->growth_left += ctrl == H2_STATUS_EMPTY ? 1 : 0; + + ctrl_table_set(ht, idx, ctrl); + fini_kv_pair(ht, HT_SLOT_AT(ht, idx)); + return true; +} + +/** + * \brief Update the key of an element that has \p old_key as key and replace it with \p new_key + * \param ht The hash table. + * \param old_key The key to update. + * \param new_key The new key. + * \return true if \p old_key was found and update, false otherwise. */ RZ_API bool Ht_(update_key)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE old_key, const KEY_TYPE new_key) { rz_return_val_if_fail(ht, false); + INDEX_TYPE idx; + ut32 old_key_size = calcsize_key(ht, old_key); + // First look for the value associated with old_key - bool found; - VALUE_TYPE value = Ht_(find)(ht, old_key, &found); - if (!found) { + if ((idx = ctrl_table_lookup(ht, old_key, old_key_size)) == INVALID_INDEX) { return false; } - // Associate the existing value with new_key - bool inserted = insert_update(ht, new_key, value, false, NULL) > 0; - if (!inserted) { + // Associate the new key with the existing value + if (insert_update(ht, new_key, HT_SLOT_AT(ht, idx)->value, false, NULL) <= 0) { return false; } - // Remove the old_key kv, paying attention to not double free the value - HT_(Bucket) *bt = &ht->table[bucketfn(ht, old_key)]; - const int old_key_len = calcsize_key(ht, old_key); - HT_(Kv) *kv; - ut32 j; + // Second lookup of the element associated with `old_key`, since the previous index could be invalidated by a resize + if ((idx = ctrl_table_lookup(ht, old_key, old_key_size)) == INVALID_INDEX) { + return false; + } - BUCKET_FOREACH(ht, bt, j, kv) { - if (is_kv_equal(ht, old_key, old_key_len, kv)) { - if (!ht->opt.dupvalue) { - // do not free the value part if dupvalue is not - // set, because the old value has been - // associated with the new key and it should not - // be freed - kv->value = HT_NULL_VALUE; - kv->value_len = 0; - } - fini_kv_pair(ht, kv); + // Do not free the value part if dupvalue is not set, because the old value will be + // associated with the new key and it should not be freed + if (!ht->opt.dupvalue) { + HT_SLOT_AT(ht, idx)->value = HT_NULL_VALUE; +#ifdef VARIABLE_VALUE_LEN + HT_SLOT_AT(ht, idx)->value_len = 0; +#endif + } - void *src = next_kv(ht, kv); - memmove(kv, src, (bt->count - j - 1) * ht->opt.elem_size); - bt->count--; - ht->count--; - return true; + return internal_ht_delete(ht, idx); +} + +static inline RZ_BORROW HT_(Kv) *internal_find_kv(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, RZ_NULLABLE bool *found) { + INDEX_TYPE idx = ctrl_table_lookup(ht, key, calcsize_key(ht, key)); + + if (idx == INVALID_INDEX) { + if (found) { + *found = false; } + return NULL; } - return false; + if (found) { + *found = true; + } + + return HT_SLOT_AT(ht, idx); } /** - * Returns the corresponding Kv entry from \p key. - * If \p found is not NULL, it will be set to true if the entry was found, - * false otherwise. + * \brief Returns the corresponding KV entry from \p key. + * \param ht The hash table. + * \param key The key to look up. + * \param[out] found Pointer to a bool, that would receive a value indicating if the key was found or not (optional). + * \return If the key is found, the function will return a pointer to its KV entry, or `NULL` otherwise. + * If \p found is not NULL, it will be set to true if the entry was found, false otherwise. */ RZ_API RZ_BORROW HT_(Kv) *Ht_(find_kv)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, RZ_NULLABLE bool *found) { - if (found) { - *found = false; - } rz_return_val_if_fail(ht, NULL); - - HT_(Bucket) *bt = &ht->table[bucketfn(ht, key)]; - ut32 key_len = calcsize_key(ht, key); - HT_(Kv) *kv; - ut32 j; - - BUCKET_FOREACH(ht, bt, j, kv) { - if (is_kv_equal(ht, key, key_len, kv)) { - if (found) { - *found = true; - } - return kv; - } - } - return NULL; + return internal_find_kv(ht, key, found); } /** - * Looks up the corresponding value from \p key. - * If \p found is not NULL, it will be set to true if the entry was found, - * false otherwise. + * \brief Looks up the corresponding value from \p key. + * + * If \p found is not NULL, it will be set to true if the entry was found, false otherwise. + * + * \param ht The hash table. + * \param key The key to look up. + * \param[out] found Pointer to a bool, that would receive a value indicating if the key was found or not (optional). + * \return If the key is found, the function will return the value associated with the key, or `HT_NULL_VALUE` otherwise. */ RZ_API VALUE_TYPE Ht_(find)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, RZ_NULLABLE bool *found) { - HT_(Kv) *res = Ht_(find_kv)(ht, key, found); + rz_return_val_if_fail(ht, HT_NULL_VALUE); + HT_(Kv) *res = internal_find_kv(ht, key, found); return res ? res->value : HT_NULL_VALUE; } /** - * Deletes an entry from the hash table \p ht with key \p key, if the pair exists. + * \brief Deletes an entry from the hash table \p ht with key \p key, if the pair exists. + * \param ht The hash table. + * \param key The key to delete. + * \return true on success, false otherwise. */ RZ_API bool Ht_(delete)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key) { rz_return_val_if_fail(ht, false); - HT_(Bucket) *bt = &ht->table[bucketfn(ht, key)]; - ut32 key_len = calcsize_key(ht, key); - HT_(Kv) *kv; - ut32 j; + INDEX_TYPE idx = ctrl_table_lookup(ht, key, calcsize_key(ht, key)); - BUCKET_FOREACH(ht, bt, j, kv) { - if (is_kv_equal(ht, key, key_len, kv)) { - fini_kv_pair(ht, kv); - void *src = next_kv(ht, kv); - memmove(kv, src, (bt->count - j - 1) * ht->opt.elem_size); - bt->count--; - ht->count--; - return true; - } + if (idx == INVALID_INDEX) { + return false; } - return false; + + return internal_ht_delete(ht, idx); } /** - * Apply \p cb for each KV pair in \p ht. - * If \p cb returns false, the iteration is stopped. + * \brief Apply \p cb for each KV pair in \p ht. If \p cb returns false, the iteration is stopped. + * \param ht The hash table. + * \param cb The callback function to invoke. + * \param user Pointer to user data (passed through to the callback). */ RZ_API void Ht_(foreach)(RZ_NONNULL HtName_(Ht) *ht, RZ_NONNULL HT_(ForeachCallback) cb, RZ_NULLABLE void *user) { rz_return_if_fail(ht && cb); - ut32 i; - for (i = 0; i < ht->size; ++i) { - HT_(Bucket) *bt = &ht->table[i]; - HT_(Kv) *kv; - ut32 j, count; - - BUCKET_FOREACH_SAFE(ht, bt, j, count, kv) { - if (!cb(user, kv->key, kv->value)) { - return; - } + // Iterate all slots + HT_FOREACH(ht, kv, { + if (!cb(user, kv->key, kv->value)) { + return; } - } + }); +} + +/** + * \brief Iterates all elements inside \p ht and invokes the callback function \p cb for each element. + * + * This function is similar to `Ht_(foreach)`, but the key/value are passed as pointer rather than by value. + * + * \param ht The hash table. + * \param cb The callback function to invoke (returning `false` will cancel further iteration). + * \param user Pointer to user data (passed through to the callback). + * \return true if all elements were iterated, false if the iteration was cancelled by the user callback + */ +RZ_API bool Ht_(foreach_kv)(RZ_NONNULL HtName_(Ht) *ht, RZ_NONNULL HT_(ForeachKvCallback) cb, RZ_NULLABLE void *user) { + rz_return_val_if_fail(ht && cb, false); + + // Iterate all slots + HT_FOREACH(ht, kv, { + if (!cb(user, kv)) { + return false; + } + }); + return true; } /** * \brief Returns the number of elements stored in the hash map \p ht. * * \param ht The hash map. - * * \return The number of elements saved in the hash map. */ RZ_API ut32 Ht_(size)(const RZ_NONNULL HtName_(Ht) *ht) { rz_return_val_if_fail(ht, 0); - return ht->count; + return ht->size; } /** @@ -549,29 +966,18 @@ RZ_API ut32 Ht_(size)(const RZ_NONNULL HtName_(Ht) *ht) { */ RZ_API RZ_BORROW VALUE_TYPE *Ht_(iter_next_mut)(RzIterator *it) { rz_return_val_if_fail(it, NULL); - HT_(IterMutState) *state = it->u; - if (state->ti >= state->ht->size) { - // Iteration is done. No elements left to select. - return NULL; - } + // Iterate over tables until a table with an element is found. - for (; state->ti < state->ht->size; state->ti++) { - if (state->ht->table[state->ti].count == 0) { - // Table has no elements. Check next table. + for (; state->ti < state->ht->capacity; state->ti++) { + if (H2_IS_EMPTY_OR_DELETED(state->ht->ctrl[state->ti])) { continue; } - if (state->bi < state->ht->table[state->ti].count) { - // Table has elements, select the element. - state->kv = &state->ht->table[state->ti].arr[state->bi]; - // For the next iteration, increment bucket index to the following element. - state->bi++; - return &state->kv->value; - } - // Reset bucket index to first bucket. - state->bi = 0; - // Go to next table + state->kv = HT_SLOT_AT(state->ht, state->ti); + state->ti++; + return &state->kv->value; } + // Iteration is done. No elements left to select. return NULL; } @@ -583,29 +989,18 @@ RZ_API RZ_BORROW VALUE_TYPE *Ht_(iter_next_mut)(RzIterator *it) { */ RZ_API const VALUE_TYPE *Ht_(iter_next)(RzIterator *it) { rz_return_val_if_fail(it, NULL); - HT_(IterState) *state = it->u; - if (state->ti >= state->ht->size) { - // Iteration is done. No elements left to select. - return NULL; - } + // Iterate over tables until a table with an element is found. - for (; state->ti < state->ht->size; state->ti++) { - if (state->ht->table[state->ti].count == 0) { - // Table has no elements. Check next table. + for (; state->ti < state->ht->capacity; state->ti++) { + if (H2_IS_EMPTY_OR_DELETED(state->ht->ctrl[state->ti])) { continue; } - if (state->bi < state->ht->table[state->ti].count) { - // Table has elements, select the element. - state->kv = &state->ht->table[state->ti].arr[state->bi]; - // For the next iteration, increment bucket index to the following element. - state->bi++; - return (const VALUE_TYPE *)&state->kv->value; - } - // Reset bucket index to first bucket. - state->bi = 0; - // Go to next table + state->kv = HT_SLOT_AT(state->ht, state->ti); + state->ti++; + return (const VALUE_TYPE *)&state->kv->value; } + // Iteration is done. No elements left to select. return NULL; } @@ -618,29 +1013,18 @@ RZ_API const VALUE_TYPE *Ht_(iter_next)(RzIterator *it) { */ RZ_API const KEY_TYPE *Ht_(iter_next_key)(RzIterator *it) { rz_return_val_if_fail(it, NULL); + HT_(IterMutState) *state = it->u; - HT_(IterState) *state = it->u; - if (state->ti >= state->ht->size) { - // Iteration is done. No elements left to select. - return NULL; - } // Iterate over tables until a table with an element is found. - for (; state->ti < state->ht->size; state->ti++) { - if (state->ht->table[state->ti].count == 0) { - // Table has no elements. Check next table. + for (; state->ti < state->ht->capacity; state->ti++) { + if (H2_IS_EMPTY_OR_DELETED(state->ht->ctrl[state->ti])) { continue; } - if (state->bi < state->ht->table[state->ti].count) { - // Table has elements, select the element. - state->kv = &state->ht->table[state->ti].arr[state->bi]; - // For the next iteration, increment bucket index to the following element. - state->bi++; - return (const KEY_TYPE *)&state->kv->key; - } - // Reset bucket index to first bucket. - state->bi = 0; - // Go to next table + state->kv = HT_SLOT_AT(state->ht, state->ti); + state->ti++; + return (const KEY_TYPE *)&state->kv->key; } + // Iteration is done. No elements left to select. return NULL; } @@ -685,6 +1069,9 @@ RZ_API RZ_OWN RzIterator /* */ *Ht_(as_iter_mut)(RZ_NONNULL HtName } RzIterator *iter = rz_iterator_new((rz_iterator_next_cb)Ht_(iter_next_mut), NULL, (rz_iterator_free_cb)Ht_(free_iter_mut_state), state); + if (!iter) { + Ht_(free_iter_mut_state)(state); + } return iter; } @@ -701,6 +1088,9 @@ RZ_API RZ_OWN RzIterator /* */ *Ht_(as_iter)(const RZ_NONNULL HtNa rz_return_val_if_fail(state, NULL); RzIterator *iter = rz_iterator_new((rz_iterator_next_cb)Ht_(iter_next), NULL, (rz_iterator_free_cb)Ht_(free_iter_state), state); + if (!iter) { + Ht_(free_iter_state)(state); + } return iter; } @@ -717,5 +1107,8 @@ RZ_API RZ_OWN RzIterator /* */ *Ht_(as_iter_keys)(const RZ_NONNULL rz_return_val_if_fail(state, NULL); RzIterator *iter = rz_iterator_new((rz_iterator_next_cb)Ht_(iter_next_key), NULL, (rz_iterator_free_cb)Ht_(free_iter_state), state); + if (!iter) { + Ht_(free_iter_state)(state); + } return iter; -} +} \ No newline at end of file diff --git a/librz/util/ht/ht_sp.c b/librz/util/ht/ht_sp.c index 661cbcb4df..779064836c 100644 --- a/librz/util/ht/ht_sp.c +++ b/librz/util/ht/ht_sp.c @@ -33,7 +33,7 @@ static void fini_kv_val(HT_(Kv) *kv, void *user) { RZ_API RZ_OWN HtName_(Ht) *Ht_(new)(HtStrOption key_opt, RZ_NULLABLE HT_(DupValue) dup_val, RZ_NULLABLE HT_(FreeValue) free_val) { HT_(Options) opt = { .cmp = (HT_(Comparator))strcmp, - .hashfn = (HT_(HashFunction))sdb_hash, + .hashfn = NULL, .dupkey = key_opt == HT_STR_DUP ? (HT_(DupKey))rz_str_dup : NULL, .dupvalue = dup_val, .calcsizeK = (HT_(CalcSizeK))strlen, @@ -42,5 +42,5 @@ RZ_API RZ_OWN HtName_(Ht) *Ht_(new)(HtStrOption key_opt, RZ_NULLABLE HT_(DupValu .finiKV_user = (void *)free_val, .elem_size = 0, }; - return internal_ht_new(ht_primes_sizes[0], 0, &opt); + return internal_ht_new(0, &opt); } diff --git a/librz/util/ht/ht_ss.c b/librz/util/ht/ht_ss.c index aa69d92aa2..69ef1ec893 100644 --- a/librz/util/ht/ht_ss.c +++ b/librz/util/ht/ht_ss.c @@ -32,7 +32,7 @@ static void fini_kv_val(HT_(Kv) *kv, void *user) { RZ_API RZ_OWN HtName_(Ht) *Ht_(new)(HtStrOption key_opt, HtStrOption val_opt) { HT_(Options) opt = { .cmp = (HT_(Comparator))strcmp, - .hashfn = (HT_(HashFunction))sdb_hash, + .hashfn = NULL, .dupkey = key_opt == HT_STR_DUP ? (HT_(DupKey))rz_str_dup : NULL, .dupvalue = val_opt == HT_STR_DUP ? (HT_(DupValue))rz_str_dup : NULL, .calcsizeK = (HT_(CalcSizeK))strlen, @@ -41,5 +41,5 @@ RZ_API RZ_OWN HtName_(Ht) *Ht_(new)(HtStrOption key_opt, HtStrOption val_opt) { .finiKV_user = val_opt == HT_STR_CONST ? NULL : (HT_(FreeValue))free, .elem_size = 0, }; - return internal_ht_new(ht_primes_sizes[0], 0, &opt); + return internal_ht_new(0, &opt); } diff --git a/librz/util/ht/ht_su.c b/librz/util/ht/ht_su.c index b863fed4c5..3786c427cb 100644 --- a/librz/util/ht/ht_su.c +++ b/librz/util/ht/ht_su.c @@ -20,7 +20,7 @@ static void fini_kv_key(HT_(Kv) *kv, RZ_UNUSED void *user) { RZ_API RZ_OWN HtName_(Ht) *Ht_(new)(HtStrOption key_opt) { HT_(Options) opt = { .cmp = (HT_(Comparator))strcmp, - .hashfn = (HT_(HashFunction))sdb_hash, + .hashfn = NULL, .dupkey = key_opt == HT_STR_DUP ? (HT_(DupKey))rz_str_dup : NULL, .dupvalue = NULL, .calcsizeK = (HT_(CalcSizeK))strlen, @@ -29,5 +29,5 @@ RZ_API RZ_OWN HtName_(Ht) *Ht_(new)(HtStrOption key_opt) { .finiKV_user = NULL, .elem_size = 0, }; - return internal_ht_new(ht_primes_sizes[0], 0, &opt); + return internal_ht_new(0, &opt); } diff --git a/librz/util/ht/ht_up.c b/librz/util/ht/ht_up.c index 23cc0eeb07..d3a9462276 100644 --- a/librz/util/ht/ht_up.c +++ b/librz/util/ht/ht_up.c @@ -36,7 +36,7 @@ static void init_options(HT_(Options) *opt, HT_(DupValue) valdup, HT_(FreeValue) RZ_API RZ_OWN HtName_(Ht) *Ht_(new)(RZ_NULLABLE HT_(DupValue) valdup, RZ_NULLABLE HT_(FreeValue) valfree) { HT_(Options) opt; init_options(&opt, valdup, valfree); - return internal_ht_new(ht_primes_sizes[0], 0, &opt); + return internal_ht_new(0, &opt); } /** diff --git a/librz/util/sdb/src/sdb.c b/librz/util/sdb/src/sdb.c index b554fe7899..37ebea8356 100644 --- a/librz/util/sdb/src/sdb.c +++ b/librz/util/sdb/src/sdb.c @@ -12,19 +12,6 @@ #include "sdb.h" #include "sdb_private.h" -static inline SdbKv *next_kv(HtSS *ht, SdbKv *kv) { - return (SdbKv *)((char *)kv + ht->opt.elem_size); -} - -#define BUCKET_FOREACH(ht, bt, j, kv) \ - for ((j) = 0, (kv) = (SdbKv *)(bt)->arr; j < (bt)->count; (j)++, (kv) = next_kv(ht, kv)) - -#define BUCKET_FOREACH_SAFE(ht, bt, j, count, kv) \ - if ((bt)->arr) \ - for ((j) = 0, (kv) = (SdbKv *)(bt)->arr, (count) = (ht)->count; \ - (j) < (bt)->count; \ - (j) = (count) == (ht)->count ? j + 1 : j, (kv) = (count) == (ht)->count ? next_kv(ht, kv) : kv, (count) = (ht)->count) - // TODO: use mmap instead of read.. much faster! RZ_API RZ_OWN Sdb *sdb_new0(void) { return sdb_new(NULL, NULL, 0); @@ -146,7 +133,7 @@ RZ_API bool sdb_isempty(Sdb *s) { return false; } } - if (s->ht && s->ht->count > 0) { + if (s->ht && ht_ss_size(s->ht) > 0) { return false; } } @@ -164,7 +151,7 @@ RZ_API int sdb_count(Sdb *s) { } } if (s->ht) { - count += s->ht->count; + count += ht_ss_size(s->ht); } } return count; @@ -241,7 +228,7 @@ RZ_API const char *sdb_const_get_len(Sdb *s, const char *key, int *vlen) { return NULL; } (void)cdb_findstart(&s->db); - if (cdb_findnext(&s->db, s->ht->opt.hashfn(key), key, keylen) < 1) { + if (cdb_findnext(&s->db, sdb_hash(key), key, keylen) < 1) { return NULL; } len = cdb_datalen(&s->db); @@ -752,19 +739,8 @@ RZ_API bool sdb_foreach(RZ_NONNULL Sdb *s, RZ_NONNULL SdbForeachCallback cb, RZ_ if (!result) { return sdb_foreach_end(s, false); } - - for (ut32 i = 0; i < s->ht->size; ++i) { - HtSSBucket *bt = &s->ht->table[i]; - SdbKv *kv; - ut32 j, count; - - BUCKET_FOREACH_SAFE(s->ht, bt, j, count, kv) { - if (kv && sdbkv_value(kv) && *sdbkv_value(kv)) { - if (!cb(user, kv)) { - return sdb_foreach_end(s, false); - } - } - } + if (!sdb_ht_foreach_kv(s->ht, cb, user)) { + return sdb_foreach_end(s, false); } return sdb_foreach_end(s, true); } @@ -778,9 +754,18 @@ static bool _insert_into_disk(void *user, const SdbKv *kv) { return false; } +static bool sdb_sync_foreach_cb(void *user, const SdbKv *kv) { + Sdb *s = user; + + if (sdb_disk_insert(s, sdbkv_key(kv), sdbkv_value(kv))) { + sdb_remove(s, sdbkv_key(kv)); + } + + return true; +} + RZ_API bool sdb_sync(Sdb *s) { bool result; - ut32 i; if (!s || !sdb_disk_create(s)) { return false; @@ -789,21 +774,7 @@ RZ_API bool sdb_sync(Sdb *s) { if (!result) { return false; } - - /* append new keyvalues */ - for (i = 0; i < s->ht->size; ++i) { - HtSSBucket *bt = &s->ht->table[i]; - SdbKv *kv; - ut32 j, count; - - BUCKET_FOREACH_SAFE(s->ht, bt, j, count, kv) { - if (sdbkv_key(kv) && sdbkv_value(kv) && *sdbkv_value(kv)) { - if (sdb_disk_insert(s, sdbkv_key(kv), sdbkv_value(kv))) { - sdb_remove(s, sdbkv_key(kv)); - } - } - } - } + sdb_ht_foreach_kv(s->ht, sdb_sync_foreach_cb, s); sdb_disk_finish(s); // TODO: sdb_reset memory state? return true; @@ -842,7 +813,7 @@ RZ_API bool sdb_stats(Sdb *s, ut32 *disk, ut32 *mem) { *disk = count; } if (mem) { - *mem = s->ht->count; + *mem = ht_ss_size(s->ht); } return disk || mem; } diff --git a/librz/util/sdb/src/sdb.h b/librz/util/sdb/src/sdb.h index a46675542d..6c3d44acb3 100644 --- a/librz/util/sdb/src/sdb.h +++ b/librz/util/sdb/src/sdb.h @@ -109,7 +109,7 @@ RZ_API void sdb_copy(Sdb *src, Sdb *dst); RZ_API bool sdb_stats(Sdb *s, ut32 *disk, ut32 *mem); -typedef bool (*SdbForeachCallback)(void *user, const SdbKv *kv); +typedef SdbHtForeachCallback SdbForeachCallback; RZ_API bool sdb_foreach(RZ_NONNULL Sdb *s, RZ_NONNULL SdbForeachCallback cb, RZ_NULLABLE void *user); RZ_API RZ_OWN RzPVector /**/ *sdb_get_items(RZ_NONNULL Sdb *s, bool sorted); RZ_API RZ_OWN RzPVector /**/ *sdb_get_items_filter(RZ_NONNULL Sdb *s, RZ_NONNULL SdbForeachCallback filter, RZ_NULLABLE void *user, bool sorted); diff --git a/librz/util/sdb/src/sdbht.c b/librz/util/sdb/src/sdbht.c index 6ed196100a..34e10577c3 100644 --- a/librz/util/sdb/src/sdbht.c +++ b/librz/util/sdb/src/sdbht.c @@ -3,6 +3,14 @@ #include "sdbht.h" +/** + * \brief A helper struct used for forwarding iteration (foreach) callbacks between `ht_` and the `sdb_` APIs. + */ +typedef struct { + SdbHtForeachCallback cb; + void *user; +} HtSSForeachKvCallbackRedirect; + RZ_API HtSS *sdb_ht_new(void) { HtSS *ht = ht_ss_new(HT_STR_DUP, HT_STR_DUP); if (ht) { @@ -61,3 +69,28 @@ RZ_API void sdb_ht_free(HtSS *ht) { RZ_API bool sdb_ht_delete(HtSS *ht, const char *key) { return ht_ss_delete(ht, key); } + +static bool sdb_ht_foreach_kv_filter(void *user, const HtSSKv *kv) { + SdbKv *sdb_kv = (SdbKv *)kv; + if (sdbkv_key(sdb_kv) && sdbkv_value(sdb_kv) && *sdbkv_value(sdb_kv)) { + HtSSForeachKvCallbackRedirect *redirect = user; + return redirect->cb(redirect->user, (SdbKv *)kv); + } + return true; +} + +/** + * \brief Iterates all elements of a `HtSS` hash table. + * + * \param ht The hash table. + * \param cb A callback to be invoked for each element. + * \param user Pointer to user data to be passed to the callback for each element. + * \return true if all elements were iterated, false if the iteration was cancelled by the user callback + */ +RZ_API bool sdb_ht_foreach_kv(RZ_NONNULL HtSS *ht, RZ_NONNULL SdbHtForeachCallback cb, RZ_NULLABLE void *user) { + HtSSForeachKvCallbackRedirect redirect = { + .cb = cb, + .user = user + }; + return ht_ss_foreach_kv(ht, sdb_ht_foreach_kv_filter, &redirect); +} \ No newline at end of file diff --git a/librz/util/sdb/src/sdbht.h b/librz/util/sdb/src/sdbht.h index b22f67967d..7243847a69 100644 --- a/librz/util/sdb/src/sdbht.h +++ b/librz/util/sdb/src/sdbht.h @@ -16,6 +16,8 @@ typedef struct sdb_kv { HtSSKv base; } SdbKv; +typedef bool (*SdbHtForeachCallback)(void *user, const SdbKv *kv); + static inline const char *sdbkv_key(const SdbKv *kv) { return kv->base.key; } @@ -54,6 +56,8 @@ RZ_API bool sdb_ht_delete(HtSS *ht, const char *key); RZ_API char *sdb_ht_find(HtSS *ht, const char *key, bool *found); // Find the KeyValuePair corresponding to the matching key. RZ_API SdbKv *sdb_ht_find_kvp(HtSS *ht, const char *key, bool *found); +// Iterate the hash table. +RZ_API bool sdb_ht_foreach_kv(RZ_NONNULL HtSS *ht, RZ_NONNULL SdbHtForeachCallback cb, RZ_NULLABLE void *user); #ifdef __cplusplus } diff --git a/meson.build b/meson.build index 1fcf4749db..264ef369b3 100644 --- a/meson.build +++ b/meson.build @@ -519,6 +519,13 @@ foreach it : ccs it_userconf.set10('HAVE_@0@'.format(func.to_upper()), ok) endforeach + # Try to compile a basic SSE2 code snippet to discover if SSE2 is supported for the target arch + sse2_code = '''#include + int main (int argc, char *argv[]) { __m128i v = _mm_set_epi32(2, 3, 0, 1); return 0; } + ''' + has_sse2 = it_cc.compiles(sse2_code, args : '-msse2', name: 'SSE2 code snippet') + it_userconf.set10('HAVE_SSE2', has_sse2) + foreach item : [ ['linux/ashmem.h', '', []], ['sys/shm.h', '', []], diff --git a/test/bench/bench_ht.c b/test/bench/bench_ht.c new file mode 100644 index 0000000000..2d291b2940 --- /dev/null +++ b/test/bench/bench_ht.c @@ -0,0 +1,443 @@ +// SPDX-FileCopyrightText: 2026 Anton Angelov +// SPDX-License-Identifier: LGPL-3.0-only + +#include "bench_utils.h" +#include +#include +#include + +/** + * \file bench_ht.c + * \brief Benchmark for hash table functions (`ht_*`) + */ + +#define ITERATION_COUNT 2000000 +#define SHUFFLE_MULTIPLIER 1037 + +static bool ht_pu_foreach_cb(RZ_UNUSED ut64 *i, RZ_UNUSED const void *key, RZ_UNUSED const ut64 value) { + return true; +} + +/** + * Used for generating a pseudo-random value based on iteration number (for randomizing HtUU keys) + */ +static inline ut64 splitmix64(ut64 v) { + uint64_t z = (v + 0x9E3779B97F4A7C15ULL); + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; + return z ^ (z >> 31); +} + +/** + * A sample object to be used as a key for a HtPU hash table + */ +typedef struct { + ut32 a; + ut32 b; +} PUKey; + +static inline PUKey make_pu_key(ut64 iteration) { + PUKey result; + result.a = iteration * 1000; + result.b = splitmix64(iteration); + return result; +} + +/** + * Reshuffles a key in order to do the following (in a deterministic way): + * - avoid sequential key insert/lookup + * - create 87.5% chance of lookup hit vs 12.5% chance of miss + * - make 10% of the elements (hot zone) to be requested 75.5% of the time + */ +static ut64 reshuffle_key(ut64 index, ut64 max_value, ut64 unexistent_key) { + const ut64 hot_zone_denom = 10; // 10% of the hash table elements are considered in the hot zone + ut64 hot_zone_size = max_value / hot_zone_denom; + + switch (index % 8) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + // 75.5% hot zone (6/8) + return (index * SHUFFLE_MULTIPLIER) % hot_zone_size * hot_zone_denom; + case 6: + // 12.5% cold zone (1/8) + return (index * SHUFFLE_MULTIPLIER) % max_value; + case 7: + // 12.5% miss rate (1/8) + return unexistent_key; + default: + rz_warn_if_reached(); + return 0; + } +} + +static ut32 pu_key_hash(void *p) { + PUKey *key = p; + return key->a ^ key->b; +} + +static int pu_key_cmp(void *p1, void *p2) { + PUKey *key1 = p1; + PUKey *key2 = p2; + + if (key1->a < key2->a) { + return -1; + } + if (key1->a > key2->a) { + return 1; + } + if (key1->b < key2->b) { + return -1; + } + if (key1->b > key2->b) { + return 1; + } + return 0; +} + +static void bench_rz_ht_pu_combined(RzTable *t_out) { + ut64 temp = 0; + HtPUOptions pu_opt = { 0 }; + pu_opt.hashfn = (HtPUHashFunction)pu_key_hash; + pu_opt.cmp = (HtPUComparator)pu_key_cmp; + PUKey *keys = malloc(sizeof(PUKey) * ITERATION_COUNT); + + // Generate keys + for (ut64 i = 0; i < ITERATION_COUNT; i++) { + keys[i] = make_pu_key(i); + } + + // Insert + { + HtPU *ht = ht_pu_new_opt(&pu_opt); + RZ_BENCH_RUN_I("[HtPU] insert", i, t_out, ITERATION_COUNT, { + ht_pu_insert(ht, &keys[i], i); + }); + RZ_BENCH_RUN_I("[HtPU] delete", i, t_out, ITERATION_COUNT, { + ht_pu_delete(ht, &keys[i]); + }); + ht_pu_free(ht); + } + + // Lookup (small, medium, large hash tables) and iterate + { + HtPU *ht_100 = ht_pu_new_opt(&pu_opt); + HtPU *ht_1k = ht_pu_new_opt(&pu_opt); + HtPU *ht_10k = ht_pu_new_opt(&pu_opt); + HtPU *ht_100k = ht_pu_new_opt(&pu_opt); + HtPU *ht_1m = ht_pu_new_opt(&pu_opt); + + for (ut64 i = 0; i < 1000000; i++) { + PUKey *key = &keys[i]; + if (i < 100) { + ht_pu_insert(ht_100, key, i); + } + if (i < 1000) { + ht_pu_insert(ht_1k, key, i); + } + if (i < 10000) { + ht_pu_insert(ht_10k, key, i); + } + if (i < 100000) { + ht_pu_insert(ht_100k, key, i); + } + ht_pu_insert(ht_1m, key, i); + } + + RZ_BENCH_RUN("[HtPU] iterate (100 elements)", t_out, ITERATION_COUNT, { + ht_pu_foreach(ht_100, (HtPUForeachCallback)ht_pu_foreach_cb, &temp); + }); + RZ_BENCH_RUN_I("[HtPU] lookup (100 elements)", i, t_out, ITERATION_COUNT, { + PUKey temp_key = make_pu_key(reshuffle_key(i, 100, UT64_MAX)); + ut64 result = ht_pu_find(ht_100, &temp_key, NULL); + }); + RZ_BENCH_RUN_I("[HtPU] lookup (1k elements)", i, t_out, ITERATION_COUNT, { + PUKey temp_key = make_pu_key(reshuffle_key(i, 1000, UT64_MAX)); + ut64 result = ht_pu_find(ht_1k, &temp_key, NULL); + }); + RZ_BENCH_RUN_I("[HtPU] lookup (10k elements)", i, t_out, ITERATION_COUNT, { + PUKey temp_key = make_pu_key(reshuffle_key(i, 10000, UT64_MAX)); + ut64 result = ht_pu_find(ht_10k, &temp_key, NULL); + }); + RZ_BENCH_RUN_I("[HtPU] lookup (100k elements)", i, t_out, ITERATION_COUNT, { + PUKey temp_key = make_pu_key(reshuffle_key(i, 100000, UT64_MAX)); + ut64 result = ht_pu_find(ht_100k, &temp_key, NULL); + }); + RZ_BENCH_RUN_I("[HtPU] lookup (1M elements)", i, t_out, ITERATION_COUNT, { + PUKey temp_key = make_pu_key(reshuffle_key(i, 1000000, UT64_MAX)); + ut64 result = ht_pu_find(ht_1m, &temp_key, NULL); + }); + + ht_pu_free(ht_100); + ht_pu_free(ht_1k); + ht_pu_free(ht_10k); + ht_pu_free(ht_100k); + ht_pu_free(ht_1m); + } + + free(keys); +} + +static char *generate_su_key(ut64 index) { + char buffer[UT8_MAX]; + + // Try to mimic real world string keys + switch (index % 8) { + case 0: + snprintf(buffer, UT8_MAX, "user_%" PRIx64, index); // user id + break; + case 1: + snprintf(buffer, UT8_MAX, "session_%" PRIx64 "%" PRIx64, index, index * 7919); // session token + break; + case 2: { + const char *sections[] = { "analysis", "asm", "scr", "graph", "str" }; + const char *keys[] = { "flags", "prefix", "editor", "times", "server" }; + snprintf(buffer, UT8_MAX, "config.%s.%s", sections[index % 5], keys[(index / 5) % 5]); // config keys + break; + } + case 3: { + const char *headers[] = { "content-type", "content-length", "authorization", "user-agent", "accept", "accept-encoding", + "cache-control", "connection", "host", "cookie", "referer", "accept-language", + "x-forwarded-for", "x-request-id", "etag" }; + snprintf(buffer, UT8_MAX, "%s-%" PRIx64, headers[index % 15], index / 15); // http header + break; + } + case 4: { + const char *dirs[] = { "/home/user/documents", "/var/log", "/etc/config", "/usr/local/bin", "/tmp/cache" }; + snprintf(buffer, UT8_MAX, "%s/file %" PRIx64 ".txt", dirs[index % 5], index); + break; + } + case 5: { + const char *domains[] = { "example.com", "test.org", "mail.net", "company.io" }; + snprintf(buffer, UT8_MAX, "user%" PRIx64 "@%s", index, domains[index % 4]); // email + break; + } + case 6: { + snprintf(buffer, UT8_MAX, "%08" PRIx64 "-%04" PRIx64 "-%04" PRIx64 "-%04" PRIx64 "-%012" PRIx64, + index, + (index >> 16) & 0xFFFF, + (index >> 8) & 0xFFFF, + (index >> 4) & 0xFFFF, + (index * 2654435761u)); // GUID + break; + } + case 7: { + // alphanumeric code with length of 2 to 64 characters + const char chars[] = "abcdefghijklmnopqrstuvwxyz0123456789"; + int len = 2 + (index % 62); + + for (ut64 i = 0; i < len && i < UT8_MAX - 1; i++) { + buffer[i] = chars[(index + i * 7) % 36]; + } + buffer[len] = '\0'; + break; + } + default: + rz_warn_if_reached(); + } + + return strdup(buffer); +} + +static bool ht_su_foreach_cb(RZ_UNUSED ut64 *i, RZ_UNUSED const char *key, RZ_UNUSED const ut64 value) { + return true; +} + +static void bench_rz_ht_su_combined(RzTable *t_out) { + char **precomputed_keys = malloc((ITERATION_COUNT + 1) * sizeof(char *)); + + // Generate test keys + for (ut64 i = 0; i < ITERATION_COUNT; i++) { + precomputed_keys[i] = generate_su_key(i); + } + precomputed_keys[ITERATION_COUNT] = "non-existent"; + + // Insert + { + HtSU *ht = ht_su_new(HT_STR_CONST); + RZ_BENCH_RUN_I("[HtSU] insert", i, t_out, ITERATION_COUNT, { + ht_su_insert(ht, precomputed_keys[i], i); + }); + RZ_BENCH_RUN_I("[HtSU] delete", i, t_out, ITERATION_COUNT, { + ht_su_delete(ht, precomputed_keys[i]); + }); + ht_su_free(ht); + } + + // Lookup (small, medium, large hash tables) and iterate + { + HtSU *ht_100 = ht_su_new(HT_STR_CONST); + HtSU *ht_1k = ht_su_new(HT_STR_CONST); + HtSU *ht_10k = ht_su_new(HT_STR_CONST); + HtSU *ht_100k = ht_su_new(HT_STR_CONST); + HtSU *ht_1m = ht_su_new(HT_STR_CONST); + + for (ut64 i = 0; i < 1000000; i++) { + if (i < 100) { + ht_su_insert(ht_100, precomputed_keys[i], i); + } + if (i < 1000) { + ht_su_insert(ht_1k, precomputed_keys[i], i); + } + if (i < 10000) { + ht_su_insert(ht_10k, precomputed_keys[i], i); + } + if (i < 100000) { + ht_su_insert(ht_100k, precomputed_keys[i], i); + } + ht_su_insert(ht_1m, precomputed_keys[i], i); + } + + RZ_BENCH_RUN("[HtSU] iterate (100 elements)", t_out, ITERATION_COUNT, { + ht_su_foreach(ht_100, (HtSUForeachCallback)ht_su_foreach_cb, NULL); + }); + RZ_BENCH_RUN_I("[HtSU] lookup (100 elements)", i, t_out, ITERATION_COUNT, { + const char *key = precomputed_keys[reshuffle_key(i, 100, ITERATION_COUNT)]; + ut64 result = ht_su_find(ht_100, key, NULL); + }); + RZ_BENCH_RUN_I("[HtSU] lookup (1k elements)", i, t_out, ITERATION_COUNT, { + const char *key = precomputed_keys[reshuffle_key(i, 1000, ITERATION_COUNT)]; + ut64 result = ht_su_find(ht_1k, key, NULL); + }); + RZ_BENCH_RUN_I("[HtSU] lookup (10k elements)", i, t_out, ITERATION_COUNT, { + const char *key = precomputed_keys[reshuffle_key(i, 10000, ITERATION_COUNT)]; + ut64 result = ht_su_find(ht_10k, key, NULL); + }); + RZ_BENCH_RUN_I("[HtSU] lookup (100k elements)", i, t_out, ITERATION_COUNT, { + const char *key = precomputed_keys[reshuffle_key(i, 100000, ITERATION_COUNT)]; + ut64 result = ht_su_find(ht_100k, key, NULL); + }); + RZ_BENCH_RUN_I("[HtSU] lookup (1M elements)", i, t_out, ITERATION_COUNT, { + const char *key = precomputed_keys[reshuffle_key(i, 1000000, ITERATION_COUNT)]; + ut64 result = ht_su_find(ht_1m, key, NULL); + }); + + ht_su_free(ht_100); + ht_su_free(ht_1k); + ht_su_free(ht_10k); + ht_su_free(ht_100k); + ht_su_free(ht_1m); + } + + // Free precomputed keys + for (ut64 i = 0; i < ITERATION_COUNT; i++) { + free(precomputed_keys[i]); + } + free(precomputed_keys); +} + +static bool ht_uu_foreach_cb(RZ_UNUSED ut64 *i, RZ_UNUSED const ut64 key, RZ_UNUSED const ut64 value) { + return true; +} + +static void bench_rz_ht_uu_combined(RzTable *t_out) { + HtUU *ht = NULL; + + // Insert + { + ht = ht_uu_new(); + RZ_BENCH_RUN_I("[HtUU] insert", i, t_out, ITERATION_COUNT, { + ht_uu_insert(ht, splitmix64(i), i); + }); + RZ_BENCH_RUN_I("[HtUU] delete", i, t_out, ITERATION_COUNT, { + ht_uu_delete(ht, splitmix64(i)); // reshuffle keys + }); + ht_uu_free(ht); + } + + // Lookup 100 elements (and iterate) + { + const ut64 size = 100; + ht = ht_uu_new(); + for (ut64 i = 0; i < size; i++) { + ht_uu_insert(ht, splitmix64(i), i); + } + + RZ_BENCH_RUN("[HtUU] iterate (100 elements)", t_out, ITERATION_COUNT, { + ht_uu_foreach(ht, (HtUUForeachCallback)ht_uu_foreach_cb, NULL); + }); + + RZ_BENCH_RUN_I("[HtUU] lookup (100 elements)", i, t_out, ITERATION_COUNT, { + ht_uu_find(ht, splitmix64(reshuffle_key(i, size, size)), NULL); + }); + + ht_uu_free(ht); + } + + // Lookup 1k + { + const ut64 size = 1000; + ht = ht_uu_new(); + for (ut64 i = 0; i < size; i++) { + ht_uu_insert(ht, splitmix64(i), i); + } + + RZ_BENCH_RUN_I("[HtUU] lookup (1k elements)", i, t_out, ITERATION_COUNT, { + ht_uu_find(ht, splitmix64(reshuffle_key(i, size, size)), NULL); + }); + + ht_uu_free(ht); + } + + // Lookup 10k + { + const ut64 size = 10000; + ht = ht_uu_new(); + for (ut64 i = 0; i < size; i++) { + ht_uu_insert(ht, splitmix64(i), i); + } + + RZ_BENCH_RUN_I("[HtUU] lookup (10k elements)", i, t_out, ITERATION_COUNT, { + ht_uu_find(ht, splitmix64(reshuffle_key(i, size, size)), NULL); + }); + + ht_uu_free(ht); + } + + // Lookup 100k + { + const ut64 size = 100000; + ht = ht_uu_new(); + for (ut64 i = 0; i < size; i++) { + ht_uu_insert(ht, splitmix64(i), i); + } + + RZ_BENCH_RUN_I("[HtUU] lookup (100k elements)", i, t_out, ITERATION_COUNT, { + ht_uu_find(ht, splitmix64(reshuffle_key(i, size, size)), NULL); + }); + + ht_uu_free(ht); + } + + // Lookup large + { + const ut64 size = 1000000; + ht = ht_uu_new(); + for (ut64 i = 0; i < size; i++) { + ht_uu_insert(ht, splitmix64(i), i); + } + + RZ_BENCH_RUN_I("[HtUU] lookup (1M elements)", i, t_out, ITERATION_COUNT, { + ht_uu_find(ht, splitmix64(reshuffle_key(i, size, size)), NULL); + }); + + ht_uu_free(ht); + } +} + +int main() { + RzTable *t = rz_table_new(); + RZ_BENCH_TABLE_INIT(t); + + // Micro benchmarks + bench_rz_ht_pu_combined(t); + bench_rz_ht_su_combined(t); + bench_rz_ht_uu_combined(t); + + // Print results + RZ_BENCH_TABLE_PRINT_AND_FREE(t); + return 0; +} \ No newline at end of file diff --git a/test/bench/bench_utils.h b/test/bench/bench_utils.h index 94c2e57771..0043753059 100644 --- a/test/bench/bench_utils.h +++ b/test/bench/bench_utils.h @@ -44,6 +44,18 @@ RZ_API void rz_bench_report(RZ_NONNULL RzBenchCtx *ctx, RZ_NONNULL RzTable *t); rz_bench_report(&ctx, table); \ } while (0) +#define RZ_BENCH_RUN_I(name, i, table, iterations, code) \ + do { \ + RzBenchCtx ctx; \ + rz_bench_init(&ctx, name, iterations); \ + rz_bench_start(&ctx); \ + for (ut64(i) = 0; (i) < iterations; (i)++) { \ + code; \ + } \ + rz_bench_end(&ctx); \ + rz_bench_report(&ctx, table); \ + } while (0) + /** * \brief Initializes the RzTable \p T used for storing results of microbenchmarks. * \param T table to initialize. diff --git a/test/bench/meson.build b/test/bench/meson.build index 0a9cf468b3..644795666a 100644 --- a/test/bench/meson.build +++ b/test/bench/meson.build @@ -21,6 +21,7 @@ if get_option('enable_benchmarks') benchmarks = [ 'bitvector', 'il', + 'ht' ] # Create benchmark executables diff --git a/test/db/analysis/x86_64 b/test/db/analysis/x86_64 index ab4577136c..af79debaaf 100644 --- a/test/db/analysis/x86_64 +++ b/test/db/analysis/x86_64 @@ -4240,10 +4240,10 @@ arg int64_t arg2 @ rsi void fcn.00010270(int64_t arg1, int64_t arg2, const char **s, int64_t arg4, int64_t arg5); var const char *s2 @ stack - 0xc8 var void *s1 @ stack - 0xc0 -var unsigned long long var_b8h @ stack - 0xb8 +var unsigned long var_b8h @ stack - 0xb8 var int64_t var_b0h @ stack - 0xb0 var int64_t var_a8h @ stack - 0xa8 -var unsigned long long var_98h @ stack - 0x98 +var unsigned long var_98h @ stack - 0x98 var int64_t var_94h @ stack - 0x94 var int64_t var_90h @ stack - 0x90 var int64_t var_88h @ stack - 0x88 diff --git a/test/db/archos/linux-x64/debuginfod b/test/db/archos/linux-x64/debuginfod index 6b73da26cb..7afcf96989 100644 --- a/test/db/archos/linux-x64/debuginfod +++ b/test/db/archos/linux-x64/debuginfod @@ -69,7 +69,7 @@ EXPECT=<>","addr":4196496,"type":"DEFAULT"},{"name":"endl>","addr":4196528,"type":"DEFAULT"}]},{"name":"std::ios_base::Init","bases":[],"vtables":[],"methods":[{"name":"Init","addr":4196432,"type":"DEFAULT"},{"name":"~Init","addr":4196480,"type":"DEFAULT"}]},{"name":"std","bases":[],"vtables":[],"methods":[]},{"name":"std::ostream","bases":[],"vtables":[],"methods":[{"name":"operator<<","addr":4196400,"type":"DEFAULT"},{"name":"operator<<","addr":4196512,"type":"DEFAULT"}]},{"name":"A","bases":[],"vtables":[{"id":"0","addr":4197672,"offset":0}],"methods":[{"name":"greet","addr":4197064,"type":"VIRTUAL","vtable_offset":0},{"name":"printValue","addr":4197108,"type":"VIRTUAL","vtable_offset":8},{"name":"A","addr":4197174,"type":"CONSTRUCTOR"}]},{"name":"C","bases":[{"id":"0","name":"A","offset":0}],"vtables":[{"id":"0","addr":4197608,"offset":0}],"methods":[{"name":"C","addr":4197316,"type":"CONSTRUCTOR"},{"name":"printValue","addr":4197368,"type":"VIRTUAL","vtable_offset":8},{"name":"method.A.greet","addr":4197064,"type":"VIRTUAL","vtable_offset":0}]}] +[{"name":"std::ios_base::Init","bases":[],"vtables":[],"methods":[{"name":"Init","addr":4196432,"type":"DEFAULT"},{"name":"~Init","addr":4196480,"type":"DEFAULT"}]},{"name":"std::ostream","bases":[],"vtables":[],"methods":[{"name":"operator<<","addr":4196400,"type":"DEFAULT"},{"name":"operator<<","addr":4196512,"type":"DEFAULT"}]},{"name":"std","bases":[],"vtables":[],"methods":[]},{"name":"C","bases":[{"id":"0","name":"A","offset":0}],"vtables":[{"id":"0","addr":4197608,"offset":0}],"methods":[{"name":"C","addr":4197316,"type":"CONSTRUCTOR"},{"name":"printValue","addr":4197368,"type":"VIRTUAL","vtable_offset":8},{"name":"method.A.greet","addr":4197064,"type":"VIRTUAL","vtable_offset":0}]},{"name":"std::basic_ostream_char__std::char_traits_char____std","bases":[],"vtables":[],"methods":[{"name":"operator<<>","addr":4196496,"type":"DEFAULT"},{"name":"endl>","addr":4196528,"type":"DEFAULT"}]},{"name":"A","bases":[],"vtables":[{"id":"0","addr":4197672,"offset":0}],"methods":[{"name":"greet","addr":4197064,"type":"VIRTUAL","vtable_offset":0},{"name":"printValue","addr":4197108,"type":"VIRTUAL","vtable_offset":8},{"name":"A","addr":4197174,"type":"CONSTRUCTOR"}]},{"name":"B","bases":[{"id":"0","name":"A","offset":0}],"vtables":[{"id":"0","addr":4197640,"offset":0}],"methods":[{"name":"B","addr":4197198,"type":"CONSTRUCTOR"},{"name":"printValue","addr":4197250,"type":"VIRTUAL","vtable_offset":8},{"name":"method.A.greet","addr":4197064,"type":"VIRTUAL","vtable_offset":0}]}] EOF RUN @@ -568,7 +568,7 @@ aaa aclj EOF EXPECT=< 0x00400c00 mov ecx, dword [var_24h] | | 0x00400c03 movsxd rax, ecx @@ -212,7 +212,7 @@ EXPECT=< 0x00400c5c mov edi, 0x08 | || 0x00400c61 call sym.imp.operator_new_unsigned_long @@ -226,7 +226,7 @@ EXPECT=< 0x00400c8c mov rax, qword [var_20h.var_20h] | 0x00400c90 mov rax, qword [rax] @@ -234,7 +234,7 @@ EXPECT=< 0x00400cbb mov eax, 0x00 | ,=< 0x00400cc0 jmp 0x400d13 .. @@ -481,7 +481,7 @@ EXPECT=< 0x1000028c0 ldur w8, [var_24h] @@ -505,7 +505,7 @@ EXPECT=< 0x100002930 mov x0, 8 @@ -519,7 +519,7 @@ EXPECT=< 0x100002980 ldur x0, [var_20h.var_20h] | || | 0x100002984 ldr x8, [x0] | || | 0x100002988 ldr x8, [x8, 0x18] ; [0x18:4]=-1 ; 24 -| || | 0x10000298c blr x8 ; Virtual Call : method.Dog.walk / method.Cat.walk / method.Human.walk +| || | 0x10000298c blr x8 ; Virtual Call : method.Dog.walk / method.Human.walk / method.Cat.walk | || | 0x100002990 ldur x8, [var_20h.var_20h] | || | 0x100002994 str x8, [var_58h] | || | 0x100002998 subs x8, x8, 0 @@ -716,7 +716,7 @@ EXPECT=< 0x100002cd4 ldr x0, [sp] | | 0x100002cd8 ldr x8, [x0] | | 0x100002cdc ldr x8, [x8, 8] ; [0x8:4]=-1 ; 8 -| | 0x100002ce0 blr x8 ; Virtual Call : sym.Dog::_Dog_0x100002fbc / sym.non_virtual_thunk_to_Dog::_Dog_0x100003068 +| | 0x100002ce0 blr x8 ; Virtual Call : sym.non_virtual_thunk_to_Dog::_Dog_0x100003068 / sym.Dog::_Dog_0x100002fbc | ,==< 0x100002ce4 b 0x100002ce8 | || ; CODE XREF from entry0 @ 0x100002ce4 | ``-> 0x100002ce8 ldur w0, [var_14h] diff --git a/test/db/cmd/cmd_avx b/test/db/cmd/cmd_avx index 60449924bc..7ea8382713 100644 --- a/test/db/cmd/cmd_avx +++ b/test/db/cmd/cmd_avx @@ -20,8 +20,8 @@ avx method.Dog.run EOF EXPECT=<base.value, i + 200, "KV is valid after rehashing"); + mu_assert_eq(tmp->extra, i + 300, "KV extra value is valid after rehashing"); + } + + ht_uu_free(ht_u); +#endif + mu_end; } @@ -297,6 +325,71 @@ bool test_delete(void) { mu_end; } +bool test_rehash_on_delete(void) { + HtUU *ht = ht_uu_new(); + ht->opt.hashfn = (HtUUHashFunction)create_collision; + + for (ut32 i = 0; i < 56; i++) { + mu_assert_true(ht_uu_insert(ht, i, i * 100), "failed to insert element"); + } + + for (ut32 i = 0; i < 56; i++) { + mu_assert_true(ht_uu_delete(ht, i), "failed to delete element"); + } + + for (ut32 i = 0; i < 56; i++) { + mu_assert_true(ht_uu_insert(ht, i, i * 100), "failed to insert element"); + } + + for (ut32 i = 0; i < 56; i++) { + mu_assert_true(ht_uu_update_key(ht, i, i + 1000), "failed to update element key"); + } + + mu_assert_eq(ht_uu_size(ht), 56, "invalid table size"); + ht_uu_free(ht); + mu_end; +} + +bool test_ht_delete_optimized(void) { + // Test case for the "deletion trick" optimization. If the optimized implementation places + // incorrect "empty" slot, this would corrupt the hashtable and make some keys unreachable + bool found = false; + + for (ut32 size = 1; size <= 32; size++) { + for (ut32 delete_key = 0; delete_key < size; delete_key++) { + HtUU *ht = ht_uu_new(); + ht->opt.hashfn = (HtUUHashFunction)create_collision; + + // Insert all + for (ut32 i = 0; i < size; i++) { + ht_uu_insert(ht, i, i * 100); + } + + // Delete 1 + ht_uu_delete(ht, delete_key); + + // Confirm + for (ut32 i = 0; i < size; i++) { + HtUUKv *kv = ht_uu_find_kv(ht, i, &found); + + if (i == delete_key) { + mu_assert_null(kv, "element expected to be deleted"); + mu_assert_false(found, "element expected to be deleted"); + } else { + mu_assert_notnull(kv, "key not found"); + mu_assert_true(found, "key not found"); + mu_assert_eq(kv->value, i * 100, "incorrect value"); + } + } + + mu_assert_eq(ht_uu_size(ht), size - 1, "invalid table size"); + ht_uu_free(ht); + } + } + + mu_end; +} + bool test_clear(void) { HtSS *ht = ht_ss_new(HT_STR_DUP, HT_STR_DUP); @@ -870,6 +963,8 @@ int all_tests() { mu_run_test(test_ht_insert_lookup); mu_run_test(test_ht_update_lookup); mu_run_test(test_ht_delete); + mu_run_test(test_ht_delete_optimized); + mu_run_test(test_rehash_on_delete); mu_run_test(test_ht_insert_kvp); mu_run_test(test_ht_insert_collision); mu_run_test(test_ht_grow); diff --git a/test/unit/test_sdb_diff.c b/test/unit/test_sdb_diff.c index 5b47a06360..3b90841ba0 100644 --- a/test/unit/test_sdb_diff.c +++ b/test/unit/test_sdb_diff.c @@ -128,12 +128,12 @@ bool test_sdb_diff_ns() { mu_assert_streq(diff, "-NS test\n" "-NS test/subspace\n" - "- test/subspace/here=lol\n" - "- test/subspace/some=values\n" "- test/subspace/are=saved\n" + "- test/subspace/some=values\n" + "- test/subspace/here=lol\n" + "- test/b=test\n" "- test/a=123\n" - "- test/c=hello\n" - "- test/b=test\n", + "- test/c=hello\n", "ns removed diff"); free(diff); @@ -142,12 +142,12 @@ bool test_sdb_diff_ns() { mu_assert_streq(diff, "+NS test\n" "+NS test/subspace\n" - "+ test/subspace/here=lol\n" - "+ test/subspace/some=values\n" "+ test/subspace/are=saved\n" + "+ test/subspace/some=values\n" + "+ test/subspace/here=lol\n" + "+ test/b=test\n" "+ test/a=123\n" - "+ test/c=hello\n" - "+ test/b=test\n", + "+ test/c=hello\n", "ns added diff"); free(diff); @@ -166,9 +166,9 @@ bool test_sdb_diff_ns_sub() { mu_assert("sub ns removed (diff)", !diff_str(a, b, &diff)); mu_assert_streq(diff, "-NS test/subspace\n" - "- test/subspace/here=lol\n" + "- test/subspace/are=saved\n" "- test/subspace/some=values\n" - "- test/subspace/are=saved\n", + "- test/subspace/here=lol\n", "sub ns removed diff"); free(diff); @@ -176,9 +176,9 @@ bool test_sdb_diff_ns_sub() { mu_assert("sub ns added (diff)", !diff_str(b, a, &diff)); mu_assert_streq(diff, "+NS test/subspace\n" - "+ test/subspace/here=lol\n" + "+ test/subspace/are=saved\n" "+ test/subspace/some=values\n" - "+ test/subspace/are=saved\n", + "+ test/subspace/here=lol\n", "sub ns added diff"); free(diff); diff --git a/test/unit/test_sdb_sdb.c b/test/unit/test_sdb_sdb.c index 403349f3b5..39ddeb8dbf 100644 --- a/test/unit/test_sdb_sdb.c +++ b/test/unit/test_sdb_sdb.c @@ -275,8 +275,8 @@ static const char *text_ref_simple_unsorted = "bbb=other stuff\n" "\n" "/subnamespace\n" - "key in sub=value in sub\n" "\\/more stuff=in sub\n" + "key in sub=value in sub\n" "\n" "/subnamespace/subsub\n" "some stuff=also down here\n"; diff --git a/test/unit/test_serialize_analysis.c b/test/unit/test_serialize_analysis.c index f6e4c84b89..da4e5ac137 100644 --- a/test/unit/test_serialize_analysis.c +++ b/test/unit/test_serialize_analysis.c @@ -181,7 +181,7 @@ Sdb *functions_ref_db() { Sdb *db = sdb_new0(); sdb_set(db, "0x4d2", "{\"name\":\"effekt\",\"type\":1,\"stack\":0,\"maxstack\":0,\"ninstr\":0,\"bp_frame\":true,\"pure\":true,\"bbs\":[1337]}"); sdb_set(db, "0xbeef", "{\"name\":\"eskapist\",\"bits\":32,\"type\":16,\"stack\":0,\"maxstack\":0,\"ninstr\":0,\"bp_frame\":true,\"bbs\":[]}"); - sdb_set(db, "0x539", "{\"name\":\"hirsch\",\"bits\":16,\"type\":0,\"cc\":\"fancycall\",\"stack\":42,\"maxstack\":123,\"ninstr\":13,\"bp_frame\":true,\"bp_off\":4,\"bbs\":[1337,1234],\"imports\":[\"earth\",\"rise\"],\"labels\":{\"beach\":1400,\"another\":1450,\"year\":1440}}"); + sdb_set(db, "0x539", "{\"name\":\"hirsch\",\"bits\":16,\"type\":0,\"cc\":\"fancycall\",\"stack\":42,\"maxstack\":123,\"ninstr\":13,\"bp_frame\":true,\"bp_off\":4,\"bbs\":[1337,1234],\"imports\":[\"earth\",\"rise\"],\"labels\":{\"year\":1440,\"another\":1450,\"beach\":1400}}"); sdb_set(db, "0xdead", "{\"name\":\"agnosie\",\"bits\":32,\"type\":8,\"stack\":0,\"maxstack\":0,\"ninstr\":0,\"bp_frame\":true,\"bbs\":[]}"); sdb_set(db, "0xc0ffee", "{\"name\":\"lifnej\",\"bits\":32,\"type\":32,\"stack\":0,\"maxstack\":0,\"ninstr\":0,\"bp_frame\":true,\"bbs\":[]}"); sdb_set(db, "0x1092", "{\"name\":\"hiberno\",\"bits\":32,\"type\":2,\"stack\":0,\"maxstack\":0,\"ninstr\":0,\"bbs\":[]}");