map的实现方式有多种,比如哈希表、红黑树等。Golang的map底层是用哈希表实现的,在介绍Golang map的实现之前首先介绍一下哈希表的两种telegram的下载方法在哪里实现方式:

  1.1、开放寻址法

  开放寻址法,底层是一个数组,每个数组都存放一个键值对,空闲的地方就是没有放键值对的地方。首先看如何插入:key-val首先经过一个哈希函数将key值进行哈希得到一个大的数字,然后将其对数组的长度取模,这样它就落在了数组中的一个位置。如果得到的位置没有被占用,那么就直接存放在对应位置。如果已经被占用了,那么就向后寻找一个槽,直到找到空闲的槽。读取也是同样的步骤,先哈希再取模,然后去对应的槽位寻找,如果没有找到,就向后找。

   telegram的下载地方在哪呢在这里插入图片描述

  1.2、拉链法

  拉链法前两个步骤一样,也是先哈希再取模,然后会落到数组的一个槽中(每个槽并不存放k-v数据,它们都是指针),然后使用链表将k-v连接起来。查询的时候,获取槽位后,遍历链表来查询。

  在这里插入图片描述

  Go语言的map是用拉链法来实现的,但是与上面所说的也有所不同。

  2.1 map的结构

  map的底层为一个结构体,结构体如下:

  map结构的图表示为如下:

  map的底层为hmap结构体,其中包含了很多字段,buckets是一个数组,大小是2的B次方个。数组中的每一个是一个bmap结构体的哈希桶,bmap包含了4个字段,后面三个字段在编译时才能确定。tophash、keys、elems都是大小为8的数组,它们每个元素一一对应。

  tophash存放哈希值的最高字节,keys存放了键,elems存放数据。

  每个桶只能存放最多8个键值对,因此当某个桶不够用时,就会使用溢出桶,溢出桶是和普通桶在一起的,都是在buckets指向的bmap数组中,。overflow指向下一个溢出桶。

  Go语言的map并没有将键值对一起存放,而是将键值分开存放。是因为如果存放在一起可能需要内存对齐,会导致空间的浪费telegram中文的下载的网址在哪呢,存放在一起会使内存更紧凑,也不会损失什么性能。

  在这里插入图片描述

  2.2 map的初始化

  我们在使用map的时候的初始化方式主要有两种:一种是使用字面量初始化,一种是使用make

  接下来我们看这两种初始化方式在底层到底是怎么做的:telegram的的官网下载地方在哪里

  首先看一段代码,使用 go build -gcflags -S main.go 来生成汇编代码

  生成的汇编文件的一段如下:

  在这里插入图片描述

  我们可以看到,这两种初始化方式分别调用了runtime.makemap_small() 和 runtime.makemap(),但不管是字面量初始化还是make初始化,如果初始化的键值对数量比较小就会使用makemap_small,否则就使用makemap()。经过测试,当初始化的数量大于8时,也就是字面量中的键值对数量大于8或make中的第二个参数大于8,就会使用makemap()来进行初始化。

  接下来我们找到这两个函数(其中省略了一些暂时不关注的内容):
telegram的的官网最新的下载网站是什么
  我们可以看到,它直接new了一个hmap,然后返回hmap的指针。与Go中的切片不同,切片是返回一个结构体,而map返回的是结构体的指针。

  new了一个hmap的结构体,然后根据传入的数据的数量来算出需要的B的数量。然后进行哈希桶的内存分配,它还会多创建一些溢出桶,extra结构体的nextoverflow字段保存了这些溢出桶,最后返回hmap的指针。

  创建哈希桶数组,根据b来计算数组的大小,如果b>=4,还会创建一些溢出桶,溢出桶的数量为 1 << (b – 4)。计算完后,创建数组,然后返回普通桶和溢出桶的地址。

  在创建map的时候,首先要确定B,假设B为3,那么桶的数量就为2^3=8,其次,如果B >= 4,也会创建一些溢出桶。然后创建mapextra类型的结构体,其中的nextoverflow保存了下一个可用的溢出桶的地址。

  假设位于一号槽的桶已经用满了,那么就会使用extra字段来寻找一个新的可用的溢出桶,然后使用bmap中的overflow字段指向溢出桶来组成一个链表。

  在这里插入图片描述

  map字面量赋值的两种方式

  当元素少于25个时,转化为简单赋值:

  在这里插入图片描述

  元素多于25个时,转化为循环赋值:

  在这里插入图片描述

  2.3 map的访问

  map的访问,首先要获取桶号,然后循环匹配该桶和溢出桶中的tophash的值,每个桶中的tophash没有保存哈希值的全部,而是保存了高八位,是为了快速遍历。匹配成功,还要验证key值是否相等,如果相等就说明找到了telegram的的官网的最新下载的地方怎么找。

  在访问map时有两种返回值:第一种,只获取值;第二种,获取值和是否查询到
