目录
1. 版权
本文为原创, 遵循 CC 4.0 BY-SA 版权协议, 转载需注明出处: https://blog.youkuaiyun.com/big_cheng/article/details/120461065.
文中代码属于 public domain (无版权).
2. 概述
上一篇里sesmgr 用m2 map 存放每个session 的过期时间来模拟堆, 如果使用slice 的话扩容copy 有耗时.
但是之后考虑slice 的性能应该高于map, 而且堆本身在概念上就是个数组. 如果m2 改用slice 的话: a) 不能一次申请太多内存; b) 避免copy; c) 删除元素没有空位问题, 因为待删元素总会先移到堆尾.
由此想到可以分块来申请内存. 例如先申请10w 个空位; 用完还需要时 再申请10w 个. 如此内存是缓慢增长的, 并且少copy.
因为内存实际上是有限的. 例如100w 个session, 假设每个需100B, 就是100M. int32/uint32 的最大值是20/40多亿, 相应个数的session 就是100~200G 内存. 所以很多时候sesmgr 可以限制session 的数量.
首先试验slice 的性能.
3. p_slice.go
func main() {
rand.Seed(time.Now().Unix())
n := 100_000
idxs := make([]int, n) // shuffled
{
m := map[int]int{}
for i := 0; i < n; i++ {
m[i] = 0
}
i := 0
for k, _ := range m {
idxs[i] = k
i++
}
}
arr := make([][2]int, n)
t0 := time.Now()
for _, idx := range idxs {
arr[idx] = [2]int{rand.Int(), rand.Int()}
}
println("init arr\n\t", len(arr), time.Now().Sub(t0).String())
...
}
上面idxs 是乱序的slice 下标值, 因为map 是无序的.
单线程遍历:
println("单线程遍历一遍")
t0 = time.Now()
for _, idx := range idxs {
if v := arr[idx]; v[0] >= 0 {
}
}
println("\t", time.Now().Sub(t0).String())
4根线程并发读+改:
println("4线程查改20s")
chX := make(chan int)
chOut := make(chan int)
mu := new(sync.Mutex)
for i := 0; i < 4; i++ {
go func(i0 int) {
cnt := 0
for {
select {
case <-chX: // 退出
chOut <- cnt
return
default:
mu.Lock()
idx := idxs[(i0+cnt*4)%n]
if v := arr[idx]; v[0] >= 0 {
v[1] = rand.Int()
arr[idx] = v
}
cnt++
mu.Unlock()
}
}
}(i)
}
搜集结果:
time.Sleep(20 * time.Second)
close(chX)
cnts, total := [4]int{}, 0
for i := 0; i < 4; i++ {
cnts[i] = <-chOut
total += cnts[i]
}
fmt.Printf("\t %v total %d(%d)\n", cnts, total, len(strconv.Itoa(total)))
手测结果:
// 数据量 init 单线程遍历一遍 4线程查改20s 内存
// 10w 7ms 0 7kw 10M
// 100w 70ms 0 5kw 105M
相比于p_map int: 单线程遍历非常快, 4线程处理数增加1/4~3/4, 内存也稍有减少. 应该是可行的.
4. t_ses_l2.go
ses
package main
import (
"container/heap"
"errors"
"fmt"
"math/rand"
"strconv"
"sync"
"time"
)
const default_duration = 30 * time.Minute // 默认有效期, 30min
type ses struct {
key2 int // 在l2 的下标
attrs map[string]interface{} // session 属性
}
func (s ses) String() string {
return fmt.Sprintf("ses/key2=%d", s.key2)
}
sesmgr
const LEN1 = 100_000
type sesmgr struct {
// m2 是map 模拟的堆, key2 是下标, 总是从0 开始连续. 节点0 过期时间最小.
//
// 堆节点swap 时, 由节点.key1 同步更新m1 ses.key2. 例如:
// 123 => ses1(key2=0) => 堆节点0 {123, t1}, 456 => ses2(key2=1) => 堆节点1 {456, t2}.
// 堆节点0,1 swap时, 由123,456 找到ses1,ses2 后更新ses.key2:
// 123 => ses1(key2=1) => 堆节点1 {123, t1}, 456 => ses2(key2=0) => 堆节点0 {456, t2}.
mu *sync.Mutex
m1 map[uint32]*ses // session map: key1 => ses.
l2 []*[LEN1][2]uint32 // 二维数组, key2 => {key1, 过期时间 (unix seconds)}.
}
func (psm *sesmgr) Len() int {
sm := *psm
return len(sm.m1)
}
func (psm *sesmgr) Less(i, j int) bool {
sm := *psm
i1, i2 := i/LEN1, i%LEN1
j1, j2 := j/LEN1, j%LEN1
return sm.l2[i1][i2][1] < sm.l2[j1][j2][1]
}
func (psm *sesmgr) Swap(i, j int) {
sm := *psm
i1, i2 := i/LEN1, i%LEN1
j1, j2 := j/LEN1, j%LEN1
sm.m1[sm.l2[i1][i2][0]].key2 = j
sm.m1[sm.l2[j1][j2][0]].key2 = i
sm.l2[i1][i2], sm.l2[j1][j2] = sm.l2[j1][j2], sm.l2[i1][i2]
}
m2 改为l2 二维数组, 每次申请LEN1 个空位. i/LEN1、i%LEN1 就是在一、二维的下标.
l2 的第二维使用指针拷贝更快, 参见补充.
func (psm *sesmgr) Push(x interface{}) {
sm := *psm
i := len(sm.m1) - 1 /* 要减1, 因为m1 已先加了. */
i1, i2 := i/LEN1, i%LEN1
if i1 >= len(sm.l2) { // 扩容
psm.l2 = append(psm.l2, &[LEN1][2]uint32{}) /* 这里不能用sm, 因为它是拷贝. */
}
psm.l2[i1][i2] = x.([2]uint32)
}
func (psm *sesmgr) Pop() interface{} {
sm := *psm
i := len(sm.m1) - 1
i1, i2 := i/LEN1, i%LEN1
x := sm.l2[i1][i2]
sm.l2[i1][i2] = [2]uint32{} // 清除
return x
}
func newSesmgr() *sesmgr {
return &sesmgr{
mu: new(sync.Mutex),
m1: map[uint32]*ses{},
l2: []*[LEN1][2]uint32{},
}
}
Push 方法注意append 要用psm 而非sm, 因为sm 是拷贝. 例如:
type ctT struct {
l []int
}
func test_slice3() {
ct := ctT{
l: []int{0},
}
ct2 := ct
fmt.Printf("%p %p\n", &ct, &ct2) // 0xc000004078 0xc000004090 - 拷贝, 地址变了
fmt.Printf("%p %p\n", ct.l, ct2.l) // 0xc000016088 0xc000016088 - l[0]的地址, 相同
ct2.l = append(ct2.l, 1)
fmt.Printf("%p %p\n", ct.l, ct2.l) // 0xc000016088 0xc0000160c0
println(len(ct.l), len(ct2.l)) // 1 2
}
新建session
// 新建session.
func (psm *sesmgr) newSes() (*ses, error) {
sm := *psm
sm.mu.Lock()
defer sm.mu.Unlock()
var key1 uint32
for i := 0; i < 11; i++ {
if i == 10 { // 尝试10次
return nil, errors.New("sesmgr: new key fail")
} else {
j := rand.Uint32()
if _, ok := sm.m1[j]; !ok {
key1 = j
break
}
}
}
key2 := len(sm.m1)
v1 := &ses{
key2: key2,
attrs: map[string]interface{}{},
}
sm.m1[key1] = v1 /* 要先加m1, 因为heap.Push 内部会去m1 查找. */
v2 := [2]uint32{key1, rand.Uint32()} // 过期时间用随机值代替
heap.Push(psm, v2)
return v1, nil
}
查找session
// 查找session. 如果找到, 过期时间增加默认有效期.
func (psm *sesmgr) getSes(key1 uint32) *ses {
sm := *psm
sm.mu.Lock()
defer sm.mu.Unlock()
ses, ok := sm.m1[key1]
if !ok {
return nil
}
i := ses.key2
i1, i2 := i/LEN1, i%LEN1
v2 := sm.l2[i1][i2]
v2[1] = v2[1] + uint32(default_duration/time.Second)
sm.l2[i1][i2] = v2
heap.Fix(psm, i)
return ses
}
删除session
// 删除session.
func (psm *sesmgr) delSes(key1 uint32) {
sm := *psm
sm.mu.Lock()
defer sm.mu.Unlock()
ses, ok := sm.m1[key1]
if !ok {
return
}
heap.Remove(psm, ses.key2)
delete(sm.m1, key1)
}
测试增删改
func main() {
rand.Seed(time.Now().Unix())
sm := newSesmgr()
ses1, _ := sm.newSes()
ses2, _ := sm.newSes()
debug(sm, ses1, ses2)
fmt.Println("\nses2 过期时间减到0")
i := ses2.key2
i1, i2 := i/LEN1, i%LEN1
ses2V2 := sm.l2[i1][i2]
ses2V2[1] = 0
sm.l2[i1][i2] = ses2V2
heap.Fix(sm, i)
debug(sm, ses1, ses2)
fmt.Println("\n删除ses2, 新增ses3(显示为ses2)")
ses2Key1 := ses2V2[0]
sm.delSes(ses2Key1)
ses2, _ = sm.newSes()
debug(sm, ses1, ses2)
...
}
func debug(sm *sesmgr, ses1, ses2 *ses) {
i, j := ses1.key2, ses2.key2
i1, i2 := i/LEN1, i%LEN1
j1, j2 := j/LEN1, j%LEN1
ses1V2 := sm.l2[i1][i2]
ses2V2 := sm.l2[j1][j2]
fmt.Printf("ses1\n\tkey1=>ses\t\t%d => %v\n\tkey2=>{key1 expire}\t%d => %v\n",
ses1V2[0], ses1, ses1.key2, ses1V2)
fmt.Printf("ses2\n\tkey1=>ses\t\t%d => %v\n\tkey2=>{key1 expire}\t%d => %v\n",
ses2V2[0], ses2, ses2.key2, ses2V2)
}
实测结果一例:
ses1
key1=>ses 950647161 => ses/key2=1
key2=>{key1 expire} 1 => [950647161 706409805]
ses2
key1=>ses 116464702 => ses/key2=0
key2=>{key1 expire} 0 => [116464702 175556386]
ses2 过期时间减到0
ses1
key1=>ses 950647161 => ses/key2=1
key2=>{key1 expire} 1 => [950647161 706409805]
ses2
key1=>ses 116464702 => ses/key2=0
key2=>{key1 expire} 0 => [116464702 0]
删除ses2, 新增ses3(显示为ses2)
ses1
key1=>ses 950647161 => ses/key2=0
key2=>{key1 expire} 0 => [950647161 706409805]
ses2
key1=>ses 3387079671 => ses/key2=1
key2=>{key1 expire} 1 => [3387079671 3950566886]
测试性能
fmt.Println("\ninit map")
n := 100_000
t0 := time.Now()
keys1 := make([]uint32, n)
for i := 0; i < n; i++ {
if ses, err := sm.newSes(); err != nil {
panic(err)
} else {
j := ses.key2
j1, j2 := j/LEN1, j%LEN1
keys1[i] = sm.l2[j1][j2][0]
}
}
fmt.Println("\t", n, time.Now().Sub(t0).String(), "len(l2)", len(sm.l2))
单线程遍历一遍:
fmt.Println("单线程遍历一遍")
t0 = time.Now()
for _, key1 := range keys1 {
sm.getSes(key1)
}
fmt.Println("\t", time.Now().Sub(t0).String())
4根线程并发遍历:
fmt.Println("4线程遍历20s")
chX := make(chan int)
chOut := make(chan int)
for i := 0; i < 4; i++ {
go func(i0 int) {
cnt := 0
for {
select {
case <-chX: // 退出
chOut <- cnt
return
default:
key1 := keys1[(i0+cnt*4)%n]
sm.getSes(key1)
cnt++
}
}
}(i)
}
搜集结果:
time.Sleep(20 * time.Second)
close(chX)
cnts, total := [4]int{}, 0
for i := 0; i < 4; i++ {
cnts[i] = <-chOut
total += cnts[i]
}
fmt.Printf("\t %v total %d(%d)\n", cnts, total, len(strconv.Itoa(total)))
单线程删除1/10:
fmt.Println("单线程删除1/10")
t0 = time.Now()
for i := n / 10; i >= 0; i-- {
sm.delSes(sm.l2[0][0][0])
}
fmt.Println("\t", time.Now().Sub(t0).String(), len(sm.m1))
手测结果对比:
p_map int
// 数据量 init 单线程遍历一遍 4线程查改20s 内存
// 10w 20ms 5ms 4kw 10M
// 100w 200ms 80ms 4kw 120M
t_ses
// 数据量 init 单线程遍历一遍 4线程遍历20s 内存 单线程删除1/10
// 10w 120ms 40ms 2.3kw 16M 60ms
// 100w 1.6s 500ms 1.9kw 197M 780ms
t_ses_l2
// 数据量 init 单线程遍历一遍 4线程遍历20s 内存 单线程删除1/10
// 10w 60ms 20ms 4.4kw 15M 20ms
// 100w 1s 200ms 3.9kw 140M 370ms
效果不错, 速度/处理数都提升了约1倍, 内存也有所减少.
5. 补充
拷贝的性能
const n = 100_000
// 2ms 20ms 0s - 拷贝10个指针不要时间.
func twoDim() {
println("l 1")
l := make([][n][2]int, 1)
arr := &l[0]
for i := 0; i < n; i++ {
arr[i] = [2]int{rand.Int(), rand.Int()}
}
// println(l[0][0][0], l[0][0][1], l[0][99][0], l[0][99][1])
//
t0 := time.Now()
l = append(l, [n][2]int{})
println(time.Now().Sub(t0).String())
// println(l[0][0][0], l[0][0][1], l[0][99][0], l[0][99][1])
println("\nl 10")
l = make([][n][2]int, 10)
// println("len", len(l), "cap", cap(l))
for i := 0; i < 10; i++ {
arr := &l[i]
for j := 0; j < n; j++ {
arr[j] = [2]int{rand.Int(), rand.Int()}
}
}
// println(l[0][0][0], l[0][0][1], l[9][99][0], l[9][99][1])
//
t0 = time.Now()
l = append(l, [n][2]int{})
println(time.Now().Sub(t0).String())
// println("len", len(l), "cap", cap(l))
// println(l[0][0][0], l[0][0][1], l[9][99][0], l[9][99][1])
println("\npl 10")
pl := make([]*[n][2]int, 10)
for i := 0; i < 10; i++ {
arr := [n][2]int{}
for j := 0; j < n; j++ {
arr[j] = [2]int{rand.Int(), rand.Int()}
}
pl[i] = &arr
}
// println(pl[0][0][0], pl[0][0][1], pl[9][99][0], pl[9][99][1])
//
t0 = time.Now()
pl = append(pl, &[n][2]int{})
println(time.Now().Sub(t0).String())
// println("len", len(pl), "cap", cap(pl))
// println(pl[0][0][0], pl[0][0][1], pl[9][99][0], pl[9][99][1])
}
拷贝指针几乎不需要时间, 可见第二维要定义成指针类型.