算法技巧算法技巧利用质数随机遍历集合的一种实现
ivansli有个随机遍历数据集合的需求,描述如下:
① 从集合的某个随机位置开始遍历
② 随机获取集合中剩余数据
③ 重复步骤②,直到遍历完整个集合
当看到这个需求时,不知道你是否会想到什么实现方式?
最近在追溯go底层源码时发现,在调度器从其他 P 中 steal 可用 goroutine 时利用质数的特性,采用了一种特殊算法随机遍历 allp 数组。觉得很是经典,特此记录一下。
例如:通过下标随机遍历数组中的 8 个元素,则最终遍历数组的下标可能是 [0,1,2,3,4,5,6,7] 中的任意一种随机排列。
遍历步骤:
① 计算小于等于 count (这里是8) 的所有质数,得到数组 [1,3,5,7]
② 从质数数组随机获取一个质数 inc,例如:5
③ 获取一个初识随机数 R,例如:16
④ (R+inc) % count = pos,pos 就是数组下标
⑤ 把 第④步 计算结果 pos 作为第三步中的 R,继续下一次循环,直到遍历完整个数组
按照遍历步骤,进行一轮计算,过程如下:
1 2 3 4 5 6 7 8
| ① R:16 inc:5 count:8 (16+5)%8 = pos:5 // 第一个R是单独获取的随机数,计算结果 pos 作为下一次 R 的输入 ② R:5 inc:5 count:8 (5+5)%8 = pos:2 ③ R:2 inc:5 count:8 (2+5)%8 = pos:7 ④ R:7 inc:5 count:8 (7+5)%8 = pos:4 ⑤ R:4 inc:5 count:8 (4+5)%8 = pos:1 ⑥ R:1 inc:5 count:8 (1+5)%8 = pos:6 ⑦ R:6 inc:5 count:8 (6+5)%8 = pos:3 ⑧ R:3 inc:5 count:8 (3+5)%8 = pos:0
|
通过上面计算,最终获得的遍历下标为 [5,2,7,4,1,6,3,0]。需要注意的是:每一轮遍历,第一次传入的随机数 R 与选取的质数 inc 不同,会得到不同的遍历次序。
在 go 底层源码的 func findrunnable() (gp *g, inheritTime bool)
方法中可以看到实现逻辑,如下所示:
代码位置:src/runtime/proc.go
注意:>=go1.16
findrunnable()代码有所变动,可在 go1.12 ~ go1.15 版本中看到相关逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| func findrunnable() (gp *g, inheritTime bool) {
for i := 0; i < 4; i++ { for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() { if gp := runqsteal(_p_, allp[enum.position()], stealRunNextG); gp != nil { return gp, false } } }
}
var stealOrder randomOrder
type randomOrder struct { count uint32 coprimes []uint32 }
func (ord *randomOrder) reset(count uint32) { ord.count = count ord.coprimes = ord.coprimes[:0]
for i := uint32(1); i <= count; i++ { if gcd(i, count) == 1 { ord.coprimes = append(ord.coprimes, i) } } }
func gcd(a, b uint32) uint32 { for b != 0 { a, b = b, a%b } return a }
type randomEnum struct { i uint32 count uint32 pos uint32 inc uint32 }
func (ord *randomOrder) start(i uint32) randomEnum { return randomEnum{ count: ord.count, pos: i % ord.count, inc: ord.coprimes[i%uint32(len(ord.coprimes))], } }
func (enum *randomEnum) done() bool { return enum.i == enum.count }
func (enum *randomEnum) next() { enum.i++ enum.pos = (enum.pos + enum.inc) % enum.count }
func (enum *randomEnum) position() uint32 { return enum.pos }
|
这种实现方式被视为是一种公平的数据集合遍历实现方式,想必在平时的业务开发过程中有很多地方会用的到。