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

2015-12-09 10:40:22 +08:00
 dbow

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())
}

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

8836 次点击
所在节点    程序员
34 条回复
cloudzhou
2015-12-10 10:58:57 +08:00
@dbow 参考我上面提问,我是想知道这个问题是什么,如果你能提供示例代码就更好了。
dbow
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
2015-12-10 12:07:31 +08:00
@cloudzhou 除了做一些底层的 dirty hack, 针对各种 golang 的内置对象, 你有什么办法用 golang 的 api 接口实现这个吗?
```golang
enter function
malloc -> new
自动 free -> delete
leave function
```
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
2015-12-10 12:37:08 +08:00
@cloudzhou
如果能实现下面这种模式, 问题也是可以解决的, while {malloc, free}, 这样内存耗用就相对较小, 不会 malloc 堆在一起又占内存而且一下 gc 造成巨大的延迟。
```gol
enter function
malloc -> new
自动 free -> delete
leave function
```
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
2015-12-10 12:44:39 +08:00
@tiancaiamao
C 语言指针可以乱跑就没 hack 这回事, 我讲的是 go 里要实现如下模式, 需要 hack
enter function
malloc -> new
自动 free -> delete
leave function
tiancaiamao
2015-12-10 12:50:25 +08:00
又没人阻止你这么做
func f(reuse []byte) {
enter function
use reuse object ,不要 malloc
不需要 free
leave function
}
dbow
2015-12-10 12:55:59 +08:00
@tiancaiamao
问题是 f 多数情况下不是自己写的, 所以要求 gc 能自己搞定这个事。
yernsun
2015-12-10 14:14:49 +08:00
可能我比较 low ,申请资源的既然能进行 free -> delete ,那么为什么就不能复用?
xufang
2015-12-10 23:02:34 +08:00
唉,我在 5# 贴得链接无人问津,明珠暗投啊。
ryd994
2015-12-11 05:37:49 +08:00
@tiancaiamao 题外话:
struct T {
struct ListNode node; // 从 node 的指针“反射”取出 T 对象
}
这是什么情况用?
CRVV
2015-12-11 09:04:39 +08:00
@dbow
Go 根本没有 delete 这个功能,如何 delete ?
或者说本来就是 gc 在自动 delete
在任何语言里,频繁地 new 和 delete 都不好,只不过用 Go 这么做显得更不好。
在任何语言里,都应该用一个池
所以,上面早就有人说过了, “官方都快有 3 种标准写法了”...
dbow
2015-12-11 10:04:17 +08:00
@CRVV 我讲如果能处理
enter function
new slice
leave function
问题就小很多
在 while 里引用了别人写的这种函数就是 for {malloc}, 给 gc 集中式回收造成了很大的压力
如果能实现把 function 内显而易见的 heap 分配可以立刻回收掉, 这种问题就能解决掉, 内存占用不会升的太快, gc 造成的延迟也会减小。
这事不在内存池上, 因为有了 gc , 别人的代码会"不负责任的"这么干.

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/242195

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX