Go源码分析: Slice内存增长方式
源文件: runtime/slice.go
slice 是一个连续的内存结构, 由3部分组成: 内存区域, 长度 和 可用长度(cap):
type slice struct {
array unsafe.Pointer
len int
cap int
}
make
创建一个slice, 实际上有3个参数: make([]type, len, cap)
当省略参数cap时,默认cap=len
cap决定了实际分配内存区域的大小, 在进行 append/copy等操作时, 如果长度没有超过cap, 则不需要重新分配内存, 也不需要在内存中移动数据
make slice原型: func makeslice
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
append
当调用 append 向 slice 添加元素时, 会发生什么?
在所需新的cap低于1024时, 每次slice达到极限, 需要扩充时, 都会乘以原来长度的两倍.
当超过1024时, 每次都会增长原长度的 1/4, 直至新的长度 > 预期长度 为止.
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
验证扩充长度变化:
func TestAppend(t *testing.T) {
var s []int
t.Log(cap(s))
for i := 0; i < 1025; i++ {
s = append(s, i)
t.Log(cap(s))
}
}
此外, 每次扩张都会需要内存迁移 memmove 进行迁移.
因此, 当slice发生扩容时, 实际包含了:
- 计算新的长度
- 分配内存
- memmove
在进行 memmove 时, 不会对原内存区域进行清零. 因此, 假如原 slice 包含敏感信息, 此时对内存进行dump, 是可以找到残留信息的 (密码学实现需要关注这一点).
另一个问题, 分配内存时是否一定会通过系统调用分配内存? 需要在单独研究中分析.
启示
从以上分析中, 我们知道 growslice 是一个代价较高的过程. 如何避免或者减少这种影响呢?
答案就是在分配时尽可能预设合适的cap. 如无必要, 避免在不预分配cap的情况下不断append.
append slice
以下代码, 源slice一经确定, 但 s 最终 cap为16, 浪费了空间, 且发生了多次调整: 0-> 1 -> 2 -> 4 -> 8 -> 16
func TestAppend(t *testing.T) {
src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var s []int
t.Log(cap(s))
for _, v := range src {
s = append(s, v)
t.Log(cap(s))
}
}
Buffer 缓存
bytes.Buffer 一般认为比直接将字符串相加效率更高, 但其存储使用的 byte slice, 也需要同样的扩容过程. 因此较好的策略是, 初始化时给它一个适度大小的slice作为起始内存区域.
// 在某段函数中, 尝试计算和预测所需buf长度
totalLen += len(l)*4 + 1
buf := make([]byte, totalLen)
b := bytes.NewBuffer(buf)