首页 » 网站建设 » phpsubstr函数界限技巧_Golang源码系列深入理解slice

phpsubstr函数界限技巧_Golang源码系列深入理解slice

duote123 2024-12-14 0

扫一扫用手机浏览

文章目录 [+]

01

slice到底是什么?

phpsubstr函数界限技巧_Golang源码系列深入理解slice

一句话概括:slice是一个构造体。

phpsubstr函数界限技巧_Golang源码系列深入理解slice
(图片来自网络侵删)

在源码中对应的构造体定义为:

type slice struct { array unsafe.Pointer len int cap int}array:指针类型,指向内存数组首地址len:数组的长度,即可以用下标形式访问的数据长度cap:数组的容量

看到这个定义,是不是觉得slice也便是一个包含三个成员变量的普通构造体?并没什么特殊的。
不过这也侧面证明了Go措辞设计所追求的目标:大略性和可读性,看到定义或者代码,能让人直接明白这段代码的含义,相称了不起。

言归正传,这里还有一些细节可以补充。

首先是,array是一个unsafe.Pointer类型,其指向的工具可以被gc扫描到,因此slice的内存不须要自己管理;其余,除了常规的slice,源码中还有一个notInHeapSlice,即不在堆上分配内存的slice,其常日分配在系统栈空间上存放一些内存管理的元数据,在sysAlloc()等内部函数中利用,详情可以参考runtime/malloc.go。

02

slice的内存布局

理解了slice的构造,我们来直不雅观的看一个slice实例在内存中的布局:

a := make([]int, 5, 8)

a的内存布局如下:

c:=a[3:6]

取a的切片[3:6]赋值为c,c对应的内存布局如下:

取切片(截取)语法为左闭右开区间,复制的切片长度即为新切片的len,这个很好理解,但是新切片的cap为什么是5?读者可以思考一分钟,本文将在03节详细描述其事理。

03

slice操作的底层事理

截取

接上文,首先来理解一下slice截取的事理。

slice的截取没有任何黑邪术,实在是通过Go编译器,在编译过程中对干系语法进行解析,然后自动天生对应的汇编代码,比如之前的例子:

c := a[3:6]// 对应汇编代码// 是的,你没看错,一行代码对应这么多行汇编~~00807 MOVQ "".a+264(SP), DX // 取出a.cap00815 MOVQ "".a+248(SP), AX // 取出a.array00823 CMPQ DX, $6 // 比较a.cap,截取切片的右边界600827 JCC 837 // 成功则跳转到地址83700829 NOP 00832 JMP 1608 // 失落败跳转到地址1608,实行panic00837 PCDATA $1, $-100837 JMP 839 // 从827跳转过来,跳转到83900839LEAQ-3(DX),CX//算术运算,打算a.cap-3=5,地址赋值给CX00843 MOVQ CX, DX // DX = CX = a.cap - 3 = 500846NEGQCX//对“5”求补,即取反加一,得到CX=-500849SARQ$63,CX//算术右移,补位为符号位,-5右移63次,得到CX=0xFFFF(64位)00853ANDQ$12,CX//与操作,12&0xFFFF,得到截取须要的偏移CX=12=3(左边界)4(sizeof(int32))//00846~00853这几步相称于把偏移量存到CX寄存器中,别的代码可能会用到这个偏移00857ADDQCX,AX//AX=AX+CX=a.array+12,截取后的起始地址00860 MOVQ AX, "".c+224(SP) // 赋值c.array00868 MOVQ $3, "".c+232(SP) // 赋值c.len = 300880 MOVQ DX, "".c+240(SP) // 赋值c.cap = DX = a.cap - 3 = 5

从上面的汇编代码可以知道,全体过程没有函数调用,效率最高,以是也不会做更多的繁芜逻辑来打算cap,后续cap的变革完备可以交给扩容流程。
因此,Go编译器用汇编指令实现了截取的全部逻辑,担保了slice截取的精确性,并且:

新slice.array = 旧slice.array + 偏移,指向同一片内存区域;新slice.len = 截取右边界 - 截取左边界;新slice.cap = 旧slice.cap - 截取左边界,如果旧slice.cap不足用,那将直接panic。

创建

利用slice的第一步,便是创建。
创建的逻辑实在并不繁芜,这里就不贴源码了,感兴趣的读者可以参考源码runtime/slice.go中的makeslice函数。

