深入解讀Khash.h之哈希表空間調整

調整空間

顯然初始化內存大小是無法記錄元素的,以及如果新增元素超過當前哈希表所能容納的大小,或者哈希表中大部分的元素都被刪除,不需要那麼多空間,我們都需要對哈希表的空間進行調整。因此在khash.h有62行代碼,即244-306,是負責哈希表的大小調整。

khash.h代碼中只有kh_put_##nameh->n_occupied >= h->upper_bound時會調用kh_resize_##name,而且是先考慮h->n_buckets > (h->size<<1), 如果桶大小比實際存放元素數的2倍還大,說明是標記刪除元素太多了,那麼需要清空哈希表,否則是真的不夠了。前者傳給kh_resize_##namenew_n_buckets = h->n_buckets - 1, 後者new_n_buckets = h->n_buckets + 1

n_buckets會先經過kroundup32函數計算出新哈希表的大小(new_n_buckets),kroundup32涉及到一系列的位運算

#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x))

它的效果是得到比當前桶的大小大且距離最近的2^n,例如桶的數目是55,那麼最近的就是64。如果桶的數目是297, 那麼最近的就是512,如果是64,那麼就是63。 如果是我寫那就只能寫出下面這種代碼

int roundup32(int x) {
    int tmp = x;
    int y = 1;
    while (tmp) {
        tmp >>= 1;
        y <<= 1;
    }
    return x==(y>>1) ? y>>1 : y;
}

接着,它還保證桶的數目最少是4,if (new_n_buckets < 4) new_n_buckets = 4;

我們先考慮申請的空間的可容納上限比已有元素多的情況

if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0;

khash.h會先計算new_flags的數目,並初始化爲0xaa. 如果當前的桶的大小低於新的桶的大小,那麼就用krealloc重新申請內存,並將數據拷貝到新的內存地址中。

new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \
if (!new_flags) return -1;                              \
memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \
if (h->n_buckets < new_n_buckets) { /* expand */        \
    khkey_t* new_keys = (khkey_t*)krealloc((void*)h->keys, new_n_buckets * sizeof(khkey_t)); \
    if (!new_keys) { kfree(new_flags); return -1; }     \
        h->keys = new_keys;                                 \
        if (kh_is_map) {
            \
                khval_t* new_vals = (khval_t*)krealloc((void*)h->vals, new_n_buckets * sizeof(khval_t)); \
                if (!new_vals) { kfree(new_flags); return -1; } \
                    h->vals = new_vals;                             \
        }                                                   \
} /* otherwise shrink */

上面的代碼相對簡單,最複雜的268-294行重新計算hash的過程。重新計算哈希的本質本質就是縮小哈希表。

因爲桶的大小是按照4,8,16,32,64,128,256,512,1024這種方式增加,所以只要是增加空間,當前的元素數目是不可能高於新的桶大小的可容納範圍的上限的。只有在h->n_buckets <= (h->size<<1)的情況下,也就是當前空間一般都是刪除的元素的情況下,纔會出現當前元素數目大於桶的可容納上限的情況。

此時新的空間大小變爲原來的一半,那麼裏面的元素就需要移動位置。搬運的時候,很有可能出現哈希碰撞。

搬運過程是一個嵌套循環,外層循環遍歷舊哈希表的每個桶,如果發現它該位置上有元素,就記錄它的key和value,然後我們算下它在新哈希表位置(如果找到不爲空的,就往後移動),並將新位置標記爲不爲空。同時檢查新哈希表位置對應的舊哈希表位置上是否有元素,如果有,就把該元素和待插入元素進行交換,我們的下一個任務就是爲這個元素查找位置,否則就可以退出了。

for (j = 0; j != h->n_buckets; ++j) {
    \
        if (__ac_iseither(h->flags, j) == 0) {
            \
                khkey_t key = h->keys[j];                           \
                khval_t val;                                        \
                khint_t new_mask;                                   \
                new_mask = new_n_buckets - 1;                       \
                if (kh_is_map) val = h->vals[j];                    \
                    __ac_set_isdel_true(h->flags, j);                   \
                    while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \
                        khint_t k, i, step = 0; \
                        k = __hash_func(key);                           \
                        i = k & new_mask;                               \
                        while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \
                            __ac_set_isempty_false(new_flags, i);           \
                            if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \
                            { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \
                                if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \
                                    __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \
                            }
                            else { /* write the element and jump out of the loop */ \
                                h->keys[i] = key;                           \
                                if (kh_is_map) h->vals[i] = val;            \
                                    break;                                      \
                            }                                               \
                    }                                                   \
        }                                                       \
}

接下來的工作就是用krealloc重新調整內存大小, 重新計算其他元信息.

if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \
h->keys = (khkey_t*)krealloc((void*)h->keys, new_n_buckets * sizeof(khkey_t)); \
if (kh_is_map) h->vals = (khval_t*)krealloc((void*)h->vals, new_n_buckets * sizeof(khval_t)); \
}                                                           \
kfree(h->flags); /* free the working space */               \
h->flags = new_flags;                                       \
h->n_buckets = new_n_buckets;                               \
h->n_occupied = h->size;                                    \
h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章