telegram的官网的最新地方是多少
  源码如下:

  首先要计算桶号:

  将a与hash0一同进行哈希运算,输出一长串哈希值。然后根据B来取二进制哈希值的后B位,得到的值即为桶号,假设B为3,那么取后三位为010b也就是2,那么桶号就是2

  在这里插入图片描述

  然后计算tophash

  取哈希值的高八位作为tophash,16进制为0x5c

  在这里插入图片描述

  匹配

  算出tophash后,再tophash数组中进行一一匹配,如果没有找到,则查看overflow是否为nil,不为nil则匹配溢出桶中的tophash,直到匹配成功或者到最后也找不到。如果找到了,也不一定是我们想要的,因为如果哈希碰撞的比较厉害,一个桶中的tophash可能有相等的,因此需要再进行key值的比较,如果相同,就找到了相匹配的键值对。如果key值不相同,就继续从tophash中找。

  在这里插入图片描述

  2.4 map的插入

  map的插入首先要查找要插入的key是否已经存在,如果存在就更新新的value。如果不存在,就插入一条记录。

  源码如下:

  map的插入步骤:

  首先根据key和hash0计算桶号以及tophash,然后在哈希桶中根据tophash查找,如果一个位置的tophash为"空"(tophash

  <= 1),说明该位置以及后面都为空,没有该key。因此直接将k-v存放在此处即可。如果匹配到了相同的tophash,还要对比key是否相等。key值相等,就直接修改val。如果key不相等,继续查找,直到找不到,如果找不到就找一个空位用来存放数据。判断是否找到了空位,如果没有找到,需要创建一个溢出桶,将数据存放入溢出桶中。

  2.5 map的删除

  map的删除首先要查找key值是否存在,如果存在,就删除key-val,如果val中存在指针,就需要删除,因为需要解除对该指针的引用,以便垃圾回收器回收垃圾,否则就不用删除。

  map的删除步骤:

  前面的步骤都差不多,计算桶号、tophash,然后在哈希桶中查找tophash,找到后,再对比key值。最终,如果找到key,就删除k-v,重置对应的tophash。

  2.6 map的清空

  map的清空方式有两种:1、make一个新的map;2、使用for range一个一个删除

  第一种会解除上一个map的引用,然后重新创建一个map,垃圾回收器会自动回收旧的map。

  第二种会被go的编译器进行优化,调用mapclear函数。

  在这里插入图片描述

  mapclear的源码如下:

  就是重置了其中的一些字段,然后新建了一个bucket数组

  那么哪个的速度更快呢?我们可以进行一个基准测试:

  C = 1000, 10000, 100000 时的测试数据如下

  可以看到,使用for range的方式更快。

  2.7 总结

  Go语言使用拉链法实现了map在哈希桶中,使用长度为8的数组来存放tophash以及key和value每个哈希桶超过8个数据,就会创建新的溢出桶来存放数据,溢出桶逻辑上组成一个链表

  map为什么需要扩容?

  首先就是当可用空间不足时就需要扩容。其次当哈希碰撞比较严重时,很多数据都会落在同一个桶中,那么就会导致越来越多的溢出桶被链接起来。这样的话,查找的时候最坏的情况就是要遍历整个链表,时间复杂度很高,效率很低。而且当删除了很多元素后,可能会导致虽然有很多溢出桶,但是桶中的元素很稀疏。

  达到最大的负载因子(6.5,也就是平均每个桶中k-v的数量大于6.5)溢出桶的数量太多

  在mapassign中会判断是否要扩容:

  map溢出桶太多时会导致严重的性能下降runtime.mapassign()可能会触发扩容的情况:

  负载因子超过6.5(平均每个槽6.5个key)使用了太多的溢出桶(溢出桶超过了普通桶)

  等量扩容:数据不多但是溢出桶太多了(整理)翻倍扩容:数据太多了

  等量扩容,溢出桶太多了,导致查询效率低。扩容时,桶的数量不增加。

  翻倍扩容,每个桶的k-v太多了,需要增加普通桶的数量,扩容后桶的数量为原来的两倍。

  3.1 map扩容的步骤

  步骤1:创建新桶

  创建一组新桶oldbuckets指向原有的桶数组buckets指向新的桶的数组map标记为扩容状态

  在这里插入图片描述

  代码如下:

  步骤2:迁移数据

  将所有的数据从旧桶驱逐到新桶采用渐进式驱逐每次操作一个旧桶时(插入、删除数据),将旧桶数据驱逐到新桶读取时不进行驱逐,只判断读取新桶还是旧桶

  在每次插入或删除数据时,会判断map是否在扩容,如果在扩容就需要执行一些扩容的工作,也就是将旧桶中的数据驱逐到新桶,会驱逐当前要操作的桶及其溢出桶。

  假设原来的桶的数量为4,那么B为2。当进行翻倍扩容后,桶的数量为8,B为3。那么这里就体现出了使用B的智慧,扩容后B为3,那么取哈希值的二进制后三位就有两种情况:010b和110b,分别是2和6。所以原来二号桶中的数据就会分布到新桶数组的2号和6号桶中。

  在这里插入图片描述

  在这里插入图片描述

  源码为:
telegram官网的最新的下载的网站在哪呢
  步骤3:回收旧桶

  所有旧桶驱逐完成后,回收oldbuckets

  3.2 扩容时访问数据

  在map扩容期间访问map需要确定数据是在旧桶还是在新桶中,也就是判断数据是否已经被疏散到新桶中。

  在mapaccess1中:

  3.3 总结

  负载因子太大或者溢出桶太多,会触发map扩容”扩容”可能并不是增加桶数,而是整理map扩容采用渐进式,桶被操作时才会重新分配

seo