map解析: juejin.cn/post/695470…
介绍
基本概念
Golang中的Slice(切片)是动态数组,支持追加、随机访问、遍历、拷贝等操作。Slice在元素数量增长时可以自动扩容,也允许对通过对已有切片进行截取获得一个更小的切片。
基本操作
// 1.初始化 []T
a := []int{0, 1, 2, 3} // 1.1 字面量初始化
a := []int{2: 2, 3, 0: 0, 1}
a := make([]int, 4) // 1.2 make初始化,参数为len
a := make([]int, 0, 4) // 参数为(len, cap)
a = b[1:2] // 1.3切割已有切片(/数组)初始化 参数为(lower_bound, upper_bound)
a = b[1:2:2] // (lower_bound, upper_bound, cap_bound)
// 2.访问
sum := a[0] + a[2]
l, c := len(a), cap(a)
// 3.遍历
for range a {}
for index := range a {} // index
for index, value := range a {} // (index, value)
for i := 0; i < len(a); i ++ {a[i]=0}
// 4.追加
a = append(a, 4)
// 5:拷贝
copy(dst, src)
复制代码
与数组的关联
Slice对元素的操作基于数组。在「内存模型」部分将会看到,切片的元素是存储在一块连续的内存中,这块内存便是slice的底层数组。
源码解析
内存模型
编译期间
在编译期间,slice的类型仅包含元素的类型字段。而array会额外包含数组的界(元素个数)
//代码地址
type Array struct {
Elem *Type // element type
Bound int64 // number of elements; <0 if unknown yet
}
type Slice struct {
Elem *Type // element type
}
复制代码
运行时
slice类型为reflect.SliceHeader,包含指向底层数组的指针Data,切片长度Len与切片容量Cap。实际上Cap也是底层数组的长度。
//代码地址
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.(这里的data并不能保证底层数组不被gc,所以程序必须维护另一份底层数据的正确类型指针)
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
复制代码
e.g.
a := make([]int, 3, 5)
a[0], a[1], a[2] = 0, 1, 2
复制代码
初始化
Slice的初始化可以分为三类:字面量初始化、关键字初始化(make)与截取初始化。
字面量
字面量初始化即用类似 []T {…} 的语句得到一个初始化的切片。我们使用 a := []int{1, 2, 3, 4} 与 b := []int{2: 3, 4, 0: 1, 2} 来生成对应的汇编指令观察发生了什么。
根据指令片段,可以发现字面量初始化时发生了如下的操作:
- 创建一个长度为4的int数组并为其分配内存
LEAQ type.[4]int(SB), AX
CALL runtime.newobject(SB)
复制代码
- 将元素值依次放入数组中
- 将数组、长度(4)与容量(4)赋值给变量的对应字段
MOVQ AX, "".a+120(SP)
MOVQ $4, "".a+128(SP)
MOVQ $4, "".a+136(SP)
复制代码
b的初始化也是同样的,只不过元素赋值的顺序有改变,按照下标为2-3-0-1的顺序赋值。未指定下标的元素使用的下标,是前一个元素的下标+1(如果没有为0),而当元素下标有冲突时会在编译时报错。
// 大概是如何确定字面量初始化时的下标...
func (index *int, lastIndex *int) int {
if index != nil {
return *index
} else if lastIndex != nil {
return *lastIndex + 1
} else {
return 0
}
}
复制代码
关键字
不同于字面量初始化中很多工作都在编译时完成,make初始化很多内容都需要运行时去完成。
make需要指定切片的长度,可选指定切片的容量,入参的校验工作在类型检查期间完成。类型检查包括对len和cap的类型、数值检查(常数时)与cap是否大于等于len的判断。
// 代码地址
func typecheck1(n *Node, top int) (res *Node) {
...
switch n.Op {
...
case OMAKE:
args := n.List.Slice()
i := 1
switch t.Etype {
case TSLICE:
if i >= len(args) { // 检查参数len是否存在
yyerror("missing len argument to make(%v)", t)
n.Type = nil
return n
}
l = args[i]
i++
l = typecheck(l, ctxExpr) // 检查len参数对应节点的类型
var r *Node
if i < len(args) { // 如果参数超过2个则是cap参数
r = args[i]
i++
r = typecheck(r, ctxExpr) // 检查cap参数对应节点的类型
}
if l.Type == nil || (r != nil && r.Type == nil) {
n.Type = nil
return n
}
// 检查len与cap是常数的情况下,len与cap的值是否在允许的范围内
if !checkmake(t, "len", &l) || r != nil && !checkmake(t, "cap", &r) {
n.Type = nil
return n
}
// 检查len与cap均为常数的情况下,cap是否满足>=len的限制
if Isconst(l, CTINT) && r != nil && Isconst(r, CTINT) && l.Val().U.(*Mpint).Cmp(r.Val().U.(*Mpint)) > 0 {
yyerror("len larger than cap in make(%v)", t)
n.Type = nil
return n
}
n.Left = l
n.Right = r
n.Op = OMAKESLICE
}
...
}
}
复制代码
在校验工作结束后,会将节点的值替换为OMAKESLICE。而在OMAKESLICE时会根据情况使用不同的方式初始化切片。当数组逃逸到堆上或者切片容量非常大时会在堆上初始化,而不满足时会在栈上初始化。这个区别也可以在生成的汇编中发现指令片段。
// 代码地址
func walkexpr(n *Node, init *Nodes) *Node {
...
siwtch n.Op {
...
case OMAKESLICE:
l := n.Left
r := n.Right
if r == nil {
r = safeexpr(l, init)
l = r
}
...
if n.Esc == EscNone { // 未发生逃逸
t = types.NewArray(t.Elem(), i) // 创建一个[r]T的数组var_
var_ := temp(t)
a := nod(OAS, var_, nil) // zero temp
a = typecheck(a, ctxStmt)
init.Append(a)
r := nod(OSLICE, var_, nil)
r.SetSliceBounds(nil, l, nil) // 截取var_的[:l]
r = conv(r, n.Type)
r = typecheck(r, ctxExpr)
r = walkexpr(r, init)
n = r
} else {
// n escapes; set up a call to makeslice.
// When len and cap can fit into int, use makeslice instead of
// makeslice64, which is faster and shorter on 32 bit platforms.
// 发生了逃逸需要调用makeslice初始化,根据len和cap是否在int范围内决定使用makeslice还是makeslice64
len, cap := l, r
fnname := "makeslice64"
argtype := types.Types[TINT64]
// Type checking guarantees that TIDEAL len/cap are positive and fit in an int.
// The case of len or cap overflow when converting TUINT or TUINTPTR to TINT
// will be handled by the negative range checks in makeslice during runtime.
// 接下来的类型检查保证字面量类型的是正数并且可以转换为int范围内;
// len和cap在转换为uint、uintptr、int溢出的情况在makeslice的范围检查中进行
if (len.Type.IsKind(TIDEAL) || maxintval[len.Type.Etype].Cmp(maxintval[TUINT]) <= 0) &&
(cap.Type.IsKind(TIDEAL) || maxintval[cap.Type.Etype].Cmp(maxintval[TUINT]) <= 0) {
fnname = "makeslice"
argtype = types.Types[TINT]
}
m := nod(OSLICEHEADER, nil, nil) // 这里构建的是sliceHeader节点
m.Type = t
fn := syslook(fnname)
// 接下来进行makeslice的调用
m.Left = mkcall1(fn, types.Types[TUNSAFEPTR], init, typename(t.Elem()), conv(len, argtype), conv(cap, argtype))
m.Left.MarkNonNil()
m.List.Set2(conv(len, types.Types[TINT]), conv(cap, types.Types[TINT])) // 节点list中增加len与cap
m = typecheck(m, ctxExpr) // typecheck接下来会调用typecheck1来构建sliceHeader的其他信息
m = walkexpr(m, init)
n = m
}
}
}
复制代码
栈上的初始化是先在栈上初始化一个长度为cap的T类型的数组,而后进行[:len]的截取获取对应的切片。
tmp := [cap]T{}
ret := tmp[:len]
复制代码
堆上的初始化,会调用runtime.makeslice,成功时会在堆上申请一块连续的内存。首先会进行一些参数检查,包括使用的内存空间是否溢出、申请的内存大小是否超过限制、len是否是负数或len是否超过cap。之后的mallocgc是在堆上申请对应大小的内存,在这里不再展开。
// 代码地址
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.
// 实际上是容量超限,但大部分情况下cap是隐式指定的,所以说是长度超限。
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
复制代码
在makeslice调用完成后,会进入OSliceHeader的typecheck,进一步填充sliceHeader的len与cap。
// 代码地址
func typecheck1(n *Node, top int) (res *Node) {
...
switch n.Op {
...
case OSLICEHEADER:
// Errors here are Fatalf instead of yyerror because only the compiler
// can construct an OSLICEHEADER node.
// Components used in OSLICEHEADER that are supplied by parsed source code
// have already been typechecked in e.g. OMAKESLICE earlier.
// 因为只有编译器能构建OSliceHeader节点所以这里的error是fatal
// 构成OSliceHeader的元素之前的节点(如OMakeSlice)检查过了
ok |= ctxExpr
t := n.Type
if t == nil { //检查是否有类型
Fatalf("no type specified for OSLICEHEADER")
}
if !t.IsSlice() { // 检查是否是slice类型
Fatalf("invalid type %v for OSLICEHEADER", n.Type)
}
if n.Left == nil || n.Left.Type == nil || !n.Left.Type.IsUnsafePtr() { // 检查data是否已被赋值
Fatalf("need unsafe.Pointer for OSLICEHEADER")
}
if x := n.List.Len(); x != 2 { // 检查节点list长度
Fatalf("expected 2 params (len, cap) for OSLICEHEADER, got %d", x)
}
n.Left = typecheck(n.Left, ctxExpr)
l := typecheck(n.List.First(), ctxExpr)
c := typecheck(n.List.Second(), ctxExpr)
l = defaultlit(l, types.Types[TINT])
c = defaultlit(c, types.Types[TINT])
if Isconst(l, CTINT) && l.Int64Val() < 0 { // 检查len是否非负
Fatalf("len for OSLICEHEADER must be non-negative")
}
if Isconst(c, CTINT) && c.Int64Val() < 0 { // 检查cap是否非负
Fatalf("cap for OSLICEHEADER must be non-negative")
}
// 检查len <= cap
if Isconst(l, CTINT) && Isconst(c, CTINT) && l.Val().U.(*Mpint).Cmp(c.Val().U.(*Mpint)) > 0 {
Fatalf("len larger than cap for OSLICEHEADER")
}
n.List.SetFirst(l)
n.List.SetSecond(c)
...
}
}
复制代码
截取
截取初始化类似关键字初始化中栈上的初始化,会复用目标源切片的底层数组,重新对len与cap赋值。
{
a := []int{1,2,3,4}
b := a[1:2]
}
0x0057 00087 (main.go:4) MOVQ AX, "".a+64(SP) // data
0x005c 00092 (main.go:4) MOVQ $4, "".a+72(SP) // len
0x0065 00101 (main.go:4) MOVQ $4, "".a+80(SP) // cap
0x006e 00110 (main.go:5) JMP 112
0x0070 00112 (main.go:5) JMP 114
0x0072 00114 (main.go:5) ADDQ $8, AX // t=a[1:]
0x0076 00118 (main.go:5) MOVQ AX, "".b+40(SP) // data=t
0x007b 00123 (main.go:5) MOVQ $1, "".b+48(SP) // len=1
0x0084 00132 (main.go:5) MOVQ $3, "".b+56(SP) // cap=3
复制代码
访问
访问切片的len与cap 即读取SliceHeader的对应字段。切片底层是数组支持随机访问,通过下标访问也是读取data中的对应值。遍历中的访问将在之后的「遍历」中介绍。
func (s *state) expr(n *Node) *ssa.Value {
...
switch n.Op{
...
case OLEN, OCAP: // 代码地址
switch {
...
case n.Left.Type.IsSlice():
op := ssa.OpSliceLen
if n.Op == OCAP {
op = ssa.OpSliceCap
}
return s.newValue1(op, types.Types[TINT], s.expr(n.Left)) // 解读为int类型
...
}
...
case OINDEX: // 代码地址
switch {
...
case n.Left.Type.IsSlice():
p := s.addr(n) // 取地址
return s.load(n.Left.Type.Elem(), p) // load为对应类型
...
}
...
}
}
复制代码
遍历
遍历分为经典循环和golang较特有的range循环
经典循环
经典循环是包含四块,分别是
- 初始化循环的Ninit
- 循环的继续条件Left
- 循环体结束时执行的Right
- 循环体NBody
for Ninit; Left; Right {
NBody
}
复制代码
这个各语言的实现基本是类似的,就不再赘述了。
range循环
range其实是golang提供的语法糖,range一个数组/切片,在编译时会将其转换为经典循环的形式。
ha := a
hv1 := 0
hn := len(ha) //切片长度
v1 := hv1 //index
v2 := nil //value
for ; hv1 < hn; hv1++ {
tmp := ha[hv1]
v1, v2 = hv1, tmp
...
}
复制代码
追加
在源码的注释中描述的比较清晰,流程大体上由是否赋值回原切片(inplace)区分。
如果不赋值回原切片,会首先判断追加后的长度是否超过原切片的容量,如果超过会调用growslice扩容,之后将元素追加到底层数组的末尾,最后调用makeslice补充len与cap 得到新的切片。
如果赋值回原切片,与上述过程类似,区别在于扩容后会更新底层数组与容量,最后会更新长度而不需要调用makeslice。
// 代码地址
// If inplace is false, process as expression "append(s, e1, e2, e3)":
ptr, len, cap := s
newlen := len + 3
if newlen > cap {
ptr, len, cap = growslice(s, newlen)
newlen = len + 3 // recalculate to avoid a spill
}
// with write barriers, if needed:
*(ptr+len) = e1
*(ptr+len+1) = e2
*(ptr+len+2) = e3
return makeslice(ptr, newlen, cap)
// If inplace is true, process as statement "s = append(s, e1, e2, e3)":
a := &s
ptr, len, cap := s
newlen := len + 3
if uint(newlen) > uint(cap) {
newptr, len, newcap = growslice(ptr, len, cap, newlen)
vardef(a) // if necessary, advise liveness we are writing a new a
*a.cap = newcap // write before ptr to avoid a spill
*a.ptr = newptr // with write barrier
}
newlen = len + 3 // recalculate to avoid a spill
*a.len = newlen
// with write barriers, if needed:
*(ptr+len) = e1
*(ptr+len+1) = e2
*(ptr+len+2) = e3
复制代码
扩容
扩容即追加中的growslice。首先是确定新切片的容量,目前新容量的计算方式如下:
-
如果所需的容量cap大于原切片容量old.cap的两倍,新容量newcap更新为cap
-
如果cap小于等于old.cap*2
- 如果old.cap小于1024,newcap为old.cap*2
- 如果old.cap大于等于1024,newcap为old.cap*1.25
func growslice(et *_type, old slice, cap int) slice {
...
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.cap < 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
}
}
}
...
}
复制代码
接下来会计算新切片所需的内存大小,默认情况下会直接通过cap * et.size 获取需要的内存大小,但是对一些特殊的元素大小(1, 指针大小, 2的整数次幂)会进行计算的优化。最后都会通过roundupsize对计算出的内存进行对齐操作以减少碎片,并重新更新newcap。
// Specialize for common values of et.size.
// For 1 we don't need any division/multiplication.
// For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
// For powers of 2, use a variable shift.
// 对于1不需要进行乘法;对于指针大小可以乘以常量; 对于2的整数幂可以左移计算
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 == sys.PtrSize:
lenmem = uintptr(old.len) * sys.PtrSize // 乘一个常量
newlenmem = uintptr(cap) * sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize) // 重新计算newcap
case isPowerOfTwo(et.size):
var shift uintptr // 左移的位数
if sys.PtrSize == 8 { // 掩码
// Mask shift for better code generation.
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)
}
复制代码
计算完lenmem与capmem后会创建一段连续的大小为capmem的内存并清空不会被覆写区域[lenmem, capmem)的内存,最后通过memmove 将原切片的数据拷贝到新的数组中。
var p unsafe.Pointer
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
// The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
// Only clear the part that will not be overwritten.
// 只清空需要覆写区域的内存
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
// Only shade the pointers in old.array since we know the destination slice p
// only contains nil pointers because it has been cleared during alloc.
// 实际上在malloc时已经清空了,这里是对新的数组染色
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
}
}
memmove(p, old.array, lenmem) // 拷贝数据
return slice{p, old.len, newcap}
复制代码
拷贝
golang 内置了copy函数,可以用来copy slice(也可以把string copy到slice),函数的声明在src/builtin/builtin.go中,copy会有防溢出设置,返回值是拷贝的元素数,返回值会等于src和dst长度的较小值( return value = min(len(src),len(dst)))。
// The copy built-in function copies elements from a source slice into a
// destination slice. (As a special case, it also will copy bytes from a
// string to a slice of bytes.) The source and destination may overlap. Copy
// returns the number of elements copied, which will be the minimum of
// len(src) and len(dst).
func copy(dst, src []Type) int
复制代码
在builtin.go中只有函数的声明,具体函数的实现还得看汇编代码,用go tool compile -S即可查看具体的汇编实现,来看一段程序。
go源代码:
package main
func main() {
}
func myCopy(src []int64) {
dst := make([]int64, 0)
copy(dst, src)
_ = dst
}
复制代码
关键汇编代码:
0x0037 00055 (main.go:9) MOVQ "".dst+128(SP), AX //dst.data
0x003f 00063 (main.go:9) MOVQ "".dst+136(SP), CX //dst.len
0x0047 00071 (main.go:9) MOVQ "".dst+144(SP), DX //dst.cap
0x004f 00079 (main.go:9) MOVQ AX, ""..autotmp_3+88(SP) //dst.data
0x0054 00084 (main.go:9) MOVQ CX, ""..autotmp_3+96(SP) //dst.len
0x0059 00089 (main.go:9) MOVQ DX, ""..autotmp_3+104(SP) //dst.cap
0x005e 00094 (main.go:9) MOVQ "".dst+40(SP), AX //src.data
0x0063 00099 (main.go:9) MOVQ "".dst+48(SP), CX //src.len
0x0068 00104 (main.go:9) MOVQ "".dst+56(SP), DX //src.cap
0x006d 00109 (main.go:9) MOVQ AX, ""..autotmp_4+64(SP) //src.data
0x0072 00114 (main.go:9) MOVQ CX, ""..autotmp_4+72(SP) //src.len
0x0077 00119 (main.go:9) MOVQ DX, ""..autotmp_4+80(SP) //src.cap
0x007c 00124 (main.go:9) MOVQ ""..autotmp_3+96(SP), AX
0x0081 00129 (main.go:9) MOVQ AX, ""..autotmp_5+32(SP) //sp+32 = dst.len
0x0086 00134 (main.go:9) CMPQ ""..autotmp_4+72(SP), AX //src.len dst.len
0x008b 00139 (main.go:9) JLT 143 //如果src.len < dst.len 则跳到143
0x008d 00141 (main.go:9) JMP 227 //否则,跳到227(调到155)
0x008f 00143 (main.go:9) MOVQ ""..autotmp_4+72(SP), AX
0x0094 00148 (main.go:9) MOVQ AX, ""..autotmp_5+32(SP) //sp+32 = src.len (这几行代码的作用是取src.len和dst.len的较小值,以确定copy的字节数) (src+32 = min(src.len,dst.len))
0x0099 00153 (main.go:9) JMP 155
0x009b 00155 (main.go:9) MOVQ ""..autotmp_4+64(SP), AX //src.data放入AX
0x00a0 00160 (main.go:9) CMPQ ""..autotmp_3+88(SP), AX //比较dst.data和src.data
0x00a5 00165 (main.go:9) JNE 169 //如果src.data!=dst.data 跳到169
0x00a7 00167 (main.go:9) JMP 225 //否则,跳到225(src和dst的data相等,无须copy)
0x00a9 00169 (main.go:9) MOVQ ""..autotmp_5+32(SP), AX //min(src.len,dst.len) 放入AX
0x00ae 00174 (main.go:9) MOVQ AX, ""..autotmp_6+24(SP) //sp+24 = minlen
0x00b3 00179 (main.go:9) MOVQ ""..autotmp_3+88(SP), CX //dst.data放入cx
0x00b8 00184 (main.go:9) MOVQ ""..autotmp_4+64(SP), DX //src.data放入dx
0x00bd 00189 (main.go:9) MOVQ CX, (SP) //dst.data放入(sp)(第一个参数)
0x00c1 00193 (main.go:9) MOVQ DX, 8(SP) //src.data放入(sp+8)(第二个参数)
0x00c6 00198 (main.go:9) SHLQ $3, AX //minlen 左移三位(因为我们的切片是int类型,int类型的长度为8Byte,所以总的copy的字节数就是minlen*8,也就等同于minlen << 3)
0x00ca 00202 (main.go:9) MOVQ AX, 16(SP) //放入(sp+16)(minlen << 3)(第三个参数)
0x00cf 00207 (main.go:9) PCDATA $1, $1
0x00cf 00207 (main.go:9) CALL runtime.memmove(SB) //memmove的三个参数准备完毕,调用runtime.memmove
0x00d4 00212 (main.go:9) JMP 214 //退出函数
0x00d6 00214 (main.go:11) PCDATA $1, $-1
0x00d6 00214 (main.go:11) MOVQ 112(SP), BP
0x00db 00219 (main.go:11) ADDQ $120, SP
0x00df 00223 (main.go:11) NOP
0x00e0 00224 (main.go:11) RET
0x00e1 00225 (main.go:9) JMP 214
0x00e3 00227 (main.go:9) JMP 155
0x00e5 00229 (main.go:9) NOP
复制代码
可以看到,copy并没有像map的make那样,直接有runtime的实现,copy的实现是汇编代码+runtime.memmove。下面让我们来看下runtime.memmove,函数的原型很简单,至于函数的实现为了性能上的优化,golang采用了汇编代码实现,与cpu架构有关(有一篇详解的文章:mzh/blog)。
// memmove copies n bytes from "from" to "to".
//
// memmove ensures that any pointer in "from" is written to "to" with
// an indivisible write, so that racy reads cannot observe a
// half-written pointer. This is necessary to prevent the garbage
// collector from observing invalid pointers, and differs from memmove
// in unmanaged languages. However, memmove is only required to do
// this if "from" and "to" may contain pointers, which can only be the
// case if "from", "to", and "n" are all be word-aligned.
//
// Implementations are in memmove_*.s.
//
//go:noescape
func memmove(to, from unsafe.Pointer, n uintptr)
复制代码
在runtime/slice.go中还可以看到有slicecopy这个函数,这个函数会被反射包的切片拷贝reflect.Copy使用。
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 size == 1 { // common case worth about 2x to do here
// TODO: is this still worth it with new memmove impl?
*(*byte)(toPtr) = *(*byte)(fromPtr) // known to be a byte pointer
} else {
memmove(toPtr, fromPtr, size)
}
return n
}
复制代码
使用时的一些注意点
- slice使用底层数组,在slice不释放时,底层数组也无法释放,这会导致,如果对大量的bigger slice做截取操作,可能会导致隐含的内存泄露,正确的做法是使用copy将小内存copy出来。
func main() {
s2 := Mem2() //385024
printMem()
s2 = nil
s1 := Mem1() //800415744
printMem()
_, _ = s1, s2
fmt.Println(s2)
fmt.Println(s1)
}
func Mem1() []int64 {
s := make([]int64, 100_000_000)
return s[:10]
}
func Mem2() []int64 {
s := make([]int64, 100_000_000)
dst := make([]int64, 10)
copy(dst, s)
return dst
}
func printMem() {
runtime.GC()
s := runtime.MemStats{}
runtime.ReadMemStats(&s)
fmt.Println(s.HeapInuse)
}
//425984
//800448512
复制代码
- range一个slice时,不要直接对index和value取地址,因为index和value并不是在每次遍历都会生成临时变量。
func Print(dst []*int64) {
for _, v := range dst {
fmt.Println(*v)
}
}
func Range1() {
src := []int64{1, 2, 3}
dst := make([]*int64, 3)
for i, v := range src {
dst[i] = &v
}
Print(dst)
}
//输出的结果是3,3,3
复制代码
正确的做法是对src[i]取地址。
func Range2() {
src := []int64{1, 2, 3}
dst := make([]*int64, 3)
for i := range src {
dst[i] = &src[i]
}
Print(dst)
}
复制代码
- 在多个切片共用同一个底层数组时,注意改动数据带来的影响
func main() {
var (
secret = []byte("secret")
data = []byte("data_raw")
flag = false
)
genMD5 := func(d []byte) []byte {
s := md5.New()
s.Write(append(d, secret...))
return s.Sum(nil)
}
encode := func(d []byte) []byte {
return append(d, genMD5(d)...)
}
getMD5 := func(d []byte) ([]byte, []byte) {
if !flag {
return d[:len(d)-16], d[len(d)-16:]
} else {
return d[: len(d)-16 : len(d)-16], d[len(d)-16:]
}
}
decode := func(d []byte) ([]byte, error) {
data, digest := getMD5(d)
if calcDigest := genMD5(data); bytes.Compare(calcDigest, digest) != 0 {
return nil, fmt.Errorf("check error, calc_digest=%x, attached_digest=%x", string(calcDigest), string(digest))
}
return data, nil
}
eData := encode(data)
dData, err := decode(eData)
fmt.Printf("data=%+v, err=%+v\n", string(dData), err)
flag = true
eData = encode(data)
dData, err = decode(eData)
fmt.Printf("data=%+v, err=%+v\n", string(data), err)
return
}
/*
data=, err=check error, calc_digest=a88000e689a266fc62a0328fd80a5aec, attached_digest=73656372657466fc62a0328fd80a5aec
data=data_raw, err=<nil>
*/
复制代码
参考资料
Go 语言切片的实现原理 | Go 语言设计与实现 (draveness.me)
Go Slices: usage and internals – The Go Blog (golang.org)
如何确定一个 Go 变量会被分配在哪里? | RussellLuo