有关于内存对齐的解答:内存对齐
字符串-切片转换:sliceHeader/StringHeader
1. 结构体
1.1 slice
看到slice的结构体,是不是感觉没啥,因为底层还是引用了数组。
| 字段 | 类型 | 含义 |
|---|---|---|
| array | unsafe.Pointer | 数组指针 |
| len | int | 长度 |
| cap | int | 容量 |
1.2 notInHeapSlice
notInHeapSlice是一个由go:notinheap内存支持的slice。(直译,暂时不知道干啥)
| 字段 | 类型 | 含义 |
|---|---|---|
| array | *notInHeap | 数组指针 |
| len | int | 长度 |
| cap | int | 容量 |
2.实现细节
2.1 扩容细节
在扩容中,当达到参考值按照:,超过参考值则 ,其中为什么引入了1.25倍的参考值,而不是直接 递增,这里面的方案其实是要这两个阶段的数据平缓过度。
和 数据模拟如下:
按照这个方案实现,在期望容量在160~400期间,随着期望容量增大,分配到的实际容量变少了?对没错就是变少了,是显得有点突兀了。
我们再看下 和 数据模拟:
在1.25倍增速中,引入一个参考值的倍数后,你会发现随着期望容量增大,其分配的实际容量处于平缓过渡。
3.公共函数
3.1 乘积的溢出判断
这段代码还是挺妙的,先补充一些前置知识:
- goarch.PtrSize ==> 4 是32位系统, 8 是 64位系统
- 1 << (4*goarch.PtrSize): 针对不同系统,取半操作
判断乘积有没有溢出,作者的处理先判断 a/b有没有超过阈值的一半,没超的话,他们的结果也一定没有超过阈值。(这个小技巧可以直接避免除法的运算),当然即使操作,也是少量的,这时候再通过除法判断就行了。
这种有没有想象到,这就是一个漏斗,先把大范围进行过滤,再将小范围的进行拦截。
/*
:params a: 乘数a
:params b: 乘数b
*/
func MulUintptr(a, b uintptr) (uintptr, bool) {
if a|b < 1<<(4*goarch.PtrSize) || a == 0 {
return a * b, false
}
overflow := b > MaxUintptr/a
return a * b, overflow
}
3.2 分配内存的映射(待补充)
返回mallocgc在请求大小时将分配的内存块的大小
func roundupsize(size uintptr) uintptr {
if size < _MaxSmallSize {
if size <= smallSizeMax-8 {
return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])
} else {
return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])
}
}
if size+_PageSize < size {
return size
}
return alignUp(size, _PageSize)
}
3.3 倍数
将n舍入为a的倍数。a必须是2的幂,看这个实现就觉得很绕了
func alignUp(n, a uintptr) uintptr {
return (n + a - 1) &^ (a - 1)
}
4.创建
/*
:params et: 元素类型(包含元素大小,类型等信息)
:params len: 长度
:params cap: 容量
*/
func makeslice(et *_type, len, cap int) unsafe.Pointer {
// 判断乘积是否溢出
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
// 申请内存过大,或者长度小于0,长度大于容量,异常提示
if overflow || mem > maxAlloc || len < 0 || len > cap {
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
// 内存分配
return mallocgc(mem, et, true)
}
5.扩容
5.1 校验
主要是做一些常规的校验
/*
:params et: 元素类型(包含元素大小,类型等信息)
:params old: 旧的切片
:params cap: 容量
*/
func growslice(et *_type, old slice, cap int) slice {
if raceenabled {
callerpc := getcallerpc()
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, abi.FuncPCABIInternal(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
}
if asanenabled {
asanread(old.array, uintptr(old.len*int(et.size)))
}
// 新容量必须大于旧容量
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
if et.size == 0 {
// Append不应该创建一个指针为nil的切片,而是一个len为非零的切片。
// 我们假设append在这种情况下不需要保存旧数组。
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
...
}
5.2 扩容策略
这里应该就是最原汁原味的扩容策略了
- 直接分配期望容量:,(你大我听你的,你小就不好意思了,听我的)
- 设置参考值 256,旧容量小于参考值,新容量直接翻倍。
- 其他的就按照 的速率递增,直到大于期望容量。
/*
:params et: 元素类型(包含元素大小,类型等信息)
:params old: 旧切片
:params cap: 期望分配的容量
*/
func growslice(et *_type, old slice, cap int) slice {
newcap := old.cap
doublecap := newcap + newcap
// 直接使用当前的期望容量,因为超过了旧容量的2倍
if cap > doublecap {
newcap = cap
} else {
// 旧容量 小于 256,则新容量 = 旧容量翻倍
const threshold = 256
if old.cap < threshold {
// 小切片 容量翻倍
newcap = doublecap
} else {
// 检查0<newcap以检测溢出并防止无限循环。
for 0 < newcap && newcap < cap {
// 从小切片的增长2倍过渡到大切片的增长1.25倍。这个公式给出了两者之间的平滑过渡。
newcap += (newcap + 3*threshold) / 4
}
// 当newcap计算溢出时,将newcap设置为请求的上限。
if newcap <= 0 {
newcap = cap
}
}
}
5.3 计算实际内存
针对预分配的数据,需要乘上元素大小,还需要进行内存对齐,才能真正被使用。其中不同的元素大小(et.size),采用的形式也不同。
- 1,我们不需要任何除法/乘法
- goarch.PtrSize,编译器会将除法/乘法优化为移位一个常数
- 2的幂,使用可变移位。
func growslice(et *_type, old slice, cap int) slice {
...
var overflow bool
var lenmem, newlenmem, capmem uintptr
switch {
case et.size == 1:
// 计算元素总的长度和容量
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
// 根据请求大小将分配的内存块大小
capmem = roundupsize(uintptr(newcap))
// 检查有没有超出阈值
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == goarch.PtrSize:
// 元素大小是系统大小(位)
lenmem = uintptr(old.len) * goarch.PtrSize
newlenmem = uintptr(cap) * goarch.PtrSize
capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
newcap = int(capmem / goarch.PtrSize)
case isPowerOfTwo(et.size):
// 2的幂次方,符合位运算
var shift uintptr
// 64位/32位系统得到的掩码
if goarch.PtrSize == 8 {
// 掩码移位以更好地生成代码。
shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
}
lenmem = uintptr(old.len) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
default:
// 其他的按照正常方式处理
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
}
...
}
5.4 内存分配
除了 capmem>maxAlloc之外,还需要检查溢出,防止可用于在32位架构上触发 segfault的溢出,使用以下实例程序:
package main
type T [1<<27 + 1]int64
var d T
var s []T
func main() {
s = append(s, d, d, d, d)
print(len(s), "\n")
}
内存分配(尚未理解)
func growslice(et *_type, old slice, cap int) slice {
...
if overflow || capmem > maxAlloc {
panic(errorString("growslice: cap out of range"))
}
// 扩容后新的切片数组起始指针地址
var p unsafe.Pointer
// 如果元素数据为空,直接获取原内存。
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
// 调用growslice的append(),将old.len覆盖cap(这就是新的长度)。仅清除不会被覆盖的部分。
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// 重新分配新的内存并返回
// 注意:不能使用rawmem(这避免了内存归零),因为这样GC可以扫描未初始化的内存。
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
// 只对 old.array中的指针进行着色,因为我们知道目标切片p只包含nil的指针,因为它在分配期间已被清除。
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
}
}
// 将旧切片中的数据复制到新的数组中,该memmove是由汇编代码写的
memmove(p, old.array, lenmem)
return slice{p, old.len, newcap}
}
6.拷贝(未掌握)
对应的是内置函数(copy)
用于将无指针元素的字符串或切片复制到切片中。
/*
:params toPtr: 目的切片指针(需要复制到的指针)
:params toLen: 目的切片长度
:params fromPtr: 源切片指针
:params width:
*/
func slicecopy(toPtr unsafe.Pointer, toLen int, fromPtr unsafe.Pointer, fromLen int, width uintptr) int {
if fromLen == 0 || toLen == 0 {
return 0
}
n := fromLen
if toLen < n {
n = toLen
}
if width == 0 {
return n
}
size := uintptr(n) * width
if raceenabled {
callerpc := getcallerpc()
pc := abi.FuncPCABIInternal(slicecopy)
racereadrangepc(fromPtr, size, callerpc, pc)
racewriterangepc(toPtr, size, callerpc, pc)
}
if msanenabled {
msanread(fromPtr, size)
msanwrite(toPtr, size)
}
if asanenabled {
asanread(fromPtr, size)
asanwrite(toPtr, size)
}
if size == 1 { // common case worth about 2x to do here
// TODO: is this still worth it with new memmove impl?
*(*byte)(toPtr) = *(*byte)(fromPtr) // 已知为字节指针
} else {
memmove(toPtr, fromPtr, size)
}
return n
}
7.总结
- 切片是对数组的封装,切片可以自动扩容,扩容前后的是否还是原数组,取决于新容量策略
- 已知切片容量或者长度时,声明时最好也指定容量或长度,因为扩容导致重新分配内存需要gc过程。