V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
dbow
V2EX  ›  程序员

golang 经验交流, 如何避免 gc 杀手

  •  
  •   dbow ·
    maliubiao · 2015-12-09 10:40:22 +08:00 · 8833 次点击
    这是一个创建于 3274 天前的主题,其中的信息可能已经有所发展或是发生改变。

    golang 的 gc 从实践来看弱爆了, while { readline, } 处理大文本很快内存消耗完, 大量 cpu 时间消耗到 gc 上。 下面两个工具可以缓解这种问题, 一是一块 buffer 上用多个 slice 连续存储, 另一个是自动缩放的一块 buffer 存变长的值。

    type BytesPool struct {
      Buffer []byte
      Index int
      Top int
    }
    
    //size, initial pool size
    func NewBytesPool(size int) *BytesPool {
      return &BytesPool{make([]byte, size), 0, 0}
    }
    
    //get slice
    func (b *BytesPool) AllocSlice(size int) []byte {
      //expand 1.5x
      if len(b.Buffer) < size + b.Index {
        nb := make([]byte, int(float64(len(b.Buffer)) * 1.5) + size)
        copy(nb,  b.Buffer)
        b.Buffer = nb
      }
      slice := b.Buffer[b.Index:b.Index+size]
      b.Top = b.Index
      b.Index += size
      return slice
    }
    
    func (b *BytesPool) GetSlice() []byte {
      return b.Buffer[b.Top:b.Index]
    }
    
    
    type AutoResizeByteArray struct {
      Buffer []byte
      Top int
    }
    
    func NewAutoResizeByteArray(size int) *AutoResizeByteArray {
      return &AutoResizeByteArray{make([]byte, size), 0}
    }
    
    
    func (b *AutoResizeByteArray) AllocSlice(size int) []byte {
      if len(b.Buffer) < size {
        nb := make([]byte, int(float64(len(b.Buffer)) * 1.5) + size)
        copy(nb,  b.Buffer)
        b.Buffer = nb
      }
      b.Top = size
      return b.Buffer[:size]
    }
    
    func (b *AutoResizeByteArray) GetSlice() []byte {
      return b.Buffer[:b.Top]
    }
    
    
    func testBytesPool() {
      b := NewBytesPool(3)
      copy(b.AllocSlice(4), []byte("abcd"))
      fmt.Printf("%s\n", b.GetSlice())
      copy(b.AllocSlice(2), []byte("ef"))
      fmt.Printf("%s\n", b.GetSlice())
    
      b1 := NewAutoResizeByteArray(3)
      copy(b1.AllocSlice(4), []byte("abcd"))
      fmt.Printf("%s\n", b1.GetSlice())
    }
    

    大家还有什么好注意? 欢迎交流。

    第 1 条附言  ·  2015-12-09 22:39:29 +08:00
    发贴目标不是要跟你们喷的, 拒不参与语言圣战, 就此终止。
    34 条回复    2015-12-11 10:04:17 +08:00
    pathletboy
        1
    pathletboy  
       2015-12-09 11:40:30 +08:00
    golang 标准库 1.4 起自带内存池
    https://golang.org/pkg/sync/#Pool
    tiancaiamao
        2
    tiancaiamao  
       2015-12-09 12:10:04 +08:00
    你为什么不用 bytes.Buffer 要自己写呢?标准库里面的 http://godoc.org/bytes

    不是 golang 的 gc 弱爆了,是你对 golang 的了解弱爆了
    mengzhuo
        3
    mengzhuo  
       2015-12-09 12:23:56 +08:00
    sync.Pool
    bytes.Buffer
    <- chan []byte (LeakyBuffer)

    官方都快有 3 种标准写法了, LZ 你弱爆了
    Comdex
        4
    Comdex  
       2015-12-09 12:39:57 +08:00
    看了楼上的回复我感觉自己弱爆了。。。
    xufang
        5
    xufang  
       2015-12-09 12:42:01 +08:00
    helloworldwt
        6
    helloworldwt  
       2015-12-09 13:13:44 +08:00
    nice
    miao1007
        7
    miao1007  
       2015-12-09 13:16:52 +08:00
    golang 是个大坑啊,比 shell 还难写
    mengzhuo
        8
    mengzhuo  
       2015-12-09 13:22:45 +08:00
    @miao1007
    这两都不是一类
    说难写的看看 Erlang CommonLisp
    yuankui
        9
    yuankui  
       2015-12-09 14:47:16 +08:00
    嗯,是实践,而不是最佳实践~
    amaranthf
        10
    amaranthf  
       2015-12-09 14:59:28 +08:00
    @mengzhuo lisp 大部分时候写起来还是蛮舒服的,除了设计宏的时候
    gamexg
        11
    gamexg  
       2015-12-09 15:37:57 +08:00
    erlang 啃了一段时间,最后实在习惯不了那种思路,放弃。
    dbow
        12
    dbow  
    OP
       2015-12-09 17:26:40 +08:00
    @pathletboy
    @mengzhuo
    @tiancaiamao
    我其实是想讨论下怎么把内存释放掉, 内存池只是避免通过 gc 的方案
    c 里有 malloc , free 成对, 成对使用就可以保持内存消耗相对较小
    golang 里就不一样, 一路 malloc, 什么时候 free 是不确定事件, 最后内存使用越来越大, 不可控制。
    pathletboy
        13
    pathletboy  
       2015-12-09 17:29:40 +08:00
    @dbow 关键你本来就不该频繁 malloc free 啊,就算你 c ,这么频繁 malloc free ,增加内存碎片,影响性能,最终还是要靠内存池来解决。
    pathletboy
        14
    pathletboy  
       2015-12-09 17:35:34 +08:00
    @dbow 如果你非得坚持错误的做法,那你可以手工 gc , runtime.GC(),不推荐!不推荐!不推荐!
    dbow
        15
    dbow  
    OP
       2015-12-09 17:50:52 +08:00
    @pathletboy
    性能不是关键问题, 随着 objects 越来越多, 进程内存越占越多, gc 的回收效果不好才是个大麻烦。
    dbow
        16
    dbow  
    OP
       2015-12-09 17:58:07 +08:00
    @dbow
    while {readline} 不建内存池这种模式下, 10 亿行文本, 吃内存嗖嗖的, 慢点是没有关系的.
    cloudzhou
        17
    cloudzhou  
       2015-12-09 19:18:36 +08:00
    -- while {readline} 不建内存池这种模式下, 10 亿行文本, 吃内存嗖嗖的, 慢点是没有关系的.

    我并不理解你这个需求,如果你能提供更多代码就更好了。因为在 while 里面,最终只是复用一个 slice 而已(当然在超过当前长度的时候会申请新的内存空间), GC 应该是可控的。
    // 使用 *bufio.Reader ReadLine

    上面提到的几种方案, sync.Pool , bytes.Buffer ,有什么理由不能用吗?
    mengzhuo
        18
    mengzhuo  
       2015-12-09 21:41:02 +08:00
    @dbow
    本来 GC 就是为了减轻心智负担的,非得自己整,不是作死是啥,还怪语言?
    觉得不“可控”,你去写 C 好了,多进程下检测泄露、死锁?
    连 cache tmpfile 都不会用,还 10 亿行文本,呵呵。
    xufang
        19
    xufang  
       2015-12-09 21:42:04 +08:00
    @mengzhuo 和小学生解释这么多干嘛。。。
    dbow
        20
    dbow  
    OP
       2015-12-09 22:17:55 +08:00
    停喷, 结贴。
    cloudzhou
        21
    cloudzhou  
       2015-12-10 10:58:57 +08:00
    @dbow 参考我上面提问,我是想知道这个问题是什么,如果你能提供示例代码就更好了。
    dbow
        22
    dbow  
    OP
       2015-12-10 11:56:47 +08:00
    @cloudzhou
    while { readline, 每次都分配内存而且不归我管的函数}, 我讲的是 gc 对这种情况反应不行,
    也就是下面的情况做不到.
    enter function
    malloc -> new
    自动 free -> delete
    leave function
    ```
    ```go
    package main

    func main() {
    b := make([][]byte, 3000000)
    for i := 0; i < 3000000; i++ {
    buffer := make([]byte, 1024)
    copy(buffer, []byte("abcd"))
    b[i] = buffer
    }
    }
    ```
    ```shell
    gc1(1): 1+0+33224+1 us, 0 -> 68 MB, 21 (21-0) objects, 2 goroutines, 16/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
    gc2(1): 0+2+29460+1 us, 68 -> 68 MB, 22 (23-1) objects, 3 goroutines, 16/0/2 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
    gc3(1): 0+8+29552+0 us, 68 -> 137 MB, 104710 (139608-34898) objects, 3 goroutines, 8808/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
    gc4(1): 0+881+29664+1 us, 136 -> 136 MB, 69814 (139608-69794) objects, 3 goroutines, 8808/0/8803 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
    gc5(1): 0+10+30403+0 us, 136 -> 273 MB, 278374 (417688-139314) objects, 3 goroutines, 26255/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
    gc6(1): 0+2650+30961+0 us, 272 -> 272 MB, 208854 (417688-208834) objects, 3 goroutines, 26255/0/26250 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
    gc7(1): 0+15+31651+0 us, 272 -> 545 MB, 624366 (971704-347338) objects, 3 goroutines, 61016/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
    gc8(1): 0+5795+32788+0 us, 543 -> 543 MB, 485862 (971704-485842) objects, 3 goroutines, 61016/0/61011 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
    gc9(1): 0+23+33831+0 us, 543 -> 1086 MB, 1313658 (2075432-761774) objects, 3 goroutines, 130267/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
    gc10(1): 0+12912+35999+0 us, 1082 -> 1082 MB, 1037726 (2075432-1037706) objects, 3 goroutines, 130267/0/130262 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
    gc11(1): 1+39+39331+1 us, 1082 -> 2164 MB, 2686898 (4274328-1587430) objects, 3 goroutines, 268233/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
    gc12(1): 0+28420+41711+0 us, 2155 -> 2155 MB, 2137174 (4274328-2137154) objects, 3 goroutines, 268233/0/268228 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
    ```
    dbow
        23
    dbow  
    OP
       2015-12-10 12:07:31 +08:00
    @cloudzhou 除了做一些底层的 dirty hack, 针对各种 golang 的内置对象, 你有什么办法用 golang 的 api 接口实现这个吗?
    ```golang
    enter function
    malloc -> new
    自动 free -> delete
    leave function
    ```
    cloudzhou
        24
    cloudzhou  
       2015-12-10 12:28:50 +08:00
    ```go
    package main

    func main() {
    b := make([][]byte, 3000000)
    for i := 0; i < 3000000; i++ {
    buffer := make([]byte, 1024)
    copy(buffer, []byte("abcd"))
    b[i] = buffer
    }
    }
    ```

    你这个例子来说几乎是无解的,因为无论如何变量会被引用到,所以 GC 本身不会回收的,哪怕使用 sync.Pool ,这种情况下和 GC 关系不大,哪怕你使用 Java ,一样遇到这个问题。

    ** 所以你的问题是,如何能够更加“紧凑”的使用内存,避免内存碎片。**

    按照你的这个例子,那么就是大量的循环里面创造 slice ,但是 slice 很大,而实际存储内容比较小。
    “看起来内存很浪费”

    解决方法来看:
    1 如果使用自己开发的内存池,在大量动态变化情况下,实际上,你就是在实现一个小型的 GC 了。并且不会比使用 sync.Pool 好多少的。
    2 借鉴 memcache 的解决方法,申请大的内存块,然后按照长度切片,比如 128b, 256b, 512b, 1k, 2k ,然后根据实际数据做一些 copy 工作。
    3 sync.Pool 和 按照长度分片的 buffer 结合起来,基本能实现你的需求了。

    节省内存和避免 COPY 是一个矛盾的问题,内存越紧凑,当长度变化时,需要申请新的空间, COPY 数据,反之就是内存越浪费,这是一个权衡的问题。
    dbow
        25
    dbow  
    OP
       2015-12-10 12:37:08 +08:00
    @cloudzhou
    如果能实现下面这种模式, 问题也是可以解决的, while {malloc, free}, 这样内存耗用就相对较小, 不会 malloc 堆在一起又占内存而且一下 gc 造成巨大的延迟。
    ```gol
    enter function
    malloc -> new
    自动 free -> delete
    leave function
    ```
    tiancaiamao
        26
    tiancaiamao  
       2015-12-10 12:38:34 +08:00
    @dbow
    什么叫 dirty....自己去操心资源的分配和释放这些事情就很 dirty
    什么叫 hack...
    比如 c 语言经常这么写:
    void f(int *multiret1, int *multiret1); // 多值返回
    void f(struct T* bufFromCallerStack); // 在调用者的栈空间中分配
    struct T {
    struct ListNode node; // 从 node 的指针“反射”取出 T 对象
    }
    ------不熟习 c 的人说,这叫 hack--------
    ------熟习 c 的人说,这是常识啊-----

    对象池,复用缓存空间,这些对于熟习 Go 的人说是常识,不是 hack
    dbow
        27
    dbow  
    OP
       2015-12-10 12:44:39 +08:00
    @tiancaiamao
    C 语言指针可以乱跑就没 hack 这回事, 我讲的是 go 里要实现如下模式, 需要 hack
    enter function
    malloc -> new
    自动 free -> delete
    leave function
    tiancaiamao
        28
    tiancaiamao  
       2015-12-10 12:50:25 +08:00
    又没人阻止你这么做
    func f(reuse []byte) {
    enter function
    use reuse object ,不要 malloc
    不需要 free
    leave function
    }
    dbow
        29
    dbow  
    OP
       2015-12-10 12:55:59 +08:00
    @tiancaiamao
    问题是 f 多数情况下不是自己写的, 所以要求 gc 能自己搞定这个事。
    yernsun
        30
    yernsun  
       2015-12-10 14:14:49 +08:00
    可能我比较 low ,申请资源的既然能进行 free -> delete ,那么为什么就不能复用?
    xufang
        31
    xufang  
       2015-12-10 23:02:34 +08:00
    唉,我在 5# 贴得链接无人问津,明珠暗投啊。
    ryd994
        32
    ryd994  
       2015-12-11 05:37:49 +08:00
    @tiancaiamao 题外话:
    struct T {
    struct ListNode node; // 从 node 的指针“反射”取出 T 对象
    }
    这是什么情况用?
    CRVV
        33
    CRVV  
       2015-12-11 09:04:39 +08:00
    @dbow
    Go 根本没有 delete 这个功能,如何 delete ?
    或者说本来就是 gc 在自动 delete
    在任何语言里,频繁地 new 和 delete 都不好,只不过用 Go 这么做显得更不好。
    在任何语言里,都应该用一个池
    所以,上面早就有人说过了, “官方都快有 3 种标准写法了”...
    dbow
        34
    dbow  
    OP
       2015-12-11 10:04:17 +08:00
    @CRVV 我讲如果能处理
    enter function
    new slice
    leave function
    问题就小很多
    在 while 里引用了别人写的这种函数就是 for {malloc}, 给 gc 集中式回收造成了很大的压力
    如果能实现把 function 内显而易见的 heap 分配可以立刻回收掉, 这种问题就能解决掉, 内存占用不会升的太快, gc 造成的延迟也会减小。
    这事不在内存池上, 因为有了 gc , 别人的代码会"不负责任的"这么干.
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3338 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 38ms · UTC 12:27 · PVG 20:27 · LAX 04:27 · JFK 07:27
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.