Golang性能优化实践

内存警察

警惕一切隐式内存分配

典型case:

  函数返回了字符串、切片,警惕一切字符串

传进去的输入,函数内部重新分配了一个新的内存返回

对象复用

1.sync.pool

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
import "sync"
 
func NewPoolCh[T any](fn func() T, chLen int) *PoolCh[T] {
    return &PoolCh[T]{
        ch:   make(chan T, chLen),
        pool: NewPool(fn),
    }
}
 
type PoolCh[T any] struct {
    ch   chan T
    pool *Pool[T]
}
 
func (p *PoolCh[T]) Get() T {
    select {
    case x := <-p.ch:
        return x
    default:
        return p.pool.Get()
    }
}
 
func (p *PoolCh[T]) Put(x T) {
    select {
    case p.ch <- x:
    default:
        p.pool.Put(x)
    }
}
 
func NewPool[T any](fn func() T) *Pool[T] {
    return &Pool[T]{
        pool: &sync.Pool{New: func() any { return fn() }},
    }
}
 
type Pool[T any] struct {
    pool *sync.Pool
}
 
func (p *Pool[T]) Get() T {
    return p.pool.Get().(T)
}
 
func (p *Pool[T]) Put(x T) {
    p.pool.Put(x)
}

  

 保证有一个ch大小的对象可用

 假设有cpu核数那么多并发任务,可以保证gc的时候有保底在

 

2.局部cache

sync.pool毕竟加锁,要本地ctx能挂载临时对象集,那肯定比pool效率高

 currAccmulator在for循环之外的一个临时变量

 封装在ctx里面的一个临时变量,跟随ctx整个生命周期销毁

 storage存储,后续还能复用

slice复用

1.len与cap

复制代码
func TestD(t *testing.T) {
    ints := make([]int, 0, 6)
    ints = append(ints, 6, 6, 6, 6, 6, 6)
    // The clear built-in function clears maps and slices.
    // For maps, clear deletes all entries, resulting in an empty map.
    // For slices, clear sets all elements up to the length of the slice
    clear(ints)
    logger.DEBUG("ints", ints, " cap: ", cap(ints), " len:", len(ints))
    ints = ints[:0] // len == 0, cap == 6 之前的元素还在
    logger.DEBUG("ints", ints, " cap: ", cap(ints), " len:", len(ints))
    ints = append(ints[:0], 1, 2, 3) // 这样就覆盖了原来的元素
    logger.DEBUG("ints", ints, " cap: ", cap(ints), " len:", len(ints))

    // recap
    additionalItems := 10
    intsLen := len(ints)
    if n := intsLen + additionalItems - cap(ints); n > 0 {
        ints = append(ints[:cap(ints)], make([]int, n)...)
    }
    ints = ints[:intsLen]
    // resize
    size := 10
    if cap(ints) > size {
        ints = ints[:size]
    } else {
        ints = make([]int, size)
    }
}
复制代码
1
2
3
4
5
=== RUN   TestD
2024/06/13 23:24:27 [DEBUG] ints[0 0 0 0 0 0] cap: 6 len:6
2024/06/13 23:24:27 [DEBUG] ints[] cap: 6 len:0
 
2024/06/13 23:24:27 [DEBUG] ints[1 2 3] cap: 6 len:3

 

 slice内部对象复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Tags struct {
    tags  []Tag
    freed bool
}
 
type Tag struct {
    Key   []byte
    Value []byte
}
 
func (t *Tags) AddTag(key, value string) {
    if cap(t.tags) > len(t.tags) {
        t.tags = t.tags[:len(t.tags)+1]
    } else {
        t.tags = append(t.tags, Tag{})
    }
    tag := &t.tags[len(t.tags)-1]
    tag.Key = append(tag.Key[:0], key...)
    tag.Value = append(tag.Value[:0], value...)
}

 

假设这样一个场景,原始数据是

由于string是定长,没办法复用,只能由byte数组转化而来。所以变成这样:

 Row里面持有的内存其实是tagsPool、fieldsPool里面的。网络协议先append到stringsPool里,假设读到的是一个"hello",stringsPool一个长度为5的切片,再unsafe转换到tag上

openGemini/lib/util/lifted/vm/protoparser/influx/parser.go at v1.2.0 · openGemini/openGemini (github.com)

2.string与bytes

 避免string到byte数组额外的拷贝

跨类型复用

1.unsafe

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
func ToUnsafeSlice[T any](b []byte) (ts []T) {
    if len(b) == 0 {
        return nil
    }
    elemSize := unsafe.Sizeof(*new(T))
    if len(b)%int(elemSize) != 0 {
        panic("ToUnsafeSlice: len(b) is not a multiple of elemSize")
    }
    bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    th := (*reflect.SliceHeader)(unsafe.Pointer(&ts))
    th.Data, th.Len, th.Cap = bh.Data, bh.Len/int(elemSize), bh.Cap/int(elemSize)
    return ts
}
 
func ToUnsafeBytes[T any](ts []T) (b []byte) {
    if len(ts) == 0 {
        return nil
    }
    elemSize := unsafe.Sizeof(*new(T))
    bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    th := (*reflect.SliceHeader)(unsafe.Pointer(&ts))
    bh.Data, bh.Len, bh.Cap = th.Data, th.Len*int(elemSize), th.Cap*int(elemSize)
    return b
}
 
func MarshalInt64s(dst []byte, is []int64) []byte {
    dst = encoding.MarshalUint32(dst, uint32(len(is)))
    dst = append(dst, ToUnsafeBytes(is)...)
    return dst
}
 
func UnsafeUnmarshalInt64s(src []byte) ([]int64, []byte, error) {
    if len(src) < 4 {
        return nil, src, errors.New("UnmarshalInt64s: src too short")
    }
    n := encoding.UnmarshalUint32(src)
    src = src[4:]
    if len(src) < int(n)*8 {
        return nil, src, errors.New("UnmarshalInt64s: src too short")
    }
    is := ToUnsafeSlice[int64](src[:n*8])
    return is, src[n*8:], nil
}

2.arena

any interface也是指针

生命周期管理

  • 改造代码结构,让数据在其中单向流动
  • 确定数据生命周期的起点和终结点
  • 中间环节确定自己是借用关系还是拥有关系

有起点和终点,中间对这块数据不会新增新的内存去对主数据做变更

所有的数据在influx.Parse里产生,rows是influx.Parse这个函数里的成果。但如果还涉及改动里面的数据,比如map,那么只能做深拷贝。当你复用内存的时候需要关注内存的生命周期

r.output.Consume是不持有table的,如果要对table进行改动,那么需要重新拷贝一个对象来操作

 

当一个函数返回一个新的内存的时候,需要思考这个内存到底哪来的

上面的gzip函数把本该放回pool对象的g返回出去了,这样导致了内存所有权问题

但下面的函数的target其实是对content的拷贝,又产生了一份新的内存。讲道理gzip可以再把这部分内存当作参数传进来

posted @   王鹏鑫  阅读(35)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示