码迷,mamicode.com
首页 > 编程语言 > 详细

【算法江湖】哈希之战【上】

时间:2015-06-27 21:14:16      阅读:343      评论:0      收藏:0      [点我收藏+]

标签:

【算法江湖】哈希之战【上】

文章很长,但超级干货,值得收藏!

 

虽然下面的文字略有嘻哈的感觉,但我还是希望您在阅读之后,能够本着严肃的态度,来审视一番当今天下最有用的数据结构~哈希表(hash table)。因为,有人的地方就有江湖,有数据的地方就有哈希。

 

2015年,6月7日~6月9日,在史上最强计算机技术微信群里,发生了一场史诗级别的算法论战。

 

此战,因参与者阵容之强,脑洞之大,足可彪炳微信群聊史册。无论你是技艺精湛的码农,还是拼杀股市的大妈,这次讨论都可以令你受益匪浅。好了,在你准备浏览下述老少咸宜男女通杀的内容之前,请先记下这个微信群的大名~“美丽互联”。(有人问如何加入?此群逼格甚高,审核极严,但是关注“待字闺中”,你将离高手越来越近)

 

每场战争都有一个原因,管它是神仙打架,还是明星撕B。这场算法论战也无例外,起因嘛,很简单很money:上证指数突破5000点大关!

 

龙博下战书

上海证交所的白老师(白硕)在群聊里稍微夸奖了一下龙博当年设计的一个大数据查询系统,于是龙博兴奋地变身“龙霸”,在群里点燃了导火索。

 

龙博:虽然哈希算法是我做的事情里面极小的一部分,但确实是值得骄傲的一个小成就。我拿这个题目考了很多人,谷歌的,微软的,百度的,基本没有及格的,哈哈。曾经有一个谷歌的技术总监要挑战,方案直接被我判了零分。

 

【书记员(硅谷寒)注:龙博自我吹捧之形,已跃然纸上。要知道,群里谷歌微软的人不要太多,“帮主”本人就是Google出来的,刚好也混了个技术总监的头衔。】

 

龙博(开始下战书了):这个哈希算法和数据结构设计也有普适性,交易所内存大部分数据都用这个算法和数据结构进行管理。当上交所的方案是标准答案,看你们的答案与标准答案差异有多大。我就不提示了。有兴趣就挑战,做一个完整的方案出来,给我...

 

龙博:我稍微把这个哈希算法和数据结构希望解决的问题描述一下

 

上亿条持仓记录在内存里面需要进行管理,一个主机上可能有几十颗处理器,每个处理器绑定一个进程(可以看成是线程),这几十个进程需要同步地对这上亿条持仓记录进行查询、修改、删除和新增的操作,每个进程每秒需要做数百万次这样的操作。

 

每条持仓记录,由三个信息来定位(key):

持仓类型(一位字符,A~F),

股东代码(10位字符,第1位A~Z,后面9位是数字0~9),

股票代号(short类型)

 

被定位到的持仓记录是一个64bit的值(value)

 

 

帮主带头应战,哎...

帮主:就完美哈希了,剩下就是数组问题了,因为是有限集合和数值是预先知道的。【书记员注:这是本群陈帮主,搜索领域大牛。帮主在第一时间跳出来应战】

 

龙博:你计算一下你需要数据结构所需的内存资源,再评估一下运算开销,是否能达到性能要求。哈希算法本身没什么大不了,上交所现在用的哈希算法平均查找次数为1.1。关键是数据结构设计。【书记员注:龙博在此处已经提示了设计的关键点,但可惜的是,之后参与论战的人都忽略了。】

 

帮主: 1. (准备阶段)将已知的所有的 KEY 值传给PHF或MPHF生成算法,生成PHF或MPHF以及相应的数据;

 

2. (使用阶段)调用已有的PHF或MPHF以及相应的数据快速计算哈希值并进行相应的操作。使用MPHF,那么我们可以分配一个Long transactions[几亿],数组。

 

剩下的就是数组操作了。删除就是置零,其它就是修改Long的值。完毕。谢谢。

