棋牌AI手记——高性能计数器

0x00 问题来由

最近在做一个斗地主网游的AI。由于斗地主这种游戏,同样数字不同花式的牌是等效的,所以,很多计算我们不需要知道具体是哪几张牌,只需要关心每种牌有多少张。

于是,我引入了一个数据结构,姑且叫做CardsVector,表示每种牌的数量。为了方便描述,本文约定以下方式来表示一个CardsVector,与具体实现无关

X = {3:3, 4:2} // X为一个CardsVector,包含3张3,2张4
其中,X[3] == 3, X[4] == 2

这种数据结构除了初始化,还有两个主要的操作:

  1. 计算一个CardsVector是否包含另一个CardsVector。场景为判断某种出牌组合在不在当前手牌中。比如:
设A={3:2, 4:2, 5:2},X={3:1, 4:1, 5:1},Y={4:3,5:3},Z={4:1, 5:1, 6:1}
那么,A包含A、X,不包含Y、Z
  1. 从一个CardsVector中,减去一个CardsVector。场景为出牌后从手牌中移除出掉的牌,如:
设A={3:2, 4:2, 5:2},X={3:1, 4:1},A移除X后为{3:1, 4:1, 5:2}

0x01 初步方案

一开始,我是这样定义CardsVector的:

type CardsVector [16]int

很简单粗暴,用一个16个元素的int数组来保存,每个元素的值表示下标对应的牌的数量。“判断包含”和“移除”算法如下:

// Contains 判断包含
func (v CardsVector) Contains(other CardsVector) bool {
  for i := 1; i < 16; i++ {
    if v[i] < other[i] {
      return false
    }
  }
  return true
}
// Remove 移除
func (v CardsVector) Remove(other CardsVector) (res CardsVector) {
  for i := 1; i < 16; i++ {
    res[i] = v[i] - other[i]
  }
}

嗯,简单粗暴,性能凑合。如果是一般场合,也就这样算了。可是,对这个数据结构的操作,在我写的棋牌AI中,属于最高频的调用,优化性能收益非常大。所以,身为强迫症患者的我决定进一步压榨性能

0x02 第一次优化

根据我们的需求,总共只有15种牌(A/2/3/4/5/6/7/8/9/10/J/Q/K/小王/大王),不会多也不会少。很多大牛说过,for循环展开有助提高性能。姑且试试。
但有没有性能优化,还是要拉出来溜溜看。于是啪啪啪的写unittest,go test之

go test -bench=.VecContains.
BenchmarkVecContainsUsingForLoop-2      20000000                73.9 ns/op
BenchmarkVecContainsNoForLoop-2         100000000               34.0 ns/op

go test -bench=.VecRemove.
BenchmarkVecRemoveUsingForLoop-2        20000000                82.7 ns/op
BenchmarkVecRemoveNoForLoop-2           20000000                57.3 ns/op

不错不错,有了大幅提升!但,还能不能更好呢?

0x03 不屈不挠的折腾

其实,以我个人性格,如果只提升1倍左右的话,我是懒得专门写文章记录的
注意到这么一点:我们的场景是记录每种牌的数量。一副扑克牌中,每种数字的牌,最多只有4张。上面的数据结构,简单粗暴的用一个[]int来保存。我们能否从这方面入手优化呢?
[]int8的方案我自己先否决了。一个int刚好就是cpu一个字长,处理速度会更快些,即使在不同环境,cpu缓存等不确定因素可能有影响,也不会有太大的提升。
重要事情再默念三遍——每种最多只有4张!每种最多只有4张!每种最多只有4张!。
也就是说,我们可以只用3个位来表示一种牌的数量。3X16=48,貌似一个uint64能解决问题。
但,能表示数据只是一个前提,这个数据结构是否可用,还取决于能否实现上面两个关键操作。而如果每张牌的数据单独通过连串位运算单独提取出来,重复上面的算法,固然是能实现功能的。但这么繁琐的操作,必定令性能大打折扣。这不符合我们的初衷。于是,我们的思路就去到,通过神器的位运算,实现多个位的同时操作

  • 判断包含
    对于问题的描述,我们可以给出以下的等价表达
    a) A包含B
    b) 对于任意一种牌x,A[x] >= B[x]
    c) 对于任意牌x,有A[x]-B[x] >= 0
    对于一个k进制多位数的减法,我们可以表示为(打公式无能,仅用一个4位数示意):
A = a3*k^3+a2*k^2+a1*k^1+a0*k^0  (任意0 <= ax < k)
B = b3*k^3+b2*k^2+b1*k^1+b0*k^0  (任意0 <= bx < k)
A-B=(a3-b3)*k^3+(a2-b2)*k^2+(a1-b1)*k^1+(a0-b0)*k^0
设 cx = ax - bx
当任意ax>=bx时,A-B可以表示为“c3 c2 c1 c0”这个4位k进展数

回到我们的上文的设定,我们一个CardsVector用一个48位uint表示,我们可以视为一个16位的8进制数。但这样,我们拿两个不确定包含关系的CardsVector进行比较时,无法保证任意ax>=bx,相减后的结果无法判断是否符合要求
好吧,我们做一个修正(此处跳过各种不靠谱的草稿本尝试),我们用4位表示一种牌的数量。4X15=60,仍然能用一个uint64表示。这时候,每种牌数量二进制的最高位,必然是0的。然后,我们对上面描述c进一步转化:
d) 对任意牌x,有A[x] + C - B[x] >= C (C为任一常数)
我们在对两个A和B这两个CardsVector相减前,先将A中每种牌的数量最高位置为1,相当于加8。由于原本每种牌最多只有4张,所以必然有A[x]+8>B[x]。但如果A[x]<B[x],则有0 < A[x]+8-B[x] < 8。回到位运算中,只需要判断:

((A[x] | 0x08) - B[x]) & 0x08 == 0x08

说到这里,我们的Contains函数就呼之欲出了

type CardsVector uint64
const mask CardsVector = 0x8888888888888888 // 多么吉利的数字
func (v  CardsVector ) Contains(other CardsVector ) bool {
  return ((v | mask) - other) & mask == mask
}
  • 移除
    有了上述的推断,在保证A包含B的前提下,A移除B,就刚好等于A-B了,代码就懒得写了

看起来万事俱备,只欠bench了。不废话,来

go test -bench=.Contains
BenchmarkVecContainsUsingForLoop-2      20000000                64.3 ns/op
BenchmarkVecContainsNoForLoop-2         30000000                44.5 ns/op
BenchmarkVecBContains-2                 2000000000               1.09 ns/op

go test -bench=.Remove
BenchmarkVecRemoveUsingForLoop-2        20000000                92.8 ns/op
BenchmarkVecRemoveNoForLoop-2           100000000               63.9 ns/op
BenchmarkVecBRemove-2                   2000000000               1.15 ns/op

0x04 结论

我觉得我不需要说什么了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容