【Free Style】深入Go语言模型(3):各种数据结构的内存模型详解

举报
霜雯 发表于 2017/11/09 11:00:56 2017/11/09
【摘要】 前言今天主要是讲解一下Go语言中各种数据结构的内存模型,主要包括基本类型的内存模型数组和切片slice的内存模型map的内存模型struct的内存模型和字节对齐interface的内存模型channel的内存模型总的来说,Go语言的内存模型和C语言是类似的,所以了解C语言的一定会很容易理解Go语言的内存模型。这里不会描述并发语义的内存模型,那个会和并发一起进行分析基本数据类型Go语言有下面这些基本

image.png

这里不会描述并发语义的内存模型,那个会和并发一起进行分析

基本数据类型

Go语言有下面这些基本数据:
整数类型(int, uint, int8, int16, byte, rune, unitptr等等), 浮点数(float32, float64), 复数(complex64, complex128), 布尔型,字符串。

这些数据结构的内存模型都很简单:
blob

数组

对于数组来说,内存结构如下:
blob

对于bytes这个[5]byte类型,他的内存就是5个连续的字节, 一个数组的定义同时包括了长度和类型。
比如:
var a [4]int
那么就表示声明了一个类型是数组,元素类型是int,长度是4。这里需要注意的是Go语言的数组和C语言的不一样,C语言的数组是一个指针,指向数组的一个元素。但是在Go语言里面数组就是一个普通的值类型。
所以[4]int和[5]int表示两种完全不同的类型。

数组的内存分布就是
blob

切片

切片是对数组中一段数据的引用。在内存中它有三段数据组成:一个指向数据头的指针、切片的长度、切片的容量。长度是索引操作的上界,如:x[i] 。容量是切片操作的上界,如:x[i:j]。
blob

比如我通过s := make([]byte, 5)创建的切片内存如下:
blob
如果我们通过修改切片引用的数据区域和大小,s = s[2:4], 那么就变成了如下的结构
blob

我们通过下面的代码可以很快弄清楚slice的内存模型

image.png


输出结果如下:
0xc042038448
{0xc042038448 5 5}
{0xc04203844a 2 3}

0xc04203844a - 0xc042038448 = 2, 刚好是偏移了两个byte。

从上面的内存模型来看,如果两个数组相互赋值,那么将会触发数组全量拷贝的动作,但是如果是传递切片,那么将只需要永远申请固定大小的切片对象就可以了,底层的数组通过引用传递。

切片的内存增长

从内存模型来看,切片就是引用了一个固定的数组, 一个切片的容量受到起始索引和底层数组容量的限制。Go语言提供了内置的copy和append函数来增长切片的容量,那么调用这些函数以后切片的内存会发生什么变化呢?

copy和append这两个是内置函数,是看不到go源码实现,可能使用C/C++/汇编实现的:

image.png

copy方法

copy方法并不会修改slice的内存模型,仅仅是将某个slice的内容拷贝到另外一个slice中去。底层的实现在runtime\slice.go中,这个方法比较简单,就不赘述了。

func slicecopy(to, fm slice, width uintptr) int

append方法

image.png

输出结果是:
{0xc04203a448 1 1}
{0xc0420369e0 4 4}

那么从上面可以看出,append方法其实重新生成了一个新的数组,然后返回的切片引用了这个新的数组,那我们来重点看一下append方法的实现,为了简单点,写出下面的代码,然后生成汇编:

image.png

从下面的汇编可以得到两个信息:
runtime.growslice是用来实现slice增长的函数
cap函数的实现仅仅是调用b.cap这个成员

image.png

growslice的实现:src\runtime\slice.go

image.png

image.png

具体实现的代码就不说了,其实就是判断cap,生成一个新的数组,将old的元素拷贝到新的slice中去。
扩容规则上面的代码已经说明了:

  • 如果新的大小是当前大小2倍以上,则大小增长为新大小

  • 否则循环以下操作:如果当前大小小于1024,按每次2倍增长,否则每次按当前大小1/4增长。直到增长的大小超过或等于新大小。

在runtime\slice.go中,我们可以看到

image.png

这个也是Go语言内部的slice数据结构,和我们前面定义的是一致的,slice的make,copy,grow等函数都在这个文件中实现

字符串

字符串在内存中其实表示成了这么一个数据结构
这个定义是在runtime\string.go中定义的

image.png

blob

image.png

从上面的代码可以输出如下的结果:
{0x4b1cbe 5}
{0x4b1cbe 3}
{0x4b1cc0 1}

所以说,字符串也是一种特殊的切片,但是是没有容量,只有长度属性。

map的实现

Go语言的map并不是像C++的map一样用二叉树实现的,而是典型Hash实现的。

image.png

在src\runtime\hashmap.go里面可以找到具体的实现:

image.png

其中桶的数据结构如下:

image.png

image.png

image.png

那么内存模型就是:
blob

image.png

blob

struct的字节对齐

在64位系统上面,Go语言的字节是8直接对齐,如果不足的,就补充padding。
这里有详细的描述:http://www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/

下面有一个简单的例子:

image.png

image.png


输出结果如下:
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

image.png

image.png

输出结果是:
blob

我们来看一下slice的make做了啥:

image.png

从上面来看,最重要就是通过mallocgc来申请了一个数组。
我们再来看一下map的make做了啥,直接看runtime的makemap函数

通过查看汇编代码就可以看出make底层是调用哪个函数了

image.png

image.png


从上面看,map初始化最重要的就是创建buckets。

总结

整体来说,Go语言的对象内存模型比C++要简单的多,毕竟没有继承,虚函数,多重继承等等,了解这些内存模型,对于平时使用这些类型时可以少踩坑是有帮助的。



【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

举报
请填写举报理由
0/200