【书记员注:帮主一下子抛出来两个英文缩写PHF、MPHF,着实把龙博吓了一跳,其实就是“完美哈希函数”和“最小完美哈希函数”】

 

龙博:你把所有可能的KEY 都放进去了想想你要用多少内存?所以我给你零分。我们总共只有一亿条记录。

 

 

帮主:根据前一天的纪录,算一次MPHF,其它新增的放另一个hashtable。如果你说一天都是新增的,那么我们一个小时(x分钟),重算一次MPHF。其实龙博和我也没什么分歧,最后是不是转化为设计一个好的hash function?

 

【书记员注:不得不说,帮主是天分极强的人,其实,他现在的方案已经很接近最终上交所的方案了,只不过缺少了一点点“画龙点睛”的东西。下面,另一个大牛,独孤虎,要登场了。确切地说,是“大牛团队”,因为独孤虎不是一个人在战斗,他拉上了自己公司里的整个团队,来解答龙博的题目。】

 

独孤虎炫目登场,杯具了!

独孤虎:我给出的答案是:

1)采用hash:根据股票代码,将请求hash到特定CPU线程;

2)采用数组+hash+SkipList+hash:持仓类型数量固定,在每个线程(CPU)采用一个10个元素的数组,每个元素为针对于一个持仓类型的Skip List,Skip List的元素的key(用于排序)为股东代码。除了key之外,跳跃表中的每个元素包括一个hash,用于将股票代码映射到特定信息。

 

优化措施,根据访问热度为每个股票代码设置一个hot值,用于分散访问,但是会带来比较麻烦的数据同步问题。

 

算法的时间复杂度为O(1)+log(2*Mi/n)+O(1),其中Mi是特定持仓类型中股东代码的最大数量,n是线程或CPU数量。内存采用hash,文件采用B+树,所以采用hash是ok的,关键是采用何种数据结构降低操作的复杂度。只有完全采用数组,才会考虑稀疏性。这个是hash+数组+跳跃表+hash仅仅是针对于固定数量的持仓类型采用了数组。

 

需要额外的2*(持仓类型,股东代码,股票代码)数量的指针,每个指针64位计算,16*(持仓类型,股东代码,股票代码)个byte。

 

时间复杂度:O(1)+log(2*Mi/n)+O(1),其中Mi是特定持仓类型中股东代码的最大数量,n是线程或CPU数量空间复杂度:需要额外的2*(持仓类型,股东代码,股票代码)数量的指针,每个指针按照64位计算,需要16*(持仓类型,股东代码,股票代码)个byte实现hash的方法,我个人感觉这个不是重点,关键是降低复杂度,因为数量太庞大了,即使通过hash,数量依然很多,需要一种数据结构降低操作复杂度。

 

O(logN)是比较优化的了,能够做的就是通过何种方式降低这个N,比如通过数组或再加hash等。下面是pseudocodelonglong M=max((long long)持仓库类型.股东代码.股票代码)?

1.

*p=malloc(M*sizeof(U))?

memset(p,0,sizeof(U)*M)?

while(Proc->lock(p))

do

read or write or change p

done

 

2. N=>CPU数

*p1=malloc(M/N*sizeof(U))?

*p2=malloc(M/N*sizeof(U))?

...

*p(N1)=

malloc(M/N*sizeof(U))?

memset(p1,0,sizeof(U)*M/N)?

memset(p2,0,sizeof(U)*M/N)?

...

memset(p(n1),

0,sizeof(U)*M/N)?

CPUi=>read or write or change p(((longlong)持仓库类型.股东代码.股票代码) mod M)

 

大型实时算法,就需要简单我估计他们的算法应该是hash+跳跃表,不同支出就是如何处理(持仓类型,股东代码,股票代码)这三个值作为hash和跳跃表的key

 

【书记员注:独孤虎用一篇超长微信,震撼登场,但杯具了...】

龙博(轻扫一眼):@独孤虎,你要回忆一下白老师讲过的,hash表每天只初始化一次。你用的数据结构太复杂了,而且锁的力度太大。你这种设计,如何做到每个进程(线程)与其他几十个线程同步地对这一亿条记录进行操作?没可能的。所以,我给你零分!

 

