1.Slice
Slice整体思路和Java里的ArrayList一致,区别在于Slice会存在共享底层数组的情况,这部分行为和ArrayList的SubList一致。
1.1.底层数据结构
1 | type slice struct { |
1.2.扩容机制
若所需容量大于当前容量的两倍就会使用期望容量;
对于容量扩展,1.18进行重构。
1.18版本之前(不含):若当前容量小于阈值1024,容量翻番;超过阈值之后,每次增加25%的容量,直到新容量满足期望容量;
1.18版本之后(含):若当前容量小于阈值256,容量翻番;超过阈值之后,每次增加(25%+192)的容量,直到新容量满足期望容量;
然后,当数据类型size为1字节,4/8字节,或者2的倍数时,会根据内存大小进行向上取整,进行内存对齐。
2.Map
Golang中的Map和Java一样,采用链地址法实现。Python采用开放地址法。
2.1.底层数据结构
1 | // A header for a Go map. |
数据存储在buckets中,每个bucket最多存8个kv对。哈希值的低位用于定位bucket,部分高位存在bucket中用于区分entry,若bucket中多于8个key,多余的kv存在extra中。
bucket中的kv不进行内部移动,防止map遍历过程中漏算或者多算。
bucket的内存分布:tophash + 8个kv对 + overflow指针
创建map时,当B>4(桶数量大于16)时,会预创建额外的溢出桶。
算法:默认多创建2^(b-4)个桶,但是最终的内存需要对其,对其后多余的桶也归到溢出桶里面。
内存对其算法与TCMalloc内存分配算法相关。
1 | // For small b, overflow buckets are unlikely. |
2.2.扩容复制
当map扩容时,bucket数组扩充两倍大小,然后渐进式从老bucket复制至新bucket数组。
扩充因子(loadFactor)为6.5, Java HashMap的扩充因子为0.75(对应Golang中应该是6),不过两者差别不大
1 | // loadFactor %overflow bytes/entry hitprobe missprobe |
2.3.查找
- 根据key计算出hash值
- 如果存在old table, 首先在old table中查找,如果找到的bucket已经evacuated,转到步骤3。 反之,返回其对应的value。
- 在new table中查找对应的value
其中,高8位不是用作offset的,而是用于加快key的比较的。
2.4. 插入过程分析
- 根据key算出hash值,进而得出对应的bucket。
- 如果bucket在old table中,将其重新散列到new table中,并额外迁移一个oldbucket。
- 在bucket中,如果已经存在需要插入的key,更新其对应的value;否则查找空闲的位置。
- 根据table中元素的个数,判断是否grow table。
- 如果对应的bucket已经full,重新申请新的bucket作为overbucket。
- 将key/value pair插入到bucket中。
若在扩容中,key对应oldbucket存在,则先将oldbucket迁移,
有点怪的地方:先找待更新的kv位置,然后再判断是否需要扩容,需要扩容的话,会重新查找kv位置。