Go语言学习笔记
Go语言源码阅读与原理分析
Go的编译
数据结构
数组
声明
|
|
第二种编译器会进行转换成第一种
上限推导
语句转换
由字面量(具体整数,浮点数,字符串)组成的数组,根据长度进行优化
- 元素数量
<=
4,元素放在栈上 >
4时,元素保存在静态区,运行时取出
元素数量<=
4时,简化为赋值表达式
|
|
review
编译之后的二进制文件包含:栈,堆,数据段,代码段
堆栈为动态区域,数据段和代码段为静态区域
栈:编译器自动分配释放,存放参数值,局部变量等
堆:程序动态申请的内存,malloc,用链表实现
代码区:函数体的二进制代码
数据段:包含
- 只读数据段 const
- 已初始化的读写数据段 初始化的全局变量,初始化的静态局部变量static
- 未初始化段 未初始化的全局变量和静态变量
当数组元素个数大于四个时
获取一个唯一的staticname,在静态存储区进行初始化,之后再拷贝到栈上
访问和赋值
编译器的静态类型检查时检测数组越界,索引是否为非负整数,索引越界
使用变量作为索引时,无法编译检查,需要运行时阻止,出发panic
发现数组切片字符串越界时通过运行时的runtime.panicIndex
和runtime.goPanicIndex
触发panic
下标没有越界时,编译器获取数组的内存地址和访问下标,计算出目标地址,使用Load将元素加载到内存中
编译时插入运行时越界检查函数
赋值时先确定目标元素地址,使用Store指令将数据存入地址,在编译阶段而不是运行时
切片
编译时确定类型,存储在Extra字段
数据结构
|
|
切片只在运行时确定内容
初始化
切片slice[l:r]
调用SliceMake函数,参数为 元素类型,数组指针,切片大小和容量
这样初始化的切片创建了指向原切片的结构体
字面量
编译时:
对字面量数组做大小推断,初始化为数组
创建一个数组指针,指向静态数组
使用[:]
通过指针创建切片
make
make([]int, len, cap)
会做参数校验,cap >= len
- 判断切片大小和容量是否足够
- 切片是否发生了逃逸,最终在堆上初始化
切片太大时也会在堆上初始化,使用运行时makeslice
makeslice会在堆上申请连续的内存
可能的运行时错误:
- 内存空间大小发生了溢出
- 申请内存大于最大可分配内存
- 传入的
len<0 or len > cap
访问
对len和cap的访问会在编译时替换为常量
使用index获取元素会直接转换为对地址的访问
append和扩容
如果append之后不需要赋值给原有变量:
判断append之后的大小和容量触发扩容
如果append之后需要赋值给原有变量:
append后的切片覆盖原切片,编译器优化为不发生拷贝,直接操作原切片
growslice
先确定新切片容量,扩容策略:
- 如果期望容量大于当前的两倍,就会使用期望容量
- 当前切片长度小于1024会将容量翻倍
- 如果当前的切片长度大于1024每次增加25%的容量,直到新容量大于期望容量
扩容之后进行内存对齐,提高内存分配效率,减少碎片
使用预制的内存大小数组向上取整,然后通过该内存大小重新计算cap
对于非指针切片,将原数组内容拷贝至新内存
growslice返回的是一个新的切片,都是新的 slice(p, len, newcap)
copy
copy(a,b)
编译时会直接使用memmove拷贝到内存
运行时会直接进行合法性检查
大切片性能开销比较大
哈希
解决冲突
开放寻址
从index处向后寻找空闲位置,读取会从index处向后匹配相等元素
装载因子=元素数量/数组大小
装载因子增大,线性探测法的平均用时增加,最坏到On
拉链法
使用链表数组,每个数组是一个桶,通过index访问
装载因子=元素数量/桶数量
一般情况下不超过1,装载因子太大会触发扩容
struct
runtime hmap
保存对数,桶的数量都是2的倍数
包含runtime bmap,能存储8个键值对,超过8个时会使用extra.nextOverflow中的溢出桶存
bmap中存储key哈希的高八位tophash uint8
初始化
字面量
当哈希表的元素<=25
个时,将初始化转化为
|
|
超过时会转换为两个切片循环加入hash
运行时
当桶的数量小于$2^4$,不创建溢出桶
否则创建$2^{B-4}$个溢出桶
读写
遍历使用for range
删除delete(hash, key)
bmap的实际存储是tophash为一个连续的空间,keys,values 。。。
访问时限获取哈希值,再获取哈希的高8位
通过哈希的最低几位获取桶序号
这里因为哈希计算出来并不在桶范围内,在二进制中体现出来是取高几位和低几位,低几位就是取模之后的值,因此可以有效避免桶中有大量重复tophash
在bmap中先比较哈希高8位,加速访问
匹配成功会根据指针和偏移量获取key进行比较,匹配成功再获取value
尽量使用双值接收结果,防止实际的value是nil
写入时会遍历正常桶和溢出桶,溢出桶也包含tophash
如果桶满,创建新的桶护着在溢出桶中保存
获取存储地址后,将值拷贝到内存
扩容
在写入时,触发扩容
- 装载因子超过6.5
- 哈希使用了太多溢出桶,容易产生内存泄露
扩容非原子过程,扩容前判断是否正在扩容
溢出桶太多触发等量扩容,新桶保存数据,回收旧桶(大量的写入删除操作)
翻倍扩容,随着写操作增量进行,不会产生性能的巨大抖动,创建一组新桶和溢出桶,将原来的桶组设置到oldbuckets,溢出桶也设置到oldoverflow上
数据迁移发生在运行时 evacuate,对传入桶的元素再分配,每个旧桶元素分流到两个新桶
分流逻辑:原来通过取模得到的桶掩码为0b11
,扩容翻倍之后将变为0b111
,因此该数据被分流到3号和7号桶
当旧桶完全被分流后清除oldbuckets和oldoverflow,通过计数器控制
扩容期间访问时,若oldbuckets存在,并且旧桶没有被分流时会先去旧桶寻找
扩容期间写入赋值时,会触发增量拷贝,向新桶分流
删除
扩容期间删除,会分流桶中的元素,然后找到桶中的目标键值对删除
与写入类似
访问,写入删除都是运行时处理
字符串
只读的字节数组,使用连续空间
data和len
分配在只读的内存空间,修改需要和[]byte
相互转换
- 拷贝内存到栈或者堆
- 将变量修改为
[]byte
然后修改字节数据 - 修改字节数组转回
string
|
|
赋值时使用scanner解析字符串成token流
strconv.Unquote
去除引号
使用+拼接,拼接字符串的数量小于等于五个时,使用concatstring{2,3,4,5},否则使用runtime.concatstrings,传入数组切片,最终通过运行时过滤空字符串计算拼接后长度,如果非空字符串数量为1,并且不在栈上,则直接返回
拷贝到目标地址空间
类型转换有性能损失,需要拷贝数据
函数调用
c语言使用寄存器传参数,小于等于六个时使用寄存器,大于6个的参数使用栈传递,使用寄存器传递返回值,并且只使用一个寄存器,因此只能有一个返回值。
go语言使用栈传递参数和返回值,因此存在性能损失,但是可以支持多返回值,便于维护编译器,不需要考虑寄存器数量和命名。
参数传递方式
传值,基本类型,结构体,指针,对参数进行拷贝