帮主(插话):用sharding保证无锁?

 

龙博(立刻否定):不用任何sharding,无锁的核心在数据结构设计。你可以想象一下,两个线程如何同步地对一个10个元素大小的数组进行无锁操作(增加、删除、修改、查询)。想通这个问题,你就知道我怎么做这个无锁设计的了。

 

帮主:一个是任务queue,然后thread pool。【书记员注:其实帮主偷偷地上微博,向广大网友征集答案,如何设计无锁结构,用两个线程操作10个元素的数组?】

 

龙博:我再给你们说一遍,数据是所有进程完全共享的,不做划分。任何一条记录在任何时候都可能被该物理机上的一个进程访问(包括修改、删除、新增)

 

擅长高性能多处理器系统设计的唐博登场

【书记员注:下面,另一大牛,唐博,要登场了。唐博在高性能多处理器系统的设计上,独树一帜。】

 

唐博:我们DDoS的处理也有类似的问题,早就有答案了。与之不同的是,hash key 是可变长的URL。可以理解为16150长度的字符。大家复习一下

1. atomic instruction

2. CAS primitive

然后再来讨论比较靠谱。

 

【书记员注:唐博一上来,就叫大家去复习功课。计算机系的小伙伴们,你们还记得原子指令和CAS原语吗?】

 

龙博:类似的内存数据管理技术,用到了交易系统的几乎所有的内存数据结构中。因此可以最大程度发挥CPU的运算能力,减小同步、进程切换的开销。全异步、无锁、用户态和核心态数据共享等技术。为了提高运算效率,我甚至连乘法都是自己实现。。。

 

书记员注:我硅谷寒搞了N年的芯片设计,觉得龙博这句“连乘法都是自己实现的”,有点儿吹牛了。他应该不知道怎么设计高速乘法器。估计他都不知道啥是booth multiplier?如何设计硬件pipeline?】

 

龙博:交易所交易系统,就是屠龙之技啊。。。呵呵。例如数组的查询,数据有key + payload。如果你把所有key放到一起,那么整个数组的key可能被装进cache中,这样查询效率就很高了。。。这个也经常要用

 

独孤虎回来了

独孤虎(憋了一下午,又和自己的团队有了新方案,再次发布超长微信):

整个问题划分为两个部分1)、2)和三个阶段a)、b)、c):

1)网络操作部分,负责接收请求和返回应答;

2)内存操作部分,负责内存持仓记录的操作;

a)接收服务请求>

b)操作持仓记录>

c)返回服务应答

 

对于网络操作部分:

因为a)为了避免操作的复杂性;b)采用非阻塞式,操作比较快,所以网络操作部分采用一个线程:a)监听、读取和写入三个socket操作采用EPOLL方式以非阻塞方式执行;b)一旦接收到服务请求,该线程根据服务请求中的(持仓类型,股东代码,股票代码)信息,通过hash将请求信息发送给对应的进程(节点),进程之间的通信方式采用非阻塞的MPI;c)在每个轮训周期内,通过MPI_Test过程测试是否返回操作应答,如果返回,则驱动执行socket写入操作。

 

对于内存操作部分,前提条件为NUMA架构。【书记员注:请大家复习NUMA】

 

a)每个Node为一个进程,每个进程内多个线程,线程数量与每个node内的CPU

数量*每颗CPU内的核数相关;

b)线程按照线程池组织,采用一个无锁的队列实现生产者和消费者模式,即一个线程负责通过非阻塞的MPI将请求放入队列中,而其他线程则分别读取,并执行具体的持仓记录操作;

c)NUMA架构的数据存放策略采用firsttouch,即通过hash将持仓记录划分到每个节点,确保每个节点仅仅操作本地内存;

由于分配到每个节点的记录数量还是非常庞大,需要有效的数据结构组织这些数据,可选方案有三种:

a)hash

b)balanced trees

c)Skip List

 

