开放寻址哈希表优化(一):寻找下标

TL;DR

inline int32_t hash_index(const uint64_t* keys, uint32_t keys_size, uint64_t key) {
    if (!keys_size || key >= HASH_TOMBSTONE)
        return -1;

    const uint32_t max_distance = keys_size;

    uint32_t i = hash_first_index(key, keys_size);
    uint32_t distance = 0;
    while (keys[i] != key) {
        if (distance > max_distance)
            return -1;
        if (keys[i] == HASH_UNUSED)
            return -1;
        i = (i + 1) & (keys_size - 1); // instead of (i+1) % keys_size
        ++distance;
    }
    return i;
}

背景

这是一个 开放寻址哈希表中 hash 值的查找函数。
参数中:keys 是存储哈希值的数组,keys_size 是表的大小,key 是要查找的 hash 值。返回 hash 值所在的数组下标(如果不存在则返回 -1)。

开放寻址哈希表 hash key 往往很大,要把它塞进数组中,常见的方法就是把 key 做被除数,哈希表数组大小做除数,把余数作为下标结果,可以有效保证它在数组范围 (0~keys_size-1) 内 ,即 key % keys_size。这种计算是频繁的,每次添加、查找都会执行一次或多次。

现在让我们关注求余运算的性能问题,
首先我们写一个求余函数:

int func(int a, int b)
{
    return a % b;
}

用 Visual Studio 开 /O2 编译选项(最大编译优化)来编译此函数,求余运算部分的汇编指令及其占用的CPU时钟周期如下:

mov eax, ebx          ; 1 cycle (零延迟移动,通常被重命名处理)
cdq                   ; 1 cycle  
idiv ecx              ; ≈20-40 cycles (有符号除法,周期数可变)
mov eax, edx          ; 1 cycle

看上去这并不是 1 个 cycle 就可以完成的运算,优化求余运算可以优化哈希表的性能。

解决

这有一个简单实用的 trick:

计算 m % n 时候,当除数 n2k 次幂(k为正整数),m % n 等于 m & (n-1)
其中 % 为求余(模除),& 为按位与。

m & (n-1) 的性能优于 m % n。因为执行同样的逻辑,它只需要 1 个 cycle:

and eax, ebx        ; ebx 等于 keys_size - 1

做到这一切,只需要保持哈希表的总大小为 2 的 幂。

证明

n = 2k
m = q⋅2k+rq 是商,r 是余数。
m & (q⋅2k−1)
= (q⋅2k+r) & (q⋅2k-1)
= (q⋅2k+r) & (2k-1)
这步可以进一步分为高 k 位和低 k 位的计算:
= q⋅2k & (2k-1) + r & (2k-1)

在二进制中:
2k1 后面跟 k0。比如 8 的二进制为 1000
2k−1k1。比如 8-1 的二进制为 0111
q⋅2k
= q 的二进制 << k
= q 的二进制后面补 k 个 0

也就是说,q⋅2k 的低 k 位全是 0,而 (2k-1) 的只有 k 位,所以它们位与的结果是 0
因此,m & (q⋅2k−1)
= q⋅2k & (2k-1) + r & (2k-1)
= r & (2k-1)

2k 二进制形式为高一位是 1,后面的 k-1 位都是 0;因为余数一定小于除数,所以 r 不会大于 2kr 的高 1 位一定是 0,后面的 k-1 位都代表自己。所以 r & (2k-1) 的值为 r 本身。
因此,m & (q⋅2k−1) = r

Q.E.D.

其他

这种优化方法确实需要更多的空间,是用空间换时间的一种办法。

哈希表的使用中,一般时间复杂度更重要些。至于空间分配上的要求比常规方式更高,这其实也符合线性探测法的需求。
线性探测法是用来处理不同 hash 值计算后下标恰好一致的方法,如果下标已经有值了,要不断重新计算可存入的下一个位置,这种开销在哈希表空间使用率高的情况下尤为明显:

空间使用率 平均查找长度 性能评价
< 0.5 ≈1.5 优秀
0.5-0.7 2-5 良好
0.7-0.8 5-10 一般
> 0.8 > 10 较差
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!