Add extended insert/update methods for HT (#4489)

* Return more info from HT insert/update methods

* Fix bug with tracking

* More doc comments

* Update tests

* Separate extented insert/update methods

* Reuse code

* Update docs

* Fix insert_update() usage

* Rehashing -> modification

* Trigger buckets reallocation

* Return enum
This commit is contained in:
pelijah 2024-05-17 13:06:01 +03:00 committed by GitHub
parent a6d4a6bd02
commit 01203509ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 210 additions and 55 deletions

View file

@ -81,8 +81,21 @@
#define HT_NULL_VALUE 0
#endif
#ifndef HT_STR_OPTION_DEFINED
#define HT_STR_OPTION_DEFINED
#ifndef HT_ENUM_DEFINED
#define HT_ENUM_DEFINED
/**
* Return codes for insert/update methods
* code < 0 <--> code == HT_RC_ERROR
* code >= 0 <--> code != HT_RC_ERROR
* code > 0 <--> code == HT_RC_INSERTED || code == HT_RC_UPDATED
*/
typedef enum {
HT_RC_ERROR = -1, ///< Error (out of memory)
HT_RC_EXISTING = 0, ///< Existing KV prevented an insertion
HT_RC_INSERTED = 1, ///< New KV was inserted during insert/update operation
HT_RC_UPDATED = 2, ///< Existing KV was updated during update operation
} HtRetCode;
typedef enum {
HT_STR_DUP = 0, ///< String is copied when inserting into HT
HT_STR_OWN, ///< String ownership is transferred when inserting into HT
@ -161,8 +174,10 @@ RZ_API RZ_OWN HtName_(Ht) *Ht_(new_opt_size)(RZ_NONNULL HT_(Options) *opt, ut32
RZ_API void Ht_(free)(RZ_NULLABLE HtName_(Ht) *ht);
// Insert a new Key-Value pair into the hashtable. If the key already exists, returns false.
RZ_API bool Ht_(insert)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, VALUE_TYPE value);
RZ_API HtRetCode Ht_(insert_ex)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, VALUE_TYPE value, RZ_OUT RZ_NULLABLE HT_(Kv) **out_kv);
// Insert a new Key-Value pair into the hashtable, or updates the value if the key already exists.
RZ_API bool Ht_(update)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, VALUE_TYPE value);
RZ_API HtRetCode Ht_(update_ex)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, VALUE_TYPE value, RZ_OUT RZ_NULLABLE HT_(Kv) **out_kv);
// Update the key of an element in the hashtable
RZ_API bool Ht_(update_key)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE old_key, const KEY_TYPE new_key);
// Delete a key from the hashtable.
@ -177,3 +192,4 @@ RZ_API void Ht_(foreach)(RZ_NONNULL HtName_(Ht) *ht, RZ_NONNULL HT_(ForeachCallb
RZ_API RZ_BORROW HT_(Kv) *Ht_(find_kv)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, RZ_NULLABLE bool *found);
RZ_API bool Ht_(insert_kv)(RZ_NONNULL HtName_(Ht) *ht, RZ_NONNULL HT_(Kv) *kv, bool update);
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);

View file

