golang实现双hash表

一,写于前

现代编程语言通常提供hash表容器,如c++中std:hash_map、golang中map底层均采用hash table等,究竟是何种魔法使hash_table能在编程上占得一席之地呢,以下将为大家缓缓揭开这一答案。

hash表有哪些功能?

在谈论hash表实现时首先要问个问题。我们知道,指针有两种:一种叫栈,另一种叫队列。栈和队列都可以用来存储数据,但是它们之间并不完全一样,栈可以存放比较大的文件;而队列只能存小文件。对数组的存取可采用索引、array〔0〕,可得到0索引中的数值,取数值的效率为O(1),若对字符串类型若能达到同类存取,则不太完善,先用字符串转数,然后转为array〔1〕取数值,则效率为O(2)。

图片[1]-golang实现双hash表-【聚禄鼎】一站式企业服务平台

hash表构成

上图神秘的黑魔法是hash功能,那hash功能如此牛气,是不是有问题呢? 其实,我们可以利用hash函数解决很多实际问题。下面我们就来看看如何使用hash函数破解各种复杂的程序吧!一、Hash函数与其他函数相比有哪些特点?1。hash函数中存在的最大问题是hash冲突。下图中,在相同槽位上面临着不同字符串的映射。

图片[2]-golang实现双hash表-【聚禄鼎】一站式企业服务平台

hash的矛盾

工程实际中通常采用挂链,即每一个hash table内的槽位为链表,发生冲突后即向链表表尾部添加新的节点。

图片[3]-golang实现双hash表-【聚禄鼎】一站式企业服务平台

化解hash矛盾

三,redis怎样实现一个双hash表

3.1优化冲突挂链效率

在普通hash table实现上,谈到用挂链表来处理hash冲突。图1题,在数据不断增加的情况下,发生冲突的可能性就会不断增加。槽位里有大约1000个节点,如果每个节点都进行维护的话,会导致整个系统的效率下降。由于冲突是无法避免的,因此当槽位中挂载的节点大于某一比例时,就会扩容出新的hash table.这种新老并存的情况被确定为rehashing.更大的hashtable能够使冲突槽位中链表长度减小,进而提高效率。我个人认为这样做有两点好处:1.增加容量。2.减少内存消耗。但是这两个都不容易做到。我们先来看看为什么会出现上述情况。如果这一点可行的话,其背后不过就是一个工程问题。

图片[4]-golang实现双hash表-【聚禄鼎】一站式企业服务平台

hash迁移图示

3.2扩容流程

上文已经想好了使用双hash表来提高数据扩容后接入效率,那么现在就来讨论一下insert流程吧,接入流程就是先对老hash table进行接入,再对新hash table进行接入,去掉同理。

注:

其中有一个好处,就是要尽可能地避免rehashing给企业带来冲击,要求平均运行的冲击,每getdelete set运行一次,就会有部分数据被移植到新的hash表中,这个在redis hashtable上就是一个实现。

十几个流程图描述如下。

图片[5]-golang实现双hash表-【聚禄鼎】一站式企业服务平台

扩容流程

3.3收缩流程

收缩流程可由hash table内完成或对外完成。

hash table通常的实现方式为指针数据如果对于某个hash的table将其存储在100w的元素中然后将其删除。

描述:

不支持内部自动收缩(hash table),通常浪费7.6MB(64位机器)8×1000000 1×1024 1×1024.0=7×62890625.此处8为64位机器1指针字节数除以1单位1024为KB除以1为MB为。这对大多数人来说已经足够了;但是对于我们这些使用了大容量存储器和处理器的人来说就不是这么简单了,因为它不仅占用大量内存。而且还需要额外的硬盘数据接口。

如果仍然极度关心这一点,则可对外调用收缩函数。

四,核心代码

下面是golang移值redis双hash表核心编码,供大家参考。

type entry[K comparable, V any] struct { key K val V next *entry[K, V] } type config struct { hashFunc func(str string) uint64 cap int } type HashMap[K comparable, V any] struct { table [2][]*entry[K, V] used [2]uint64 sizeExp [2]int8 rehashidx int keySize int config isKeyStr bool init bool }

func (h *HashMap[K, V]) findIndexAndEntry(key K) (i uint64, e *entry[K, V], err error) { if err := h.expand(); err != nil { return 0, nil, err } hashCode := h.calHash(key) idx := uint64(0) for table := 0; table < 2; table++ { idx = hashCode & sizeMask(h.sizeExp[table]) head := h.table[table][idx] for head != nil { if key == head.key { return idx, head, nil } head = head.next } if !h.isRehashing() { break } } return idx, nil, nil }

func (h *HashMap[K, V]) Set(k K, v V) error { h.lazyinit() if h.isRehashing() { h.rehash(1) } index, e, err := h.findIndexAndEntry(k) if err != nil { return err } idx := 0 if h.isRehashing() { idx = 1 } if e != nil { e.val = v return nil } e = &entry[K, V]{key: k, val: v} e.next = h.table[idx][index] h.table[idx][index] = e h.used[idx]++ return nil }

五,性能压测

以下是性能压测数据。

读取速度是标准库的两倍

写入速度比标准库的一次写入速度慢0.018ns

goos: darwin goarch: amd64 pkg: github.com/guonaihong/gstl/rhashmap cpu: Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz BenchmarkGet-8 1000000000 0.4066 ns/op BenchmarkGetStd-8 1000000000 0.8333 ns/op PASS ok github.com/guonaihong/gstl/rhashmap 130.007s. 比标准库快一倍. goos: darwin goarch: amd64 pkg: github.com/guonaihong/gstl/rhashmap cpu: Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz BenchmarkSet-8 1000000000 0.1690 ns/op BenchmarkSetStd-8 1000000000 0.1470 ns/op PASS ok github.com/guonaihong/gstl/rhashmap 3.970s 五百万数据的Get操作时间

六,完整代码

如果你还有兴趣了解其他部分,请看以下网址,其中包括Get, Set和Delete的完整实现。

原文链接:http://www.sfdkj.com/13314.html

 

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片