go基础08-map的内部实现

和切片相比,map类型的内部实现要复杂得多。Go运行时使用一张哈希表来实现抽象的map类型。运行时实现了map操作的所有功能,包括查找、插入、删除、遍历等。在编译阶段,Go编译器会将语法层面的map操作重写成运行时对应的函数调用。

下面是大致的对应关系:

// $GOROOT/src/cmd/compile/internal/gc/walk.go
// $GOROOT/src/runtime/map.go
m := make(map[keyType]valType, capacityhint) → m := runtime.makemap(maptype, capacityhint, m)
v := m["key"] → v := runtime.mapaccess1(maptype, m, "key")
v, ok := m["key"] → v, ok := runtime.mapaccess2(maptype, m, "key")
m["key"] = "value" → v := runtime.mapassign(maptype, m, "key") // v是用于后续存储value 的空间的地址
delete(m, "key") → runtime.mapdelete(maptype, m, "key")

下图是map类型在运行时层实现的示意图。

在这里插入图片描述

1. 初始状态

从图上中我们可以看到,与语法层面map类型变量一一对应的是runtime.hmap类型的实例。hmap是map类型的header,可以理解为map类型的描述符,它存储了后续map类型操作所需的所有信息。

● count:当前map中的元素个数;对map类型变量运用len内置函数时,len函数返回的就是count这个值。

● flags:当前map所处的状态标志,目前定义了4个状态值——iterator、oldIterator、hashWriting和sameSizeGrow。

● B:B的值是bucket数量的以2为底的对数,即2^B = bucket数量。

● noverflow:overflow bucket的大约数量。

● hash0:哈希函数的种子值。

● buckets:指向bucket数组的指针。

● oldbuckets:在map扩容阶段指向前一个bucket数组的指针。

● nevacuate:在map扩容阶段充当扩容进度计数器。所有下标号小于nevacuate的bucket都已经完成了数据排空和迁移操作。

● extra:可选字段。如果有overflow bucket存在,且key、value都因不包含指针而被内联(inline)的情况下,该字段将存储所有指向overflow bucket的指针,保证overflow bucket是始终可用的(不被垃圾回收掉)。

真正用来存储键值对数据的是bucket(桶),每个bucket中存储的是Hash值低bit位数值相同的元素,默认的元素个数为BUCKETSIZ(值为8,在$GOROOT/src/cmd/compile/internal/gc/reflect.go中定义,与runtime/map.go中常量bucketCnt保持一致)。

当某个bucket(比如buckets[0])的8个空槽(slot)都已填满且map尚未达到扩容条件时,运行时会建立overflow bucket,并将该overflow bucket挂在上面bucket(如buckets[0])末尾的overflow指针上,这样两个bucket形成了一个链表结构,该结构的存在将持续到下一次map扩容。

每个bucket由三部分组成:tophash区域、key存储区域和value存储区域。

1)tophash区域

当向map插入一条数据或从map按key查询数据的时候,运行时会使用哈希函数对key做哈希运算并获得一个哈希值hashcode。这个hashcode非常关键,运行时将hashcode“一分为二”地看待,其中低位区的值用于选定bucket,高位区的值用于在某个bucket中确定key的位置。这个过程可参考下图。

在这里插入图片描述

因此,每个bucket的tophash区域是用于快速定位key位置的,这样避免了逐个key进行比较这种代价较大的操作,尤其是当key是size较大的字符串类型时,这是一种以空间换时间的思路。

2)key存储区域

tophash区域下面是一块连续的内存区域,存储的是该bucket承载的所有key数据。

运行时在分配bucket时需要知道key的大小。那么运行时是如何知道key的大小的呢?当我们声明一个map类型变量时,比如var m map[string]int,Go运行时就会为该变量对应的特定map类型生成一个runtime.maptype实例(如存在,则复用):

// $GOROOT/src/runtime/type.go
type maptype struct {
typ _type
key *_type
elem *_type
bucket *_type // 表示hash bucket的内部类型
keysize uint8 // key的大小
elemsize uint8 // elem的大小
bucketsize uint16 // bucket的大小
flags uint32
}

该实例包含了我们所需的map类型的所有元信息。前面提到过编译器会将语法层面的map操作重写成运行时对应的函数调用,这些运行时函数有一个共同的特点:

第一个参数都是maptype指针类型的参数。Go运行时就是利用maptype参数中的信息确定key的类型和大小的,map所用的hash函数也存放在maptype.key.alg.hash(key, hmap.hash0)中。

同时maptype的存在也让Go中所有map类型共享一套运行时map操作函数,而无须像C++那样为每种map类型创建一套map操作函数,从而减少了对最终二进制文件空间的占用。

运行该程序:

$go run map_concurrent_read_and_write.go
fatal error: concurrent map iteration and map write

我们会得到上述panic信息。如果仅仅是并发读,则map是没有问题的。