@ -172,46 +172,64 @@ RZ_API void Ht_(free)(RZ_NULLABLE HtName_(Ht) *ht) {
free(ht);
}
// Increases the size of the hashtable by 2.
static void internal_ht_grow(HtName_(Ht) *ht) {
HtName_(Ht) *ht2;
HtName_(Ht) swap;
/**
* Increases the size of the hashtable by 2.
* Tracks change of KV \p tracked position.
*/
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);
ut32 i;
ht2 = internal_ht_new(sz, idx, &ht->opt);
HtName_(Ht) *ht2 = internal_ht_new(sz, idx, &ht->opt);
if (!ht2) {
// we can't grow the ht anymore. Never mind, we'll be slower,
// but everything can continue to work
return;
return tracked;
}
for (i = 0; i < ht->size; i++) {
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) {
Ht_(insert_kv)(ht2, kv, false);
if (kv == tracked) {
continue;
}
if (Ht_(insert_kv_ex)(ht2, kv, false, NULL) < 0) {
ht2->opt.finiKV = NULL;
Ht_(free)(ht2);
return tracked;
}
}
}
if (Ht_(insert_kv_ex)(ht2, tracked, false, &tracked) < 0) {
ht2->opt.finiKV = NULL;
Ht_(free)(ht2);
return tracked;
}
// And now swap the internals.
swap = *ht;
HtName_(Ht) swap = *ht;
*ht = *ht2;
*ht2 = swap;
ht2->opt.finiKV = NULL;
Ht_(free)(ht2);
return tracked;
}
static void check_growing(HtName_(Ht) *ht) {
static HT_(Kv) *check_growing(HtName_(Ht) *ht, HT_(Kv) *tracked) {
if (ht->count >= LOAD_FACTOR * ht->size) {
internal_ht_grow(ht);
return internal_ht_grow(ht, tracked);
}
return tracked;
}
static HT_(Kv) *reserve_kv(HtName_(Ht) *ht, const KEY_TYPE key, const int key_len, bool update) {
/**
* \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) {
HT_(Bucket) *bt = &ht->table[bucketfn(ht, key)];
HT_(Kv) *kvtmp;
ut32 j;
@ -220,66 +238,150 @@ static HT_(Kv) *reserve_kv(HtName_(Ht) *ht, const KEY_TYPE key, const int key_le
if (is_kv_equal(ht, key, key_len, kvtmp)) {
if (update) {
fini_kv_pair(ht, kvtmp);
return kvtmp;
*code = HT_RC_UPDATED;
} else {
*code = HT_RC_EXISTING;
}
return NULL;
return kvtmp;
}
}
HT_(Kv) *newkvarr = realloc(bt->arr, (bt->count + 1) * ht->opt.elem_size);
if (!newkvarr) {
*code = HT_RC_ERROR;
return NULL;
}
bt->arr = newkvarr;
bt->count++;
ht->count++;
*code = HT_RC_INSERTED;
return kv_at(ht, bt, bt->count - 1);
}
/**
* \brief Insert KV \p kv into hash table \p ht or replace an existing KV with \p kv,
* if hash table \p ht already contains a KV with the same key as \p kv
* \param ht Hash table
* \param kv KV; shallow copy is made when writing to the hash table
* \param update Update flag; if set to true, replacement of existing KV is allowed
* \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) {
rz_return_val_if_fail(ht && kv, false);
HT_(Kv) *kv_dst = reserve_kv(ht, kv->key, kv->key_len, update);
if (!kv_dst) {
return false;
}
memcpy(kv_dst, kv, ht->opt.elem_size);
check_growing(ht);
return true;
return Ht_(insert_kv_ex)(ht, kv, update, NULL) > 0;
}
static bool insert_update(HtName_(Ht) *ht, const KEY_TYPE key, VALUE_TYPE value, bool update) {
ut32 key_len = calcsize_key(ht, key);
HT_(Kv) *kv_dst = reserve_kv(ht, key, key_len, update);
if (!kv_dst) {
return false;
}
/**
* \brief Insert KV \p kv into hash table \p ht or replace an existing KV with \p kv,
* if hash table \p ht already contains a KV with the same key as \p kv
* \param ht Hash table
* \param kv KV; shallow copy is made when writing to the hash table
* \param update Update flag; if set to true, replacement of existing KV is allowed
* \param[out] out_kv Pointer to the inserted/updated KV
* or pointer to the KV that prevented insertion (only if \p update set to false)
* or NULL in case of error. Pointers are valid until the next modification of the hash table.
* \return Returns HT_RC_INSERTED/HT_RC_UPDATED if KV was inserted/updated;
* returns HT_RC_EXISTING if key \p key already exists (only if \p update set to false);
* 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;
}
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) {
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) {
*out_kv = kv_dst;
}
return rc;
}
kv_dst->key = dupkey(ht, key);
kv_dst->key_len = key_len;
kv_dst->value = dupval(ht, value);
kv_dst->value_len = calcsize_val(ht, value);
check_growing(ht);
return true;
kv_dst = check_growing(ht, kv_dst);
if (out_kv) {
*out_kv = kv_dst;
}
return rc;
}
/**
* Inserts the key value pair \p key, \p value into the hashtable \p ht.
* Doesn't allow for "update" of the value.
* \brief Insert the key value pair \p key, \p value into the hash table \p ht
* \param ht Hash table
* \param key KV key; copy is made according to the options of \p ht
* \param value KV value; copy is made according to the options of \p ht
* \return Returns true if insertion took place;
* returns false if out of memory or if key \p key already exists.
*/
RZ_API bool Ht_(insert)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, VALUE_TYPE value) {
rz_return_val_if_fail(ht, false);
return insert_update(ht, key, value, false);
return insert_update(ht, key, value, false, NULL) > 0;
}
/**
* Inserts the key value pair \p key, \p value into the hashtable \p ht.
* Does allow for "update" of the value.
* \brief Insert the key value pair \p key, \p value into the hash table \p ht
* \param ht Hash table
* \param key KV key; copy is made according to the options of \p ht
* \param value KV value; copy is made according to the options of \p ht
* \param[out] out_kv Pointer to the inserted KV
* or pointer to the KV that prevented insertion
* or NULL if out of memory. Pointers are valid until the next modification of the hash table.
* \return Returns HT_RC_INSERTED if KV was inserted;
* returns HT_RC_EXISTING if key \p key already exists;
* returns HT_RC_ERROR if out of memory.
*/
RZ_API HtRetCode Ht_(insert_ex)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, VALUE_TYPE value, RZ_OUT RZ_NULLABLE HT_(Kv) **out_kv) {
rz_return_val_if_fail(ht, HT_RC_ERROR);
return insert_update(ht, key, value, false, out_kv);
}
/**
* \brief Insert the key value pair \p key, \p value into the hash table \p ht
* or update value of current KV if key \p key already exists
* \param ht Hash table
* \param key KV key; copy is made according to the options of \p ht
* \param value KV value; copy is made according to the options of \p ht
* \return Returns true if insertion/update took place;
* returns false if out of memory.
*/
RZ_API bool Ht_(update)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, VALUE_TYPE value) {
rz_return_val_if_fail(ht, false);
return insert_update(ht, key, value, true);
return insert_update(ht, key, value, true, NULL) > 0;
}
/**
* \brief Insert the key value pair \p key, \p value into the hash table \p ht
* or update value of current KV if key \p key already exists
* \param ht Hash table
* \param key KV key; copy is made according to the options of \p ht
* \param value KV value; copy is made according to the options of \p ht
* \param[out] out_kv Pointer to the inserted/updated KV or NULL in case of error.
* Pointers are valid until the next modification of the hash table.
* \return Returns HT_RC_INSERTED/HT_RC_UPDATED if KV was inserted/updated;
* returns HT_RC_ERROR if out of memory.
*/
RZ_API HtRetCode Ht_(update_ex)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE key, VALUE_TYPE value, RZ_OUT RZ_NULLABLE HT_(Kv) **out_kv) {
rz_return_val_if_fail(ht, HT_RC_ERROR);
return insert_update(ht, key, value, true, out_kv);
}
/**
@ -295,7 +397,7 @@ RZ_API bool Ht_(update_key)(RZ_NONNULL HtName_(Ht) *ht, const KEY_TYPE old_key,
}
// Associate the existing value with new_key
bool inserted = insert_update(ht, new_key, value, false);
bool inserted = insert_update(ht, new_key, value, false, NULL) > 0;
if (!inserted) {
return false;
}

View file

@ -16,14 +16,9 @@ RZ_API const char *rz_str_constpool_get(RzStrConstPool *pool, const char *str) {
if (!str) {
return NULL;
}
HtSPKv *kv = ht_sp_find_kv(pool->ht, str, NULL);
if (kv) {
return kv->key;
HtSPKv *kv;
if (ht_sp_insert_ex(pool->ht, str, NULL, &kv) < 0) {
return NULL;
}
ht_sp_insert(pool->ht, str, NULL);
kv = ht_sp_find_kv(pool->ht, str, NULL);
if (kv) {
return kv->key;
}
return NULL;
return kv->key;
}

View file

@ -550,20 +550,20 @@ EXPECT=<<EOF
fs
env
string
threads
network
dylib
threads
alloc
time
process
stdout
["fs":[],"env":[],"string":[],"threads":[],"network":[],"dylib":[],"alloc":[],"time":[],"process":[],"stdout":[]]
["fs":[],"env":[],"string":[],"network":[],"dylib":[],"threads":[],"alloc":[],"time":[],"process":[],"stdout":[]]
fs:
env:
string:
threads:
network:
dylib:
threads:
alloc:
time:
process:

View file

@ -666,8 +666,8 @@ global unicode.RangeTable * unicode.Braille @ 0x55e160
global struct struct { runtime.signalLock uint32; runtime.hz int32 } runtime.prof @ 0x58ecb8
global unicode.RangeTable * unicode.Noncharacter_Code_Point @ 0x55e4e0
global error runtime.overflowError @ 0x55e930
global struct []string syscall.envs @ 0x565780
global unicode.RangeTable * unicode.foldInherited @ 0x55e7e0
global struct []string syscall.envs @ 0x565780
global unicode.RangeTable * unicode.Other_Lowercase @ 0x55e588
global unicode.RangeTable * unicode.Variation_Selector @ 0x55e780
global uintptr runtime.skipPC @ 0x58ecd0

View file

@ -145,8 +145,8 @@ global void * __guard_xfg_table_dispatch_icall_fptr @ 0x140015040
global void (*type_0x1001)() [8] __rtc_iaa @ 0x14000ed88
global struct _onexit_table_t module_local_atexit_table @ 0x140011b40
global wchar_t [46] mspdbName @ 0x14000e078
global bool init @ 0x140011470
global void (*type_0x1001)() [8] __xt_z @ 0x14000caa0
global bool init @ 0x140011470
global void (*type_0x1001)() pre_cpp_initializer @ 0x14000c110
global struct _RS5_IMAGE_LOAD_CONFIG_DIRECTORY64 _load_config_used @ 0x14000e2d0
global struct __type_info_node __type_info_root_node @ 0x140011b80

View file

@ -529,6 +529,47 @@ bool test_ht_pu_ops(void) {
mu_end;
}
bool test_insert_update_ex(void) {
HtSU *ht = ht_su_new(HT_STR_CONST);
HtSUKv *inserted_kv = NULL;
mu_assert_eq(ht_su_insert_ex(ht, "foobar", 1337, &inserted_kv), HT_RC_INSERTED, "HT_RC_INSERTED");
mu_assert_notnull(inserted_kv, "inserted_kv");
mu_assert_streq(inserted_kv->key, "foobar", "key");
mu_assert_eq(inserted_kv->value, 1337, "value");
HtSUKv *existing_kv = NULL;
mu_assert_eq(ht_su_insert_ex(ht, "foobar", 101, &existing_kv), HT_RC_EXISTING, "HT_RC_EXISTING");
mu_assert_notnull(existing_kv, "existing_kv");
mu_assert_streq(existing_kv->key, "foobar", "key");
mu_assert_eq(existing_kv->value, 1337, "value");
HtSUKv *inserted_kv2 = NULL;
mu_assert_eq(ht_su_update_ex(ht, "deadbeef", 404, &inserted_kv2), HT_RC_INSERTED, "HT_RC_INSERTED");
mu_assert_notnull(inserted_kv2, "inserted_kv2");
mu_assert_streq(inserted_kv2->key, "deadbeef", "key");
mu_assert_eq(inserted_kv2->value, 404, "value");
HtSUKv *updated_kv = NULL;
mu_assert_eq(ht_su_update_ex(ht, "deadbeef", 123456, &updated_kv), HT_RC_UPDATED, "HT_RC_UPDATED");
mu_assert_notnull(updated_kv, "updated_kv");
mu_assert_streq(updated_kv->key, "deadbeef", "key");
mu_assert_eq(updated_kv->value, 123456, "value");
HtUU *ht2 = ht_uu_new();
for (size_t i = 0; i < 100; ++i) {
HtUUKv *tmp = NULL;
ht_uu_insert_ex(ht2, 4 * i, i + 200, &tmp);
mu_assert_notnull(tmp, "KV is set after rehashing");
mu_assert_eq(tmp->value, i + 200, "KV is valid after rehashing");
}
ht_su_free(ht);
ht_uu_free(ht2);
mu_end;
}
int all_tests() {
mu_run_test(test_ht_insert_lookup);
mu_run_test(test_ht_update_lookup);
@ -549,6 +590,7 @@ int all_tests() {
mu_run_test(test_foreach_delete);
mu_run_test(test_update_key);
mu_run_test(test_ht_pu_ops);
mu_run_test(test_insert_update_ex);
return tests_passed != tests_run;
}