标签:
欢迎个人转发朋友圈,机构及媒体转载需在开篇声明,转自微信公号“知象科技”
论战主角之一龙博:知象科技CEO,欲了解龙博及知象科技,请点击文末“阅读原文”。
这是“美丽互联”微信群里的一次算法论战,感谢书记员硅谷寒(梁寒)精彩的说书般的整理。
书接上文
一夜过去了…孤独虎精神抖擞的回来了!
【书记员注:上集说到独孤虎被龙博两次判零分,已经到了精神分裂的边缘。于是他决定回家休养生息,以图再战。果不其然,第二天,独孤虎首先跳出来,带来了他的第四种方案。我们的眼睛又一次被他的长微信轰炸。。。】
独孤虎:昨天给出的第三个方案存在如下两个问题:
1)没有考虑结构体的填充问题,struct结构内的数据要对齐,从而导致其数据占有更大的内存;
2)CAS的操作受限,目前gcc仅仅针对1,2,4或8字节长度的int类型提供了CAS以及原子性
的加减逻辑运算;
3)没有考虑数据的读写原子性问题,有可能这个元素在同时读写,从而导致问题?改进之后第四种方法的核心思想是每个线程仅仅操作一个固定范围的数组,确保一个元素最多仅有一个线程在操作,从而保证操作的原子性。
下面给出第四种方案:
a)每个Node为一个进程,每个进程内K个线程,K与node内的CPU数量*每颗CPU内的核数相关;
b)线程按照线程池组织,采用K个无锁的队列实现生产者和消费者模式,即一个线程负责通过非阻塞的MPI接收请求,并将请求放入特定的队列中,而其他每一个线程读取特定队列中的请求,并执行具体的持仓记录操作;
c)NUMA架构的数据存放策略采用firsttouch,即通过hash将持仓记录划分到每个节点,确保每个节点仅仅操作本地内存;
采用一个非常大的数组D作为Hash表,表中的每个元素值包括(持仓类型,股东代码,股票代码,记录数据,count),如果存在key值冲突,则相同key的值连续放置,其中count表示后继节点有多少与之具有相同key的元素。其中股东代码可以表示为一个字节字符和四个字节无符号整数,共5个字节,股票代码表示为2个字节的无符号整数,股票持仓为8个字节整数,持仓类型和count公用一个字节(分别占用四位),因此采用如下的结构保存
struct record {
unsigned long compoundKey?
unsigned long data?
}
其中data表示股票持仓数据,而compoundKey为(持仓类型,股东代码,股票代码,count)的复用,其中最低四位表示count。将数组D划分为K段,每段由一个线程负责操作,即根据hash值确定所在范围,然后放入对应的队列中由对应的线程处理,由于确保了每个元素仅有一个线程操作,从而整个操作过程无需CAS或者枷锁。
1)插入操作,根据(持仓类型,股东代码,股票代码)计算key:如果D[key]所在位置的compoundKey非零,则冲突,查看下一个位置,即key+1位置,直到不冲突为止,即compoundKey为0时,对所在位置的元素赋值。
2)删除操作,如果要删除一个hash值为key的元素,则首先找到该元素所在位置key+m,m大于等于0。a)如果key+m所在位置的count为0,则直接赋值为0,并跳转到步骤c)? b)如果key+m所在位置的count大于0,则逐次向后操作,将此位置设置为后继的、并且hash值为key的元素,直到hash值为key并且count值为0时结束,并跳转到步骤c)?c)从此位置向前一直到key,将所有具有相同key所在位置的count减1。
3)更改操作/读取操作:比较简单。
这个方法,仅仅需要多个无锁队列,其他的操作即无需锁,也无需CAS。对数据和请求进行了两次划分,第一个次是根据hash将数据和请求划分到不同的节点(每个节点具有多个CPU,共享本地内存),第二次是根据hash将数据和请求划分到数组的不同位置段,并由不同线程负责操作
龙博:@独孤虎在接近正确答案。“部分”思路正确了。不过你不用考虑生产者消费者这种复杂的模型。要考虑如果让多个进程无锁同步地访问同一个大数组(哈希表),不要分区。
独孤虎(相当开心):我跟团队又讨论了一个下午。
龙博解密
龙博:如果你能设计出完全的无锁结构,就没必要做这个划分了。所以能够设计出完全的无锁数据结构,是关键。作为compound key的三部分,我已经告诉你每个部分的特征了,你试试看。
龙博:其实这个题目在白老师的发言里面已经给了一个很大的提示,哈希表。别看这个提示很小,绝大部分人连这一步都跨不过去。无锁数据结构的关键是什么?就是 key不要超出字长 ...在我们现在的机器上,字长就是8字,64bit...提示到这里,该做出来了吧...
龙博:因为只要你的key不超出字长,哈希表的新增、删除、修改操作都可以是原子操作。就是机器指令直接支持的原子操作,也就是“无锁”操作。哈希表的新增,删除和修改操作就是对这个8字进行赋值而已。。。明白了么?
【书记员注:我其实蛮失望的,原来最关键的地方就是拼一个64bit以内的Key,感觉像是郭靖的亢龙有悔,最厉害的一招就是最平淡无奇的一招!】
龙博:这是最核心的,说出来其实也很简单。我在总结一下,最关键的几部分:
1. 开放地址探测的哈希(非链式哈希!)
2. 哈希表的key值压缩在一个8字以内
3. 稍微特别考虑一下哈希表的删除(你得维护key冲突的时候的链条)
龙博:实际测试结果,一亿条记录,在哈希表的装载率(load factor)为70%的情况下,平均查询次数为1.1次,最大查询次数不超过4次。。这应该是最好的结果了。key+value 共16字节。这应该是最高效的存储结构了。
帮主:这么来说,这题的难点在哪?地址探测,最坏情况可能是 O (n)。所以关键是将key编码成64bits?根据那三种信息,不是显而易见的吗?我倒觉得,线性探测是核心,但那玩意有worstcase。独孤虎居然花了那么多时间,那么严肃的研究,感觉龙博在逗我们玩。
【书记员注:呵呵,帮主跟我一样,心有不甘,觉得自己败在了最平凡的一招上。】
龙博:用开放地址探测的散列,要保证在一定步数内找到一个空位,你必须让数组长度为一个质数。在装载率为70%的情况下,一个质数长度的数组,基本就是几步之内你就一定能找到一个空位。
帮主:好吧。关键是你怎么能控制一天的交易数?一开始开多大数组?万一当天的订单数好几亿,超出了数组容量呢?
龙博:一天的交易数有上限,到目前为止,还没突破过。但即使突破,交易系统后台会返回一个“技术错误“告诉券商这笔交易无法执行,但交易系统一切正常。包括内存被分配光也是一样,你要保证在极端情况下系统可以正常反应。
帮主:你说的一天可能上亿,整个都可能是新增的,对吗?
龙博:可以,因为这个哈希表增加一个元素的操作开销跟查询没什么分别。
【独孤虎团队,经过一番修整,终于给出了最终的第五个完美方案】
独孤虎:现设计数据结构如下:
struct record {
unsigned long compoundKey?
unsigned long data?
}
其中data表示股票持仓数据,而compoundKey为(股东代码,股票代码,持仓类型,flag)的复合体,股东代码占用最63~27位(5位字符和四个字节无符号整数),股票代码占26~11位(两个字节),持仓类型占用10~6位(四个字节),isTail占用第5位,表示是否为尾部,即后继元素中没有相同hash值的元素。doWrite占用第4位,表示该key正在更改/写入,readerCount占用3~0为表示该key正在读的线程数量。
采用非常大的数组,数组中的每个元素为struct record
具体操作如下:
1)插入操作,根据(持仓类型,股东代码,股票代码)计算hashkey:
a)如果D[key]所在位置doWrite为1,则循环判断,直到其值为0为止(在循环中采用cache操作,直接从内存读取doWrite),继续执行?
b)采用CAS操作,将doWrite设置为1,如果操作失败,则跳转到步骤a),如果成功,则继续执行;
c)如果readerCount非0,则循环等待,直到readerCount为0(在循环中采用cache操作,直接从内存读取);
d)如果readerCount为0,则从key开始连续查找到key相同并且isTail为1的元素,然后将其设置为0,并从后继元素找到第一个compoundKey为0的位置,并写入;
e)将doWrite为0,并采用cache操作,将其写回内存。
2)删除操作:如果要删除一个hash值为key的元素,其操作类似与插入操作,不同的地方是在获取doWrite和readerCount之后:
a)如果被删除元素的isTail为1,则从该位置开始到key进行查找,找到第一个
hash值为key的元素,将其的isTail设置为1;
b)如果被删除的元素的isTail为0,则需要继续查找,将其isTail为1并且hash值为
key的元素拷贝到这里,然后将原来尾部元素的上一个元素的isTail设置为1;
独孤虎收获满满,一掷千金
独孤虎(兴奋道):我们团队上可证NP,下可写哈希。
独孤虎:感谢龙博慷慨地贡献出一个如此优秀系统的核心算法!比如做大规模流量交易平台,算法相当重要。如果一天要处理百亿千亿次交易,就需要考虑系统核心算法的性能,而不仅仅是拼机器的数量和硬件性能。
独孤虎:我发个红包给群里所有人!!
【书记员注:独孤虎乃真土豪也,群里平均每个人收到了¥50...独孤虎在此次论战中表现出了超级的持久作战力,强大的团队协作精神,堪称“中国互联网之铁血军魂”!】
【书记员注:最后,本群创始人,院长,出来总结陈词,论功行赏。院长是网络系统安全界的超级高手,尤其精通PowerPC、MIPS、ARM这些RISC计算机系统!】
院长总结陈词
院长:
1. 龙博在巨大项目压力下,靠straightsmart能想出算法,并在白老师的鼓励和指导下,实践完成,为8年后的股波波的到来立下了卓越贡献。建议中央军委给予龙博记一等功,花翅一枚。
2. 帮主基础扎实,算法雄厚。由于长期在search领域,对hash算法,retrivial系统了若熊掌。非常的脚工赞。但由于他打酱油太多。窃以为龙博略微牛一些。
3. 独孤虎,从理论界,再次横空跨界。身体力行,团队作战。令人泣血。方案里有链表,有cas,还考虑了cache。深感算法设计课需要重新设计。手工赞!
院长最后科普了一下“原子操作”的概念:
原子操作与cpu强相关。不同的cpu实现的方式不一样。由于现代cpu都是load store模型。基本上可以理解对一个变量赋值需要:load;寄存器操作一哈;写回。所以,这3个指令之间可能中断,并发线程都有touch那个变量的可能,形成并发冲突。
现代cpu都会提供一些相关的指令,从而操作系统可以提供一些atomic原语,例如,add,set,dec,test等等。其实现原理是通过一些特殊的指令可以监视其他逻辑对一个memoryspace是否有touch,通过监控bus上的transaction。
简单说来,就是,如果我想own,我reserve这个mem。一旦reserve失败或者被abort,来回try。
史上最强之技术聊天,持续了两天半,以独孤虎团队的胜利告终。台上选手高声论战,台下群友默默潜水,虽然台下也有许多“高手”,但都怕一出声,不着调,毁了自己半生清誉。这也算是一道有趣的风景吧。
本次讨论,难得众高手相聚于“美丽互联”,正如古人所云:
“今番良晤,豪兴不浅,若得山水重逢,再当把酒言欢。”
是夜,暖风轻,圆月明,朋友聚还散,路人停复行。
标签:
原文地址:http://www.cnblogs.com/yymn/p/4604532.html