Go 1.9版本中引入了支持并发写安全的sync.Map类型,可以用来在并发读写的场景下替换掉map。另外考虑到map可以自动扩容,map中数据元素的value位置可能在这一过程中发生变化,因此Go不允许获取map中value的地址,这个约束是在编译期间就生效的。示例
代码如下:

p := m[key] // 无法获取m[key]的地址
fmt.Println(p)

尽量使用cap参数创建map

从上面的自动扩容原理我们了解到,如果初始创建map时没有创建足够多可以应付map使用场景的bucket,那么随着插入map元素数量的增多,map会频繁扩容,而这一过程将降低map的访问性能。

因此,如果可能的话,我们最好对map使用规模做出粗略的估算,并使
用cap参数对map实例进行初始化。下面是使用cap参数与不使用map参数的map写性能基准测
试及测试结果:


const mapSize = 10000
func BenchmarkMapInitWithoutCap(b *testing.B) {for n := 0; n < b.N; n++ {m := make(map[int]int)for i := 0; i < mapSize; i++ {m[i] = i}}
}
func BenchmarkMapInitWithCap(b *testing.B) {for n := 0; n < b.N; n++ {m := make(map[int]int, mapSize)for i := 0; i < mapSize; i++ {m[i] = i}
}
}

可以看出,使用cap参数的map实例的平均写性能是不使用cap参数的2倍。

goos: darwin
goarch: amd64
BenchmarkMapInitWithoutCap-8 2000 645946 ns/op 687188 B/op 276 allocs/op
BenchmarkMapInitWithCap-8 5000 317212 ns/op 322243 B/op 11 allocs/op
PASS
ok command-line-arguments 2.987s

和切片一样,map是Go语言提供的重要数据类型,也是Gopher日常编码中最常使用的类型之一。通过本条的学习我们掌握了map的基本操作和运行时实现原理,并且我们在日常使

用map的场合要把握住下面几个要点:

● 不要依赖map的元素遍历顺序;

● map不是线程安全的,不支持并发写;

● 不要尝试获取map中元素(value)的地址;

● 尽量使用cap参数创建map,以提升map平均访问性能,减少频繁扩容带来的不必要损耗

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/72119.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

YOLOV7改进-添加Deformable Conv V2

可变形卷积link class DCNv2(nn.Module):def __init__(self, in_channels, out_channels, kernel_size, stride1,padding1, groups1, actTrue, dilation1, deformable_groups1):super(DCNv2, self).__init__()self.in_channels in_channelsself.out_channels out_channelsse…

QT for andriod

QT for andriod 开发 apk软件&#xff0c;因为一些特殊的原因&#xff0c;在这里简单的记录一哈自己开发apk的流程和心得。 首先说明我采用的环境有哪些&#xff1f; 1、QT的版本&#xff0c;个人建议5.15.2的版本及以上&#xff0c;我是用的5.15.2。 2、andriod studio 可以…

3D数据导出工具HOOPS Publish:3D数据查看、生成标准PDF或HTML文档!

HOOPS中文网http://techsoft3d.evget.com/ 一、3D导出SDK HOOPS Publish是一款功能强大的SDK&#xff0c;可以创作丰富的工程数据并将模型文件导出为各种行业标准格式&#xff0c;包括PDF、STEP、JT和3MF。HOOPS Publish核心的3D数据模型是经过ISO认证的PRC格式(ISO 14739-1:…

STM32移植FAT文件系统

所谓“移植”&#xff0c;就是打通FAT源码和物理设备之间的软件接口。 FAT源码早就被公益组织给写好了&#xff0c;直接下载源码。但是FAT作为顶层应用程序&#xff0c;它需要面对的底层物理设备是不确定的&#xff0c;那么底层的物理设备驱动程序就需要程序员来自己写。物理设…

Android:基于mvvm框架使用viewPage

一、前言&#xff1a; 最近在学习viewpage的使用&#xff0c;加上一直以来用mvvm框架。就想着记录一下。 二、代码展示&#xff1a; 1.引入依赖 //viewPage2引用(微信左右滑动页面)implementation androidx.viewpager2:viewpager2:1.0.0 2.在xml中的使用 3.在代码中找到vie…

脚本:python实现樱花树

文章目录 代码效果 代码 from turtle import * from random import * from math import * def tree(n, l):pd () # 下笔# 阴影效果t cos ( radians ( heading () 45 ) ) / 8 0.25pencolor ( t, t, t )pensize ( n / 3 )forward ( l ) # 画树枝if n > 0:b random () *…

算法通关村第12关【黄金】| 字符串冲刺题

1.最长公共前缀 思路&#xff1a;纵向比较&#xff0c;每个字符串从头挨个比较 class Solution {public String longestCommonPrefix(String[] strs) {StringBuilder sb new StringBuilder();for(int i 0;i<strs[0].length();i){char c strs[0].charAt(i);for(int j 1;j…