上述三种方案都有很多种不同lockfree的实现方式,只要拿来用就ok了,但是balanced trees的操作比较复杂一点,首先排除。只剩下hash和Skip List:a)hash的问题需要每个节点分配一个非常非常大的数组,并且保证hash表的负载比较合理,可以采用将持仓类型+股东代码+股票代码形成一个字符串,在采用Murmurhash3这类算法进行hash;b)采用Hash+Skip List,当难以保证hash表的负载比较平衡的时候,可以采用小一点的hash表,但是每个表的元素为一个Skip List。

 

两种方式,关键要考虑持仓记录的变动情况,如果变动情况,不影响hash的负载均衡,采用方案a)。否则,采用方案b)

 

无锁操作,采用CAS方式实现hash和skip list的方式很多,拿过来用就可以。无锁方案很多,如果采用java实现,比如生产和消费者模式,就可以采用disruptor。

 

龙博(估计没看完独孤虎的长文,一直攻击其数据结构设计):你说cas指令随便用,但前提是cas或者其他无锁的数据结构需要满足某些条件,你的数据结构能满足这些条件么?这些都是很重要的工程问题。不是说,有cas, 有很多无锁设计和实现,直接拿来用就行。。。

 

独孤虎(赞许道):龙博,你一招鲜,吃遍天。二十年后还靠无锁设计、CAS吃饭。

 

龙博:我现在已不靠这个吃饭,靠这个吹牛。。。

 

独孤虎(接着给方案):1)所有节点的操作的内存是本地的,无需操作其他节点内容;2)hash表是一个数组,数组中的不同元素可以并行操作;3)数组内的每一个元素是一个链表,仅仅在头部插入和内部删除,有多种无锁操作方案。

 

龙博(还是说独孤虎的数据结构不对):你想象一下,所有这些数据在共享内存里面。。。如果是链表,很难实现完全的无锁设计。~ 零分

 

独孤虎:一下午,还是零分,我哭!

 

独孤虎:如果不用链表:第一种方式是copy on write,hash包中的每个元素为指向一个数组的指针,当指向插入和删除操作时,采用内存copy这个数组操作,然后在这个副本上进行插入和删除操作,此种方式不是最优,但是最容易想到的。

 

第二种方式是采用无链表的hash,有两个数组,第一个数组H是一个Hash表,存放一个无符号整数,指向第二个数组的一个具体位置,第二个数组D是一个非常大的数组,数组中的每个元素包括(持仓类型,股东代码,股票代码,记录数据,next),其中next指向D中的另一个位置,从而模拟一个链表:

a)添加操作,一旦通过(持仓类型,股东代码,股票代码)计算出在数组A中key所指D的位置已经存在值,则在数组D中查到next为0的元素tail,并变化一下(持仓类型,股东代码,股票代码)重新计算自己的hash,并插入到D中,然后采用CAS方式将其位置添加到tail的next?

b)删除操作:删除一个位置d,则采用CAS方式将父亲p中的next指向自己的next即可。

 

Penny :独孤虎的角度基本是做一个erp系统的sense,不是做高性能系统的sense啊,链表内存分布都是飘的,咋缓存感知啊!【书记员注:Penny是新鲜出炉的清华计算机博士,其创办的公司,拥有世界上最多的人均IP数量。】

 

独孤虎:工作太努力容易精神分裂。你们知道吗?Jeff Hammerbacher在哈佛读书时就精神分裂,返校毕业后先是加入华尔街做量子码工,之后是Facebook的早期数据科学家,后来是cloudera的创始人,因为工作太疯狂,精神分裂了,现在投身于基于数据科学的疾病治疗,主要是免疫和药物及基因的关系,为病人做个性化治疗。

 

【书记员注:估计独孤虎快被龙博搞精神崩溃了,准备向Jeff Hammerbacher学习。独孤虎究竟有没有精神分裂呢?】

 

一夜过去了…孤独虎精神抖擞地回来了!

欲知后事如何,且听下回分解......

 

【算法江湖】哈希之战【上】

标签:

原文地址:http://www.cnblogs.com/yymn/p/4604524.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!