map操作函数的定义和使用 map是什么意思( 五 )


map操作函数的定义和使用 map是什么意思


evacuate 函数的代码注释非常清晰,对着代码和注释是很容易看懂整个的搬迁过程的,耐心点 。搬迁的目的就是将老的 buckets 搬迁到新的 buckets 。而通过前面的说明我们知道,应对条件 1,新的 buckets 数量是之前的一倍,应对条件 2,新的 buckets 数量和之前相等 。对于条件 1,从老的 buckets 搬迁到新的 buckets,由于 bucktes 数量不变,因此可以按序号来搬,比如原来在 0 号 bucktes,到新的地方后,仍然放在 0 号 buckets 。对于条件 2,就没这么简单了 。要重新计算 key 的哈希,才能决定它到底落在哪个 bucket 。例如,原来 B = 5,计算出 key 的哈希后,只用看它的低 5 位,就能决定它落在哪个 bucket 。扩容后,B 变成了 6,因此需要多看一位,它的低 6 位决定 key 落在哪个 bucket 。这称为 rehash 。
map操作函数的定义和使用 map是什么意思


因此,某个 key 在搬迁前后 bucket 序号可能和原来相等,也可能是相比原来加上 2^B(原来的 B 值),取决于 hash 值 第 6 bit 位是 0 还是 1 。理解了上面 bucket 序号的变化,我们就可以回答另一个问题了:为什么遍历 map 是无序的?map 在扩容后,会发生 key 的搬迁,原来落在同一个 bucket 中的 key,搬迁后,有些 key 就要远走高飞了(bucket 序号加上了 2^B) 。而遍历的过程,就是按顺序遍历 bucket,同时按顺序遍历 bucket 中的 key 。搬迁后,key 的位置发生了重大的变化,有些 key 飞上高枝,有些 key 则原地不动 。这样,遍历 map 的结果就不可能按原来的顺序了 。当然,如果我就一个 hard code 的 map,我也不会向 map 进行插入删除的操作,按理说每次遍历这样的 map 都会返回一个固定顺序的 key/value 序列吧 。的确是这样,但是 Go 杜绝了这种做法,因为这样会给新手程序员带来误解,以为这是一定会发生的事情,在某些情况下,可能会酿成大错 。当然,Go 做得更绝,当我们在遍历 map 时,并不是固定地从 0 号 bucket 开始遍历,每次都是从一个随机值序号的 bucket 开始遍历,并且是从这个 bucket 的一个随机序号的 cell 开始遍历 。这样,即使你是一个写死的 map,仅仅只是遍历它,也不太可能会返回一个固定序列的 key/value 对了 。多说一句,“迭代 map 的结果是无序的”这个特性是从 go 1.0 开始加入的 。再明确一个问题:如果扩容后,B 增加了 1,意味着 buckets 总数是原来的 2 倍,原来 1 号的桶“裂变”到两个桶 。例如,原始 B = 2,1号 bucket 中有 2 个 key 的哈希值低 3 位分别为:010,110 。由于原来 B = 2,所以低 2 位 10 决定它们落在 2 号桶,现在 B 变成 3,所以 010、110 分别落入 2、6 号桶 。
map操作函数的定义和使用 map是什么意思


理解了这个,后面讲 map 迭代的时候会用到 。再来讲搬迁函数中的几个关键点:evacuate 函数每次只完成一个 bucket 的搬迁工作,因此要遍历完此 bucket 的所有的 cell,将有值的 cell copy 到新的地方 。bucket 还会链接 overflow bucket,它们同样需要搬迁 。因此会有 2 层循环,外层遍历 bucket 和 overflow bucket,内层遍历 bucket 的所有 cell 。这样的循环在 map 的源码里到处都是,要理解透了 。源码里提到 X, Y part,其实就是我们说的如果是扩容到原来的 2 倍,桶的数量是原来的 2 倍,前一半桶被称为 X part,后一半桶被称为 Y part 。一个 bucket 中的 key 可能会分裂落到 2 个桶,一个位于 X part,一个位于 Y part 。所以在搬迁一个 cell 之前,需要知道这个 cell 中的 key 是落到哪个 Part 。很简单,重新计算 cell 中 key 的 hash,并向前“多看”一位,决定落入哪个 Part,这个前面也说得很详细了 。有一个特殊情况是:有一种 key,每次对它计算 hash,得到的结果都不一样 。这个 key 就是 math.NaN() 的结果,它的含义是 not a number,类型是 float64 。当它作为 map 的 key,在搬迁的时候,会遇到一个问题:再次计算它的哈希值和它当初插入 map 时的计算出来的哈希值不一样!你可能想到了,这样带来的一个后果是,这个 key 是永远不会被 Get 操作获取的!当我使用 m[math.NaN()] 语句的时候,是查不出来结果的 。这个 key 只有在遍历整个 map 的时候,才有机会现身 。所以,可以向一个 map 插入任意数量的 math.NaN() 作为 key 。当搬迁碰到 math.NaN() 的 key 时,只通过 tophash 的最低位决定分配到 X part 还是 Y part(如果扩容后是原来 buckets 数量的 2 倍) 。如果 tophash 的最低位是 0,分配到 X part;如果是 1,则分配到 Y part 。这是通过 tophash 值与新算出来的哈希值进行运算得到的:

推荐阅读