Qt实现图书管理系统(C++)

文章目录 数据库表的实现创建表将powerDesigner里面的表导出成xxx.sql脚本将SQL文件导入数据库创建表 图书管理系统思维导图创建工程开发阶段创建Dlg_login登录页面login页面样式主页页面布局主函数测试login设置logo打包程序子页面的样子将子页面放到StackedWidget里面按钮直接…

Python学习 -- logging模块

logging 模块是 Python 中用于记录日志的标准库&#xff0c;它提供了丰富的功能&#xff0c;可以帮助开发者进行日志记录和管理。以下是关于logging模块的详细使用方式&#xff0c;包括日志级别、处理流程、Logger 类、Handler 类、Filter 类、Formatter 类以及模块中常用函数等…

shell入门运算符操作、条件判断

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a; 小刘主页 ♥️努力不一定有回报&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️学习两年总结出的运维经验&#xff0c;以及思科模拟器全套网络实验教程。专栏&#xf…

PCIe 5.0验证实战,经常遇到的那些问题?

PCIe 5.0是当前最新的PCI Express规范&#xff0c;提供了更高的数据传输速率和更大的带宽。 PCIe是连接两个芯片的接口&#xff0c;负责两个芯片通信, 连接芯片的通路为高速SerDes, 称之为链路。PCIe确保通路正常-链路训练状态机。PCIe在芯片内部是非常重要的一个大的模块&…

YOLOv5改进算法之添加CA注意力机制模块

目录 1.CA注意力机制 2.YOLOv5添加注意力机制 送书活动 1.CA注意力机制 CA&#xff08;Coordinate Attention&#xff09;注意力机制是一种用于加强深度学习模型对输入数据的空间结构理解的注意力机制。CA 注意力机制的核心思想是引入坐标信息&#xff0c;以便模型可以更好地…

大数据课程K20——Spark的SparkSQL概述

文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 了解Spark的SparkSQL由来; ⚪ 了解Spark的SparkSQL特点; ⚪ 了解Spark的SparkSQL优势; ⚪ 掌握Spark的SparkSQL入门; 一、SparkSQL概述 1. 概述 Spark为结构化数据处理引入了一个称…

STM32单片机OLED贪吃蛇游戏记分计时

实践制作DIY- GC00165---OLED贪吃蛇游戏 一、功能说明&#xff1a; 基于STM32单片机设计---OLED贪吃蛇游戏 二、功能说明&#xff1a; STM32F103C系列最小系统板0.96寸OLED显示器上、下、左、右4个按键 1.通过OLED配合按键实现贪吃蛇游戏 2.可以上下左右移动。 3.可以统计显…

golang-bufio 缓冲写

1. 缓冲写 在阅读这篇博客之前&#xff0c;请先阅读上一篇&#xff1a;golang-bufio 缓冲读 // buffered output// Writer implements buffering for an io.Writer object. // If an error occurs writing to a Writer, no more data will be // accepted and all subsequent…

搭建vue3项目并git管理

搭建vue3项目 采用vue3的create-vue脚手架搭建项目&#xff0c;底层是vite&#xff0c;要求环境 node 16.0及以上&#xff08;node -v检查node版本&#xff09; 在文件夹右键->终端-> npm init vuelatest&#xff0c;输入项目名称&#xff0c;根据需要选择是否装包 src…

04 卷积神经网络搭建

一、数据集 MNIST数据集是从NIST的两个手写数字数据集&#xff1a;Special Database 3 和Special Database 1中分别取出部分图像&#xff0c;并经过一些图像处理后得到的[参考]。 MNIST数据集共有70000张图像&#xff0c;其中训练集60000张&#xff0c;测试集10000张。所有图…

deepstream6.2部署yolov5详细教程与代码解读

文章目录 引言一.环境安装1、yolov5环境安装2、deepstream环境安装 二、源码文件说明三.wts与cfg生成1、获得wts与cfg2、修改wts 四.libnvdsinfer_custom_impl_Yolo.so库生成五.修改配置文件六.运行demo 引言 DeepStream 是使用开源 GStreamer 框架构建的优化图形架构&#xf…

cesium创建基本的实体、点、线、多边形(vue)

1.通过viewer实例的entities对象实现 实现代码&#xff1a; <template><div id"container"></div> </template><script> import * as Cesium from cesium/Cesium import "cesium/Widgets/widgets.css" export default {mo…

LeetCode刷题笔记【25】:贪心算法专题-3(K次取反后最大化的数组和、加油站、分发糖果)

文章目录 前置知识1005.K次取反后最大化的数组和题目描述分情况讨论贪心算法 134. 加油站题目描述暴力解法贪心算法 135. 分发糖果题目描述暴力解法贪心算法 总结 前置知识 参考前文 参考文章&#xff1a; LeetCode刷题笔记【23】&#xff1a;贪心算法专题-1&#xff08;分发饼…