【Free Style】深入Go语言模型(3):各种数据结构的内存模型详解
这里不会描述并发语义的内存模型,那个会和并发一起进行分析
基本数据类型
Go语言有下面这些基本数据:
整数类型(int, uint, int8, int16, byte, rune, unitptr等等), 浮点数(float32, float64), 复数(complex64, complex128), 布尔型,字符串。
这些数据结构的内存模型都很简单:
数组
对于数组来说,内存结构如下:
对于bytes这个[5]byte类型,他的内存就是5个连续的字节, 一个数组的定义同时包括了长度和类型。
比如:
var a [4]int
那么就表示声明了一个类型是数组,元素类型是int,长度是4。这里需要注意的是Go语言的数组和C语言的不一样,C语言的数组是一个指针,指向数组的一个元素。但是在Go语言里面数组就是一个普通的值类型。
所以[4]int和[5]int表示两种完全不同的类型。
数组的内存分布就是
切片
切片是对数组中一段数据的引用。在内存中它有三段数据组成:一个指向数据头的指针、切片的长度、切片的容量。长度是索引操作的上界,如:x[i] 。容量是切片操作的上界,如:x[i:j]。
比如我通过s := make([]byte, 5)创建的切片内存如下:
如果我们通过修改切片引用的数据区域和大小,s = s[2:4], 那么就变成了如下的结构
我们通过下面的代码可以很快弄清楚slice的内存模型
输出结果如下:
0xc042038448
{0xc042038448 5 5}
{0xc04203844a 2 3}
0xc04203844a - 0xc042038448 = 2, 刚好是偏移了两个byte。
从上面的内存模型来看,如果两个数组相互赋值,那么将会触发数组全量拷贝的动作,但是如果是传递切片,那么将只需要永远申请固定大小的切片对象就可以了,底层的数组通过引用传递。
切片的内存增长
从内存模型来看,切片就是引用了一个固定的数组, 一个切片的容量受到起始索引和底层数组容量的限制。Go语言提供了内置的copy和append函数来增长切片的容量,那么调用这些函数以后切片的内存会发生什么变化呢?
copy和append这两个是内置函数,是看不到go源码实现,可能使用C/C++/汇编实现的:
copy方法
copy方法并不会修改slice的内存模型,仅仅是将某个slice的内容拷贝到另外一个slice中去。底层的实现在runtime\slice.go中,这个方法比较简单,就不赘述了。
func slicecopy(to, fm slice, width uintptr) int
append方法
输出结果是:
{0xc04203a448 1 1}
{0xc0420369e0 4 4}
那么从上面可以看出,append方法其实重新生成了一个新的数组,然后返回的切片引用了这个新的数组,那我们来重点看一下append方法的实现,为了简单点,写出下面的代码,然后生成汇编:
从下面的汇编可以得到两个信息:
runtime.growslice是用来实现slice增长的函数
cap函数的实现仅仅是调用b.cap这个成员
growslice的实现:src\runtime\slice.go
具体实现的代码就不说了,其实就是判断cap,生成一个新的数组,将old的元素拷贝到新的slice中去。
扩容规则上面的代码已经说明了:
如果新的大小是当前大小2倍以上,则大小增长为新大小
否则循环以下操作:如果当前大小小于1024,按每次2倍增长,否则每次按当前大小1/4增长。直到增长的大小超过或等于新大小。
在runtime\slice.go中,我们可以看到
这个也是Go语言内部的slice数据结构,和我们前面定义的是一致的,slice的make,copy,grow等函数都在这个文件中实现
字符串
字符串在内存中其实表示成了这么一个数据结构
这个定义是在runtime\string.go中定义的
从上面的代码可以输出如下的结果:
{0x4b1cbe 5}
{0x4b1cbe 3}
{0x4b1cc0 1}
所以说,字符串也是一种特殊的切片,但是是没有容量,只有长度属性。
map的实现
Go语言的map并不是像C++的map一样用二叉树实现的,而是典型Hash实现的。
在src\runtime\hashmap.go里面可以找到具体的实现:
其中桶的数据结构如下:
那么内存模型就是:
struct的字节对齐
在64位系统上面,Go语言的字节是8直接对齐,如果不足的,就补充padding。
这里有详细的描述:http://www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/
下面有一个简单的例子:
输出结果如下:
size Example: 8
Alignment Boundary: 8
BoolValue = Size: 1 Offset: 0 Addr: 0xc04200a230
IntValue = Size: 2 Offset: 2 Addr: 0xc04200a232
FloatValue = Size: 4 Offset: 4 Addr: 0xc04200a234
可以看出在64位机器是按照8字节对齐的,并且bool的后面增加了一个字节的padding
输出结果是:
我们来看一下slice的make做了啥:
从上面来看,最重要就是通过mallocgc来申请了一个数组。
我们再来看一下map的make做了啥,直接看runtime的makemap函数
通过查看汇编代码就可以看出make底层是调用哪个函数了
从上面看,map初始化最重要的就是创建buckets。
总结
整体来说,Go语言的对象内存模型比C++要简单的多,毕竟没有继承,虚函数,多重继承等等,了解这些内存模型,对于平时使用这些类型时可以少踩坑是有帮助的。
- 点赞
- 收藏
- 关注作者
评论(0)