简单介绍了Golang中string、slice、array、map四种数据结构,以及Golang内存对齐的策略。
一、string
变长字节存储,根据不同的前缀来判断字节长度。
Golang中的字符串结构:
string [data][lenght] lenght是实际字节byte长度 data为起始地址
所以string的内存是 指针8+长度8=16
Golang中不允许通过下标修改字符,字符串被分配到只读内存段;
字符串是可以共用内存段的;
可以通过赋值直接指向一段新的空间;
还可以转换为slice切片,会给slice重新分配一段新内存,再将原内容copy到slice中;(有方法可以让slice仍然指向原内存)
type stringStruct struct {
str unsafe.Pointer
len int
}
type stringStructDWARF struct {
str *byte
len int
}
二、slice
slice中有三个结构:data、len、buf.
slice的元素要存放在一段连续的内存中,实际上就是个数组;
data就是这个数组的起始地址,len表示数组长度
var int_slice []int
上面这个语句只分配了slice结构,并没有分配底层数组,所有data==nil、len=0、buf=0;
int_slice[0] = 5
因为len==0所以触发了panic
int_slice := make([]int, 2, 5)
上面这个语句开辟了一段容纳了5个零值int的元素的内存作为它的底层数组,但是此时len==2,所以读取它是只能读到[0,0]
int_slice = append(int_slice, 5)
此时再添加一个5,len变成3,修改3位置为5
int_slice := new([]int)
通过new返回一个切片指针,但是new不会进行底层数组的分配,data==nil、len==0,buf==0;
int_slice[0] = 5
因为len==0所以触发了panic
*int_slice = append(*int_slice, 3)
append会给slice开辟一个底层数组
type slice struct {
array unsafe.Pointer
len int
cap int
}
type notInHeapSlice struct {
array *notInHeap
len int
cap int
}
三、array
数组就是同一种类型的元素一个挨着一个的存储,int类型的slice指向的就是int类型的数组
slice不一定是指向array的开头的;
因为slice长度是不可变的,如果slice指向的array长度不够slice怎么办?
slice会重新开辟一段array,将slice指向新的array,并把原数据复制到新array;
如果slice中buf不够用怎么办?slice的扩容规则
if 扩容前的容量*2<所需最小容量,则扩容后容量为所需最小容量
else if 扩容前的容量<1024,则扩容翻倍
else 扩容1/4
一段array到底能申请到多少内存呢?
按照预估内存最接近的span size分配mspan给array
四、内存对齐
CPU从内存读取数据需要通过内存总线把地址传输给内存,内存准备好数据输出到数据总线,交给CPU。
如果地址总线只有8根,那这个地址就只有8位,可以表示256个地址(0~255);
数据总线越宽,一次可操作数据就越大,即机器字长越大。
为了尽可能的少的读取就把数据读出来,
编译器会把数据各种类型的数据按照不同的大小,安排到合适的地址,并占用合适的长度,这就是内存对齐;
内存对齐要求每个数据存储地址以及占用的内存长度都要是它对齐边界的倍数。
平台对齐边界和硬件平台有关,64位:8btye;
数据结构对其边界是取内存大小和平台最大对其边界中较小的那个。
结构体的内存对齐:
首先要确定每个成员的对其边界,取其中最大的作为内存边界。
type struct1 struct {
a int8
b int64
c int16
d int32
}
struct1的内存分配:
初始节点为0;
a需要1字节,此时占用1byte;
b需要8字节,因为需要对齐,所以会后移到8位置开始对齐,此时占用16字节;
c需要2字节,可以直接对齐,此时占用18字节;
d需要4字节,不能对齐,后移到20位置,对齐后占用了24字节
最后看整体一共24字节,不需要对齐,所以这个结构体再内存中最终为24byte
type struct2 struct {
a int8
d int16
c int32
b int64
}
struct2的内存分配:
初始位置为0;
a需要1字节,此时占用1byte;
b需要2字节,此时不能对齐,后移到2开始对齐,此时占用4btye;
c需要4字节,可以对齐,此时占用8字节;
d需要8字节,可以对齐,此时占用16字节;
一共占用了16字节,不用对齐,所以这个结构体最终占用16byte
五、map
hashmap的实现原理:
哈希表用m个桶来存储键值对,当一个key-value键值对进入hashmap时,
经过hash函数得到哈希值,根据哈希值找到应该存放的桶,找桶的方法一般有两种:
1. 哈希值对m取模,得到的值0~m-1,就能找到应该存放的位置
2. 哈希值&(m-1),但是这样的话要求m是2的幂(有且只有一个1,m-1表示这一位后面的都为1),
做&运算也能让得到的值0~m-1
后续又来了一个key2,求哈希值找桶还是这个桶(哈希冲突),应该怎么处理?
1. 开放地址法:定位到的桶已经满了,就往下一个桶滑,直到遇到一个空的桶放入;
当要查询这个key时,定位在其他桶时,会一直往后滑,如果直到最后都没有查找到就判断没放入。
(开放地址法虽然省内存,但是效率很低)
2. 拉链法:定位到的桶被占用了,就在它后面链上一个新桶放入;
查找时,一路向后查找就行。
如果链的长度过长,hashmap的时间复杂度也会变高,就需要扩容
(通过存储的数量count和m的比值作为是否需要扩容的标准):
把旧桶中所有的k-v值全部迁移到新的桶里,如果hashmap中的数据过多,
迁移的代价就会变得很大。
所以会先分配足够多的新桶,再记录迁移的编号和进度,每次使用哈希表时就迁移一部分,直到所有数据都迁移完成。
(渐进式扩容:把大量的迁移时间分摊到多次进行,可以避免一次性扩容带来的性能波动)
go语言中的map:
map类型的变量本质是一个*hamp指针;
bmap的存储结构:
tophash [bucketCnt]uint8 存储着
8个key放一起
8个value放在一起
overflow指向下一个溢出桶
bmap(桶)把key-value分开存储的原因:
因为key的存储类型和value不一定相同,为了保证内存排列更加紧凑
hmap中mapextra结构是用来管理溢出桶的
map的扩容:
Golang的负载因子 count/(2^B) >6.5:
翻倍扩容,扩容分配桶的方法:
因为Golang的map同分配采用的是与运算,假设现在B为2;
0桶中的key的hash第八位的后两位一定是00,因为再之前分配的时候是hashval & 011;
那么现在B变成了3,即hashval & 0111,
因为已知后两位已经确定为00,所以只要判断第三位是0还是1就能知道是放入4号桶还是1号桶。
noverflow较多:等量扩容,键值对数量少但是创建的桶还有很多,这种情况一般是del操作比较多。
type hmap struct {
count int // 键值对的数量
flags uint8 // 状态标识,标识map的状态(如正在写、遍历、扩容等)
B uint8 // 桶0数,2^B
noverflow uint16 // 溢出桶的数量
hash0 uint32 // 哈希种子
buckets unsafe.Pointer // 桶的位置
oldbuckets unsafe.Pointer // 旧桶的位置
nevacuate uintptr // 迁移的进度
extra *mapextra
}
type bmap struct {
tophash [bucketCnt]uint8
// 因为哈希表中可能存储不同类型的键值对,且Golang不支持泛型(有时间看看1.18),
// 所以键值对占据的内存大小只能再编译中推导
// Followed by bucketCnt keys and then bucketCnt elems.
// Followed by an overflow pointer.
}
type mapextra struct {
overflow *[]*bmap // 已经使用的溢出桶
oldoverflow *[]*bmap // 扩容时期旧的溢出桶
nextOverflow *bmap // 下一个空闲溢出桶
}