内容简介:要说 slice,那实在是太让人熟悉了,从功能上讲 slice 支持追加,按索引引用,按索引范围生成新的 slice,自动扩容等,和 C++ 或 Java 中的 Vector 有些类似,但也有一些区别。不过 Go 里的 slice 还有一个底层数组的概念,这一点和其它语言不同。slice 的底层结构定义非常直观,指向底层数组的指针,当前长度 len 和当前 slice 的 cap。
slice 和 array
要说 slice,那实在是太让人熟悉了,从功能上讲 slice 支持追加,按索引引用,按索引范围生成新的 slice,自动扩容等,和 C++ 或 Java 中的 Vector 有些类似,但也有一些区别。
不过 Go 里的 slice 还有一个底层数组的概念,这一点和其它语言不同。
runtime/slice.go type slice struct { array unsafe.Pointer len int cap int }
slice 的底层结构定义非常直观,指向底层数组的指针,当前长度 len 和当前 slice 的 cap。
数据指针不一定就是指向底层数组的首部,也可以指腰上:
[]int{1,3,4,5} struct { array unsafe.Pointer --------------+ len int | cap int | } | | v +------|-------|------|------+-----+ | | 1 | 3 | 4 | 5 | | | | | | | +------|-------|------|------+-----+ [5]int
我们可以轻松地推断出,是可以有多个 slice 指向同一个底层数组的。一般情况下,一个 slice 的 cap 取决于其底层数组的长度。如果在元素追加过程中,底层数组没有更多的空间了,那么这时候就需要申请更大的底层数组,并发生数据拷贝。这时候的 slice 的底层数组的指针地址也会发生改变,务必注意。
len 和 cap
Go 语言虽然将 len 和 cap 作为 slice 和 array 附带的 builtin 函数,但对这两个函数的调用实际上最终会被编译器直接计算出结果,并将值填到代码运行的位置上。所以 len 和 cap 更像是宏一样的东西,在 slice 和 array 的场景,会被直接展开为 sl->len 和 sl->cap 这样的结果。
源码分析
形如:
var a = make([]int, 10, 20)
的代码,会被编译器翻译为 runtime.makeslice。
func makeslice(et *_type, len, cap int) slice { maxElements := maxSliceCap(et.size) if len < 0 || uintptr(len) > maxElements { panic(errorString("makeslice: len out of range")) } if cap < len || uintptr(cap) > maxElements { panic(errorString("makeslice: cap out of range")) } p := mallocgc(et.size*uintptr(cap), et, true) return slice{p, len, cap} }
如果是
var a = new([]int)
这样的代码,则会被翻译为:
func newobject(typ *_type) unsafe.Pointer { return mallocgc(typ.size, typ, true) }
实在是太简单了,没啥可说的。mallocgc 函数会根据申请的内存大小,去对应的内存块链表上找合适的内存来进行分配,是 Go 自己改造的 tcmalloc 那一套。
内存拷贝:
func slicecopy(to, fm slice, width uintptr) int { if fm.len == 0 || to.len == 0 { return 0 } n := fm.len if to.len < n { n = to.len } 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)(to.array) = *(*byte)(fm.array) // known to be a byte pointer } else { memmove(to.array, fm.array, size) } return n }
最终最关键的就是 memmove,所有平台的 memmove 都是用汇编实现的,每个平台会针对 memmove 的目标的长度,选择对应平台的优化指令来进行内存移动。比如 intel 平台就有大量的 VMOVDQU,VMOVDQA 指令。不过别人都帮我们实现好了,大概瞟一眼就行,这里截个片段:
.... 前面有很多看不懂的代码 VMOVDQU -0x40(SI), Y1 VMOVDQU -0x60(SI), Y2 VMOVDQU -0x80(SI), Y3 SUBQ $0x80, SI ....总之就是弃疗 VMOVNTDQ Y0, -0x20(DI) VMOVNTDQ Y1, -0x40(DI) VMOVNTDQ Y2, -0x60(DI) ....后面也有很多看不懂的代码
逻辑上稍微有点复杂的就只有 growslice,在对 slice 执行 append 操作时,如果 cap 不够用了,会导致 slice 扩容:
func growslice(et *_type, old slice, cap int) slice { if raceenabled { callerpc := getcallerpc() racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice)) } if msanenabled { msanread(old.array, uintptr(old.len*int(et.size))) } if et.size == 0 { if cap < old.cap { panic(errorString("growslice: cap out of range")) } // append should not create a slice with nil pointer but non-zero len. // We assume that append doesn't need to preserve old.array in this case. return slice{unsafe.Pointer(&zerobase), old.len, cap} } 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 } } } var overflow bool var lenmem, newlenmem, capmem uintptr const ptrSize = unsafe.Sizeof((*byte)(nil)) switch et.size { case 1: lenmem = uintptr(old.len) newlenmem = uintptr(cap) capmem = roundupsize(uintptr(newcap)) overflow = uintptr(newcap) > _MaxMem newcap = int(capmem) case ptrSize: lenmem = uintptr(old.len) * ptrSize newlenmem = uintptr(cap) * ptrSize capmem = roundupsize(uintptr(newcap) * ptrSize) overflow = uintptr(newcap) > _MaxMem/ptrSize newcap = int(capmem / ptrSize) default: lenmem = uintptr(old.len) * et.size newlenmem = uintptr(cap) * et.size capmem = roundupsize(uintptr(newcap) * et.size) overflow = uintptr(newcap) > maxSliceCap(et.size) newcap = int(capmem / et.size) } if cap < old.cap || overflow || capmem > _MaxMem { panic(errorString("growslice: cap out of range")) } var p unsafe.Pointer if et.kind&kindNoPointers != 0 { p = mallocgc(capmem, nil, false) memmove(p, old.array, lenmem) // 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 !writeBarrier.enabled { memmove(p, old.array, lenmem) } else { for i := uintptr(0); i < lenmem; i += et.size { typedmemmove(et, add(p, i), add(old.array, i)) } } } return slice{p, old.len, newcap} }
扩容时会判断 slice 的 cap 是不是已经大于 1024,如果在 1024 之内,会按二倍扩容。超过的话就是 1.25 倍扩容了。
slice 扩容必然会导致内存拷贝,如果是性能敏感的系统中,尽可能地提前分配好 slice 是较好的选择。
var arr = make([]int, 0, 10)
值还是引用传递
网上有很多鬼扯的结论说 Go 的 slice 是按引用传递的,证据是类似下面这样的代码:
func main() { var a = make([]int, 10) fmt.Println(a) } func doSomeHappyThings(sl []int) { if len(sl) > 0 { sl[0] = 1 } }
把 a 传入到 doSomeHappyThings,然后 a 的第一个元素就被修改了,进而认为在 Go 中,slice 是引用传递的。
但实际上并不是这样的,从汇编层面来讲,Go 的 slice 实际上是把三个参数传到函数内部了,这就类似于我们写一段 c 代码:
void doSomeHappyThings(int * arr, int len, int cap) { if(len > 0) { arr[0] = 1 } }
所以如果你在函数内对这个 slice 进行 append 时导致了 slice 的扩容,那理论上外部是不受影响的,哪怕不扩容,也可能只影响底层数组,而不影响传入的 slice。举个例子:
func main() { var arr = make([]int,0,10) doSomeHappyThings(arr) fmt.Println(arr, len(arr), cap(arr), "after return") } func doSomeHappyThings(arr []int) { arr = append(arr, 1) fmt.println(arr, "after append") }
脏活编译器帮你做了一些,但也就导致有些结论不那么直观了。
当然,你可以尝试用汇编来写一个处理 slice 的函数。
a.s:
#include "textflag.h" // func sum(sl []int64) int64 TEXT ·sum(SB),NOSPLIT, $0-32 MOVQ $0, SI MOVQ sl+0(FP), BX // &sl[0], addr of the first elem MOVQ sl+8(FP), CX // len(sl) INCQ CX start: DECQ CX // CX-- JZ done ADDQ (BX), SI // SI += *BX ADDQ $8, BX // 指针移动 JMP start done: // 返回地址是 24 是怎么得来的呢? // 可以通过 go tool compile -S math.go 得知 // 在调用 sum1 函数时,会传入三个值,分别为: // slice 的首地址、slice 的 len, slice 的 cap // 不过我们这里的求和只需要 len,但 cap 依然会占用参数的空间 // 就是 16(FP) MOVQ SI, ret+24(FP) RET
a.go:
package main import "fmt" func sum(s []int64) int64 func main() { arr := []int64{1, 2, 3, 4, 10} sux := sum(arr) fmt.Println(sux) }
以上所述就是小编给大家介绍的《Go 系列文章 9: slice》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 《文章推荐系统》系列之构建离线文章画像
- 《文章推荐系统》系列之构建离线用户和文章特征
- 批量导出某个简书用户的所有文章列表和文章超链接
- “狗屁不通文章生成器”在GitHub热榜火了:文章真够啰嗦的
- 用weexplus从0到1写一个app(2)-页面跳转和文章列表及文章详情的编写
- 8 - 博客文章详情页
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。