• 首页
  • Github
手工分配和回收 []byte

go 虽然支持自动管理内存,但如果可以确定对象的生命期,使用手工管理,可以减少一些 GC 的压力。 本文探讨手工管理 []byte 的各个方面。

free list

首先需要一个 free list。可以自己实现,也可以用标准库的 sync.Pool。 sync.Pool 在 1.13 版本有一个改进,不会在每次 GC 时都清空了,所以是可以用的。

有一个提案对实现这类结构有益:Proposal: percpu.Sharded, an API for reducing cache contention, 不过还没有实现。简洁起见,本文就用 sync.Pool。

支持多个大小

一个 sync.Pool 应当只支持特定大小的 []byte,所以应该有多个 sync.Pool。 []byte 的大小可以分成多级,每一级对应一个 sync.Pool。

代码如下:

var bytesClasses, bytesPools = func() (
	classes []int,
	pools []sync.Pool,
) {
	for i := 7; i <= 16; i++ {
		size := 1 << i
		classes = append(classes, size)
		pools = append(pools, sync.Pool{
			New: func() interface{} {
				return make([]byte, size)
			},
		})
	}
	return
}()

上面代码,初始化了大小从 128 到 65536 的多个 sync.Pool。 对于大于 64K 的,直接分配而不做管理。大小的上下界可以根据具体场景做调整。

分配

分配就是根据传入的大小,找到对应级别的 sync.Pool,用 Get 方法获得 []byte,将切片长度改成传入的大小,返回即可。

返回的是一个 Bytes 对象,使用时,就用它的 Bytes 字段。

type Bytes struct {
	Bytes []byte
	class int
}

func getBytes(size int) Bytes {
	class := 0
	for size > bytesClasses[class] {
		class++
		if class == len(bytesClasses) {
			break
		}
	}
	if class == len(bytesClasses) {
		return Bytes{
			Bytes: make([]byte, size),
			class: -1,
		}
	}
	return Bytes{
		Bytes: bytesPools[class].Get().([]byte)[:size],
		class: class,
	}
}

如果大小参数超出最大值,则返回一个用 make 分配的对象。 这是用于兜底,保证正确性的。如果实际场景里触发了这个,那应该调整分配的最大值。

回收

回收就是将 []byte 放回 sync.Pool。

如果级别是 -1,表示这是 make 分配的,不需要管理。

func (b Bytes) Put() {
	if b.class == -1 {
		return
	}
	bytesPools[b.class].Put(b.Bytes)
}

正确性保证

手工管理对象的难点在于确定对象的生命期。 如果回收过早,或者回收后扔有指针指向内部,就可能引发竞态。 不像 rust,go 是没有语言机制保证这些方面的安全的。 所以使用的时候要小心。

运行时提供的竞态检测器,对保证正确性有帮助。如果对象被错误回收,极有可能触发竞态告警。

可能静态代码分析,也可以实现类似 rust 的生命期分析。 如果分析器不能推断一个手工分配的对象的使用是正确的,就报错,要求改动代码直到分析器可以推断。 我不太熟悉这个方面,感觉是个值得研究的方向。

逃逸分析就类似生命期分析,编译器不能保证对象的生命期不大于栈帧的生命期,就只能分配到堆上。 但对于堆上的对象的生命期,逃逸分析是不关心的。 感觉这方面有优化的空间,如果某些堆对象也可以通过静态分析确定生命期,那 GC 的时候,是可以跳过的。

可以参考或者使用的:

  • https://godoc.org/golang.org/x/tools/go/pointer
  • https://godoc.org/golang.org/x/tools/go/ssa
2019-08-07
comments powered by Disqus