一个高性能无锁哈希表的设计和实现

无锁哈希表(Lock-Free Hash Table)是多线程编程中的理想数据结构,但是实现以及使用都需要一定的技巧。作者对此做了一个巧妙的设计实现,在现代X86平台上能取得千万次每秒的并发查找/增加/删除操作。

通过考察各种基于CAS原子操作的无锁数据结构实现,目前公认可实现无锁安全的数据结构是数组和单向队列。其他实现都一定程度上受到ABA问题的威胁。数组的实现相对于单向队列要简单,所以无锁hash table理想的选择是数组,对于冲突不拉链。但是如何解决hash冲突呢?基本思想依然是开放寻址探测法。为了同时支持增加/查找/删除三种操作,业界各种开放寻址探测的算法,都为一次探测做了优化且照顾了其他探测次数以及最坏情况,但是这个照顾动作对于无锁设计实在是不太完美。为了达到O(1)的操作性能,开放寻址应该限制探测的次数为固定有限次,超过这个数目的探测应该通过算法降低为0,但同时用一个很小的数组(遍历模式)收容最坏情况(这里称之为保险数组)。由此可以实现,所有的操作都是对数组做无锁设计。

为了保证固定有限的探测次数极为有效(也就是够用),应该通过算法保证冲突率够低。降低冲突,一般不外乎两种措施:

一,增加hash表长度。此措施受限于内存,所以对每个bucket要尽量节省内存。一般用指针作为bucket单元就是很节省内存了。但64bit系统指针是8个字节,我认为4个字节作为bucket单元,就可以支持40亿buckets了,同样的内存带来双倍hash表长度,对降低hash冲突率有很大的改进。不用指针,怎么去找hash node呢?你自然会想到“内存池”。对,4个字节作为hash node在内存池的索引。

二,增加hash函数的分散性能,尽量接近理论极限。业界对于hash函数的讨论和实现已经有很多了,高计算性能分散性极佳的已有很多,比如murmur hash,city hash。

但是,好像上面两种办法并没有什么特别的,跟大家常用的降低冲突率没啥改进啊。这里增加了第三种措施:固定有限的探测位置,如果尽量彼此独立(也就是不会导致二次卷积),将会从理论上降低hash冲突概率。那么如何做到彼此独立呢?作者的设计就是利用hash函数的输出 -- 选用输出128位结果的hash函数,分割成四个32位整数(128位是随机的因而四个整数是独立的),每个32bit整数对hash table长度求模,可以得到4个独立的位置,这样将大大减少冲突率。

但是,这样一般还不够好。原因是我们想尽量提hash table的装载率(load factor)且仍然能够处理冲突增加的倾向。为此,增加第二个数组,用上面四个32bit整数同样地映射出第二个数组内的4个位置,这样对一个key我们就有8个独立的位置了。8个位置都探测过了仍然冲突的怎么办?扔到保险数组里面去。

至此我们有三个优化目标:1,进入保险数组的概率应该极低;2,总的bucket利用率(性能墙)尽量高;3,两个数组占用的内存之和应该最小。通过计算(此略),可以限定1,得到2和3的最佳配置。在作者的实现中创建了三个数组:一个高load factor的大数组作为主hash表,一个低load factor的小数组作为辅hash表,和一个极其微小(64或128足够)的数组作为保险。

这个优化目标需要一个前提,就是基于概率计算的前提是可靠的,也就是对于任何key,选用hash函数输出要足够随机。这个前提,对于key足够长,city hash和murmurhash函数已经做得足够好。但是对于key小于12字节,就变差了。这个也是理论上没办法的事情。使用者应该明白并避让这一点。

时间: 2024-08-05 14:54:53

一个高性能无锁哈希表的设计和实现的相关文章

handy之日志--高性能无锁日志系统

服务器编程中,日志系统需要满足几个条件 .高效,日志系统不应占用太多资源 .简洁,为了一个简单的日志功能引入大量第三方代码未必值得 .线程安全,服务器中各个线程都能同时写出日志 .轮替,服务器不出故障是不重启的,半年一年的日志放到一个文件会导致文件过大 .及时保存,程序故障导致异常退出,此时需要通过日志诊断问题,不缓冲的日志系统更易用 著名的日志库有log4xxx系列,提供了非常灵活的功能,当然随之而来的代价就是庞大的库.在大多数服务器应用中,所需的功能不多,我偏向于选择一个支持按时间轮替的简洁

数据结构基础(18) --哈希表的设计与实现

哈希表 根据设定的哈希函数 H(key)和所选中的处理冲突的方法,将一组关键字映射到一个有限的.地址连续的地址集 (区间) 上,并以关键字在地址集中的"映像"作为相应记录在表中的存储位置,如此构造所得的查找表称之为"哈希表". 构造哈希函数的方法 1. 直接定址法(数组) 哈希函数为关键字的线性函数H(key) = key 或者 H(key) = a*key + b 此法仅适合于:地址集合的大小 == 关键字集合的大小 2. 数字分析法 假设关键字集合中的每个关键字

2017-5-26/描述一个高性能高可靠的网站架构——如何设计一个秒杀系统

一.秒杀的应用场景 电商网站的抢购活动.12306网站的抢票.抢红包. 二.秒杀的特点 1.秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增. 2.数据库的并发读写冲突以及资源的锁请求冲突非常严重. 3.秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功. 三.秒杀架构的原则 1.将请求拦截在系统上游,降低下游压力:秒杀系统特点是并发量极大,请求都压倒了后端数据层,但实际秒杀成功的请求数量却很少.所以如果不在前端拦截很可能造成数据库读写锁冲突严重,并发高响应慢,甚至导

一个极简易 int 类型哈希表的实现

看了算法导论的影印版的哈希表时,开始还不太明白, 想了下后觉得似乎哈希表就是数组和链表的组合, 于是根据这个思路实现了一个最简易的哈希表. 这个其实我还是不太满意, 可能在以后会更新, 因为我觉得不满足 DRY 原则. class HashTable { private: const size_t initSize = 13; const int32_t hashNum = 13; vector<list<int32_t>> hashTable; int32_t Hash (con

哈希函数和哈希表综述 (转)

哈希表及哈希函数研究综述 摘要 随着信息化水平的不断提高,数据已经取代计算成为了信息计算的中心,对存储的需求不断提高信息量呈现爆炸式增长趋势,存储已经成为急需提高的瓶颈.哈希表作为海量信息存储的有效方式,本文详细介绍了哈希表的设计.冲突解决方案以及动态哈希表.另外针对哈希函数在相似性匹配.图片检索.分布式缓存和密码学等领域的应用做了简短得介绍 哈希经过这么多年的发展,出现了大量高性能的哈希函数和哈希表.本文通过介绍各种不同的哈希函数的设计原理以及不同的哈希表实现,旨在帮助读者在实际应用中,根据问

Code Review:C#与JAVA的哈希表内部机制的一些区别

看C#与JAVA源码时发现C#与JAVA哈希表的实现略有不同,特此分享一下. 我觉得看哈希表的机制可以从"碰撞"这里划线分为两部分来分析. 1,发生碰撞前 在发生碰撞前决定get与put的速度唯一因素是通过哈希函数计算键值位置的速度.而占用空间大小取决于需要的桶的数量(取决于极限装载值(load factor)(假设已知需要放入哈希表中元素的数量)),和桶的大小. C#的默认装载系数=0.72 // Based on perf work, .72 is the optimal load

从头到尾彻底解析哈希表算法

说明:本文分为三部分内容,第一部分为一道百度面试题Top K算法的详解:第二部分为关于Hash表算法的详细阐述:第三部分为打造一个最快的Hash表算法. 第一部分:Top K 算法详解 问题描述 百度面试题: 搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节. 假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个.一个查询串的重复度越高,说明查询它的用户越多,也就是越热门.),请你统计最热门的10个查

高并发编程之无锁

前几期简单介绍了一些线程方面的基础知识,以及一些线程的一些基础用法以及通过jvm内存模型的方式去介绍了一些并发中常见的问题(想看往期文章的小伙伴可以直接拉到文章最下方飞速前往).本文重点介绍一个概念“无锁” 本期精彩什么是无锁无锁类的原理AtomicIntegerUnsafeAtomicReferenceAtomicStampedReference 什么是无锁 在高并发编程中最重要的就是获取临界区资源,保证其中操作的原子性.一般来说使用synchronized关键字进行加锁,但是这种操作方式其实

深入理解哈希表

有两个字典,分别存有 100 条数据和 10000 条数据,如果用一个不存在的 key 去查找数据,在哪个字典中速度更快? 有些计算机常识的读者都会立刻回答: “一样快,底层都用了哈希表,查找的时间复杂度为 O(1)”.然而实际情况真的是这样么? 答案是否定的,存在少部分情况两者速度不一致,本文首先对哈希表做一个简短的总结,然后思考 Java 和 Redis 中对哈希表的实现,最后再得出结论,如果对某个话题已经很熟悉,可以直接跳到文章末尾的对比和总结部分. 哈希表概述 Objective-C 中