创建的语法常日为:make([]type, len, cap),源码实现只要两步:

打算"capsizeof(type)"的内存空间,把稳是cap而不是len,并判断是否存在非常情形,比如cap特殊大,超过了可用内存分配空间等;向内存管理器MCache申请对应内存并返回。

可以看到,这里我们只申请了slice.array,即存放slice元素的内存空间,但是没有为len和cap的存放申请空间,这是为什么呢?

缘故原由是,slice.array申请的内存可能很大,是无法在栈等分配的,以是须要通过Go的内存管理,在runtime库中完成;但slice.len和slice.cap,是可以直接在栈中进行分配的,这样效率更高,在64位机器下,两个int只须要16个字节,以是在Go编译期间解析到干系语法就已经自动翻译成汇编指令,封装对应的len和cap,放在slice.array后面的十六位地址中。

扩容

接下来,我们理解下slice中最主要,最常见,但又最不随意马虎感知到的操作:扩容。

扩容的完全源码在runtime/slice.go#growslice函数中,这里截取最核心的部分:

funcgrowslice(et_type,oldslice,capint)slice{//.../ cap小于1024,按照两倍扩容;大于1024,按照1/4扩容 / 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 } } }/根据slice中元素的_type,进行内存对齐,重新调度cap/var overflow bool var lenmem, newlenmem, capmem uintptr // 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. 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): var shift uintptr if goarch.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) }// ...memmove(p, old.array, lenmem)return slice{p, old.len, newcap}}

核心流程实在只有三步:

cap小于1024,按照double扩容;cap大于即是1024,按照1/4扩容;根据slice中元素的实际类型,和sizeof大小,进行内存对齐,重新调度cap,这一步是为了提高内存利用效率,减少内存碎片;memmove,拷贝旧数据,到新申请的内存中。

个中,比较特殊的是第二点,举一个例子来加深理解:

b := make([]bool, 2, 2)fmt.Printf("bbefore:len=%v,cap=%v\n",len(b),cap(b))b=append(b,true)fmt.Printf("bafter:len=%v,cap=%v\n",len(b),cap(b)//Output:// b before: len=2, cap=2// b after: len=3, cap=8

每个bool类型元素size大小为一个字节,在64位机器按照8字节对齐的情形下,append后cap直接扩容到8。

下标访问

末了一起来理解下slice的下标访问是如何实现的。

item := c[2]

实在,思路大家都该当可以想到,便是指针移动,Go编译器也是这么实现的,而且也相称简洁,当碰着上面代码时,编译后自动转成汇编指令:

01260MOVQ"".c+216(SP),CX//取c.len01268MOVQ"".c+208(SP),AX//取c.array01276NOP01280CMPQCX,$2//len与下标"2"判断01284JHI1291//大于则跳转到地址129101286JMP1583//否则无条件跳转到地址1583,实行panic逻辑01291MOVL 8(AX),AX//一个int32占用4个字节,从c.array移动8个字节赋值给AX01294MOVLAX,"".item+64(SP)//将AX寄存器的值读取出来,赋值给item

04

slice值通报的秘密

通过前面三个小节的阐述,相信你已经理解了slice的事理,现在便是运用的时候了,首先来一个口试中最大略但常被cue到的问题:

golang中函数传参是值通报还是指针通报?

答案各位读者该当都可以脱口而出:值通报,并且golang中不管什么类型,函数传参都是值通报。
那么接下来,来看这么一段代码:

package mainimport ( "fmt""math/rand")func addItem(a []int32) { a = append(a, rand.Int31()) fmt.Printf("a add [%v]: pointer=%p, len=%v, cap=%v\n", a[len(a)-1], a, len(a), cap(a))}func main() { a := make([]int32, 5, 8) addItem(a) fmt.Printf("a after add: pointer=%p, len=%v, cap=%v\n", a, len(a), cap(a))}

上面代码中两行fmt.Printf对应的输出分别是多少呢?思考10秒钟,我们直接go run运行一下,得到下面的结果:

a add [1298498081]: pointer=0xc0000bc000, len=6, cap=8a after add: pointer=0xc0000bc000, len=5, cap=8

可以看到,addItem内部append元素之后,a的len已经即是6,指针相同并且没有重新分配空间,为什么在main中调用addItem之后,输出a的len又即是5了呢?a对应的切片元素到底新增了没有?

先抛答案:a对应的切片元素确实新增了(即a.array指向的数组元素增多了),但是由于值通报,main中a对应的len仍旧为5,而addItem函数中的a是对main中a的拷贝,转头看看01小节,这里实际拷贝了对应的slice构造体,个中len为int根本类型!
以是不管addItem函数中向a.array中插入了多少个元素,外层main中a的len都不会改变,并且新增的元素也都无法用下标访问到,由于根据03小节中下标访问事理,Go编译器取的是当前栈空间中的a对应的len,如果这个len小于即是下标,那么直接进入了panic流程。

通过在main中加一行代码证明panic:

func main() { a := make([]int32, 5, 8) addItem(a) fmt.Printf("a after add: pointer=%p, len=%v, cap=%v\n", a, len(a), cap(a))fmt.Printf("a[5]=%v,willpanic!\n",a[5])}

运行后得到:

a add [1298498081]: pointer=0xc000020060, len=6, cap=8a after add: pointer=0xc000020060, len=5, cap=8panic: runtime error: index out of range [5] with length 5goroutine 1 [running]:main.main()./test_slice.go:24+0x4bc

相信读者通过这一段代码的思考,更加深入理解了slice和值通报。
但是上面答案的表述中,有一个隐蔽的Tip,无法用下标访问,是还有其他的访问办法吗?答案是有的,在main中加入一段代码:

func main() { a := make([]int32, 5, 8)//这里拷贝了slice构造体,根据01小节,个中a.array是一个指针,指向数组首地址 addItem(a)fmt.Printf("aafteradd:pointer=%p,len=%v,cap=%v\n",a,len(a),cap(a))//取a.array数组首地址 header := (uintptr)(unsafe.Pointer(&a))//取第6个元素,即a.array[5]的地址,int32类型的size=4,以是偏移20fmt.Printf("a[5]=%v\n",(int32)(unsafe.Pointer(header+20)))}

终极我们可以得到输出,打破了当前len的限定!

a add [1298498081]: pointer=0xc000020060, len=6, cap=8a after add: pointer=0xc000020060, len=5, cap=8a[5] = 1298498081

相信仔细读完的读者,已经对slice胸有成竹。
那么文章的末了,给大家留一个拓展的思考题:

func addItem(a map[string]string) { a["test"] = "ok" fmt.Printf("addItem ==> pointer=%p, a.len=%v\n", a, len(a))}func main() { a := make(map[string]string) addItem(a) fmt.Printf("pointer=%p, a.len=%v\n", a, len(a))}// Output://addItem ==> pointer=0xc000072180,a.len=1// pointer=0xc000072180, a.len=1

为什么同样的流程,函数内对map的操作又可以在main中感知到呢?答案将不才一篇文章揭晓。

标签:

相关文章

大数据时代,数据驱动下的未来生活图景

随着互联网、物联网、云计算等技术的飞速发展,大数据已经成为推动社会进步的重要力量。大数据以其独特的优势,改变着我们的生产、生活、学...

网站建设 2024-12-16 阅读0 评论0

大数据时代,数据驱动的未来与挑战

随着信息技术的飞速发展,大数据已经成为当今时代最具影响力的关键词之一。大数据不仅改变了我们的生活方式,也推动了各行各业的变革。本文...

网站建设 2024-12-16 阅读0 评论0

大数据时代,既美科技引领美妆产业新风尚

随着互联网技术的飞速发展,大数据已经成为现代社会不可或缺的一部分。在我国,大数据产业呈现出蓬勃发展的态势,各行各业都在积极探索如何...

网站建设 2024-12-16 阅读0 评论0

jsp怎么转成php技巧_JSP的编程

JSP(全称Java Server Pages)是由 Sun Microsystems 公司倡导和许多公司参与共同创建的一种使软件...

网站建设 2024-12-16 阅读0 评论0

ajaxphpa技巧_AJAX技能进修笔记

AJAX 是一种用于创建快速动态网页的技能,通过在后台与做事器进行少量数据交流,使网页实现异步更新。这意味着可以在不重载全体页面的...

网站建设 2024-12-16 阅读0 评论0