Go中MAP底层原理分析

article/2025/8/13 20:40:41

MAP底层原理分析

参考

  1. https://golang.design/go-questions/map/principal
  2. map | Golang 中文学习文档

  1. 先来看一下map结构体,(runtime.hmap结构体就是代表着 go 中的map,与切片一样map的内部实现也是结构体)

    type hmap struct {// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.// Make sure this stays in sync with the compiler's definition.count     int // # live cells == size of map.  Must be first (used by len() builtin)flags     uint8B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for detailshash0     uint32 // hash seedbuckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growingnevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)clearSeq   uint64extra *mapextra // optional fields
    }
    

    内部实现图

在这里插入图片描述

简单说明一下各个字段含义

  • count:表示 hamp 中的元素数量,结果等同于len(map)

  • flags : hmap 的标志位,用于表示 hmap 处于什么状态,有以下几种可能

    const (iterator     = 1 // 迭代器正在使用桶oldIterator  = 2 // 迭代器正在使用旧桶hashWriting  = 4 // 一个协程正在写入hmapsameSizeGrow = 8 // 正在等量扩容
    )
    
  • B:hmap 中的哈希桶的数量为1 << B

  • noverflow,hmap 中溢出桶的大致数量。

  • 哈希种子,在 hmap 被创建时指定,用于计算哈希值。

  • buckets,存放哈希桶数组的指针。

  • oldbuckets,存放 hmap 在扩容前哈希桶数组的指针。

  • extra,存放着 hmap 中的溢出桶,溢出桶指的是就是当前桶已经满了,创建新的桶来存放元素,新创建的桶就是溢出桶。

    type mapextra struct {// 溢出桶的指针切片overflow    *[]*bmap// 扩容前旧的溢出桶的指针切片oldoverflow *[]*bmap// 指向下一个空闲的溢出桶的指针nextOverflow *bmap
    }
    
  1. 一直说的桶是个什么东西 ?

    hamp 中的buckets也就是桶切片指针,在 go 中对应的结构为runtime.bmap,如下所示

    // A bucket for a Go map.
    type bmap struct {tophash [abi.OldMapBucketCount]uint8
    }
    // OldMapBucketCount是如下的这个变量
    // Maximum number of key/elem pairs a bucket can hold.
    OldMapBucketCountBits = 3 // log2 of number of elements in a bucket.
    OldMapBucketCount     = 1 << OldMapBucketCountBits // 默认为8
    

    表面上看,它只有一个字段tophash,不过实际上来说,bmap的字段不止这些,这是因为map可以存储各种类型的键值对,所以需要在编译时根据类型来推导占用的内存空间,在cmd/compile/internal/reflectdata/reflect.go中的MapBucketType函数的功能就是在编译时构造 bmap,它会进行一系列检查工作,比如 key 的类型是否comparable

    // MapBucketType makes the map bucket type given the type of the map.
    func MapBucketType(t *types.Type) *types.Type
    

    bmap内部结构:HOB Hash 指的就是 top hash。

在这里插入图片描述

实际上bmap结构如下;不过这些字段对我们是不可见的,go 在实际操作中是通过移动 unsafe 指针来进行访问

// 实际运行时定义(简化为可理解的结构)
type bmap struct {// 哈希值高8位数组 (每个槽位1字节)tophash [abi.OldMapBucketCount]uint8  // bucketCnt = 8// 以下是隐式字段(编译器在编译时根据键值类型动态生成内存布局)keys    [abi.OldMapBucketCount]keyType   // 键数组(连续存储)elems   [abi.OldMapBucketCount]elemType  // 值数组(连续存储)注意:1.17+ 改名为 elems// 溢出桶指针(重要:在 keys/elems 之后存储)overflow *bmap
}
// 注意:有些文章还有pad字段 ,该字段是为了保持内存对齐,提高一些操作的效率的

tophash字段是什么 ?

首先,根据字面意思就是一个为uint8类型的长度为 8的数组 ,此时也就定义了 一个桶中可以存放8个键值对,桶中每个元素是uint8,代表每个键(key)对应的hash值得高八位, 用于定位该键(key)在本桶的位置。

另外,tophash还有一些特殊的值,通常是处于扩容的迁移状态下:

const (emptyRest      = 0 // 当前元素是空的,并且该元素后面也没有可用的键值了emptyOne       = 1 // 当前元素是空的,但是该元素后面有可用的键值。evacuatedX     = 2 // 扩容时出现,只能出现在oldbuckets中,表示当前元素被搬迁到了新哈希桶数组的上半区evacuatedY     = 3 // 扩容时出现只能出现在oldbuckets中,表示当前元素被搬迁到了新哈希桶数组的下半区evacuatedEmpty = 4 // 扩容时出现,元素本身就是空的,在搬迁时被标记minTopHash     = 5 // 对于一个正常的键值来说tophash的最小值
)
  • minTopHash:当一个 cell(每个tophash元素抽象成每一个cell) 的 tophash 值小于 minTopHash 时,标志这个 cell 的迁移状态。因为这个状态值是放在 tophash 数组里,为了和正常的哈希值区分开,会给 key 计算出来的哈希值一个增量:minTopHash。这样就能区分正常的 top hash 值和表示状态的哈希值
  • 其余变量是迁移过程中用到的 ,后续说明
  1. key定位过程

    参考文章中说明的很好:


    key 经过哈希计算后得到哈希值,共 64 个 bit 位(64位机,32位机就不讨论了,现在主流都是64位机),计算它到底要落在哪个桶时,只会用到最后 B 个 bit 位。还记得前面提到过的 B 吗?如果 B = 5,那么桶的数量,也就是 buckets 数组的长度是 2^5 = 32。

    例如,现在有一个 key 经过哈希函数计算后,得到的哈希结果是:

     10010111 | 000011110110110010001111001010100010010110010101010 │ 01010
    

    用最后的 5 个 bit 位,也就是 01010,值为 10,也就是 10 号桶。这个操作实际上就是取余操作,但是取余开销太大,所以代码实现上用的位操作代替。

    再用哈希值的高 8 位,找到此 key 在 bucket 中的位置,这是在寻找已有的 key。最开始桶内还没有 key,新加入的 key 会找到第一个空位,放入。

在这里插入图片描述

上图中,假定 B = 5,所以 bucket 总数就是 2^5 = 32。首先计算出待查找 key 的哈希,使用低 5 位 00110,找到对应的 6 号 bucket,使用高 8 位 10010111,对应十进制 151,在 6 号 bucket 中寻找 tophash 值(HOB hash)为 151 的 key,找到了 2 号槽位,这样整个查找过程就结束了。

如果在 bucket 中没找到,并且 overflow 不为空,还要继续去 overflow bucket 中寻找,直到找到或是所有的 key 槽位都找遍了,包括所有的 overflow bucket。


keyvalue的定位方法

// key 定位公式
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))// value 定位公式
e := add(unsafe.Pointer(b), dataOffset+abi.OldMapBucketCount*uintptr(t.KeySize)+i*uintptr(t.ValueSize))

b 是 bmap 的地址,这里 bmap 还是源码里定义的结构体,只包含一个 tophash 数组,经编译器扩充之后的结构体才包含 key,value,overflow 这些字段。dataOffset 是 key 相对于 bmap 起始地址的偏移, 也就是bmap中key地址开始的位置:

dataOffset = unsafe.Offsetof(struct {b bmapv int64}{}.v)

当定位到一个具体的 bucket 时,里层循环就是遍历这个 bucket 里所有的 cell,或者说所有的槽位,也就是 bucketCnt=8 个槽位。整个循环过程:

在这里插入图片描述

遍历过程中会判断每一个cell的状态 ,判断这个bucket是否搬迁完毕,源码中用到的函数

func evacuated(b *bmap) bool {h := b.tophash[0]return h > empty && h < minTopHash
}

只取了 tophash 数组的第一个值,判断它是否在 0-4 之间。对比上面的常量,当 top hash 是 evacuatedEmptyevacuatedXevacuatedY 这三个值之一,说明此 bucket 中的 key 全部被搬迁到了新 bucket。


http://www.hkcw.cn/article/ZQyRHppPQx.shtml

相关文章

第十六章 EMQX黑名单与连接抖动检测

系列文章目录 第一章 总体概述 第二章 在实体机上安装ubuntu 第三章 Windows远程连接ubuntu 第四章 使用Docker安装和运行EMQX 第五章 Docker卸载EMQX 第六章 EMQX客户端MQTTX Desktop的安装与使用 第七章 EMQX客户端MQTTX CLI的安装与使用 第八章 Wireshark工具的安装与使用 …

构建系统maven

1 前言 说真的&#xff0c;我是真的不想看构建了&#xff0c;因为真的太多了。又多又乱。Maven、Gradle、Make、CMake、Meson、Ninja&#xff0c;Android BP。。。感觉学不完&#xff0c;根本学不完。。。 但是没办法最近又要用一下Maven&#xff0c;所以咬着牙再简单整理一下…

java CountDownLatch‌

CountDownLatch是用于线程同步的工具类&#xff0c;主要作用是让当前线程等待其他线程完成操作后再继续执行。 示例代码&#xff1a; import java.util.concurrent.CountDownLatch;private static void testCountDownLatch() {int taskNum 5;CountDownLatch latch new Count…

[yolov11改进系列]基于yolov11引入上下文锚点注意力CAA的python源码+训练源码

【CAA介绍】 本文记录的是基于CAA注意力模块的RT-DETR目标检测改进方法研究。在远程遥感图像或其他大尺度变化的图像中目标检测任务中&#xff0c;为准确提取其长距离上下文信息&#xff0c;需要解决大目标尺度变化和多样上下文信息时的不足的问题。CAA能够有效捕捉长距离依赖…

【Java基础】Java入门教程

文章目录 一、Java开发环境概述☕ Java开发全景架构&#x1f4e6; JDK (Java Development Kit)&#x1f5a5;️ IDE (集成开发环境)&#x1f504; 工作流关系 二、JDK下载与安装2.1 下载JDK2.2 安装JDK 三、环境变量配置3.1 Windows配置3.2 macOS/Linux配置为当前用户配置环境变…

通过openpyxl在excel中插入散点图

实现代码 # -*- coding: utf-8 -*- """ Created on Sat May 31 23:30:12 2025author: anyone """from openpyxl import load_workbook from openpyxl.chart import ScatterChart, Reference, Series from openpyxl.chart.series import SeriesL…

零基础安装 Python 教程:从下载到环境配置一步到位(支持 VSCode 和 PyCharm)与常用操作系统操作指南

零基础安装 Python 教程&#xff1a;从下载到环境配置一步到位&#xff08;支持 VSCode 和 PyCharm&#xff09;与常用操作系统操作指南 本文是一篇超详细“Python安装教程”&#xff0c;覆盖Windows、macOS、Linux三大操作系统的Python安装方法与环境配置&#xff0c;包括Pyt…

数据结构第6章 图(竟成)

第 6 章 图 【考纲内容】 1.图的基本概念 2.图的存储及基本操作&#xff1a;(1) 邻接矩阵法&#xff1b;(2) 邻接表法&#xff1b;(3) 邻接多重表、十字链表 3.图的遍历&#xff1a;(1) 深度优先搜索&#xff1b;(2) 广度优先搜索 4.图的基本应用&#xff1a;(1) 最小 (代价) 生…

Microsoft Fabric - 尝试一下Data Factory一些新的特性(2025年5月)

1.简单介绍 Microsoft Fabric是微软提供的一个数据管理和分析的统一平台&#xff0c;感觉最近的新特性也挺多的。 Data Factory是Microsoft Fabric的一个功能模块&#xff0c;也是一个cloud service。Data Factory可以和多种数据源进行连接&#xff0c;同时提供了data movemen…

思科设备网络实验

一、 总体拓扑图 图 1 总体拓扑图 二、 IP地址规划 表格 1 接口地址规划 设备名称 接口/VLAN IP 功能 PC0 VLAN580 10.80.1.1 访问外网 PC1 VLAN581 10.80.2.1 访问外网 PC2 Fa0 20.80.1.100 端口镜像监控流量 PC3 VLAN585 10.80.6.1 远程登陆多层交换机0…

《机器学习数学基础》补充资料:韩信点兵与拉格朗日插值法

本文作者&#xff1a;卓永鸿 19世纪的伟大数学家高斯&#xff0c;他对自己做的数学有非常高的要求&#xff0c;未臻完美不轻易发表。于是经常有这样的情况&#xff1a;其他也很厉害的数学家提出自己的工作&#xff0c;高斯便拿出自己的文章说他一二十年前就做出来了&#xff0…

Go 即时通讯系统:日志模块重构,并从main函数开始

重构logger 上次写的logger.go过于繁琐&#xff0c;有很多没用到的功能&#xff1b;重构后只提供了简洁的日志接口&#xff0c;支持日志轮转、多级别日志记录等功能&#xff0c;并采用单例模式确保全局只有一个日志实例 全局变量 var (once sync.Once // 用于实现…

力扣面试150题--二叉树的锯齿形层序遍历

Day 56 题目描述 思路 锯齿形就是一层是从左向右&#xff0c;一层是从右向左&#xff0c;那么我们可以分析样例&#xff0c;对于第奇数层是从左向右&#xff0c;第偶数层是从右向左&#xff0c;于是可以采取一个计数器&#xff0c;采取链表方式&#xff0c;从左向右就是正常插…

uni-app学习笔记二十一--pages.json中tabBar设置底部菜单项和图标

如果应用是一个多 tab 应用&#xff0c;可以通过 tabBar 配置项指定一级导航栏&#xff0c;以及 tab 切换时显示的对应页。 在 pages.json 中提供 tabBar 配置&#xff0c;不仅仅是为了方便快速开发导航&#xff0c;更重要的是在App和小程序端提升性能。在这两个平台&#xff…

Vue3+SpringBoot全栈开发:从零实现增删改查与分页功能

前言 在现代化Web应用开发中&#xff0c;前后端分离架构已成为主流。本文将详细介绍如何使用Vue3作为前端框架&#xff0c;SpringBoot作为后端框架&#xff0c;实现一套完整的增删改查(CRUD)功能&#xff0c;包含分页查询、条件筛选等企业级特性。 技术栈介绍 前端&#xff1…

用户资产化视角下开源AI智能名片链动2+1模式S2B2C商城小程序的应用研究

摘要&#xff1a;在数字化时代&#xff0c;平台流量用户尚未完全转化为企业的数字资产&#xff0c;唯有将其沉淀至私域流量池并实现可控、随时触达&#xff0c;方能成为企业重要的数字资产。本文从用户资产化视角出发&#xff0c;探讨开源AI智能名片链动21模式S2B2C商城小程序在…

用dayjs解析时间戳,我被提了bug

引言 前几天开发中突然接到测试提的一个 Bug&#xff0c;说我的时间组件显示异常。 我很诧异&#xff0c;这里初始化数据是后端返回的&#xff0c;我什么也没改&#xff0c;这bug提给我干啥。我去问后端&#xff1a;“这数据是不是有问题&#xff1f;”。后端答&#xff1a;“…

适配器模式:让不兼容接口协同工作

文章目录 1. 适配器模式概述2. 适配器模式的分类2.1 类适配器2.2 对象适配器 3. 适配器模式的结构4. C#实现适配器模式4.1 对象适配器实现4.2 类适配器实现 5. 适配器模式的实际应用场景5.1 第三方库集成5.2 遗留系统集成5.3 系统重构与升级5.4 跨平台开发 6. 类适配器与对象适…

多模态AI的企业应用场景:视觉+语言模型的商业价值挖掘

关键词&#xff1a;多模态AI | 视觉语言模型 | 企业应用 | 商业价值 | 人工智能 &#x1f4da; 文章目录 一、引言&#xff1a;多模态AI时代的到来二、多模态AI技术架构深度解析三、客服场景&#xff1a;智能化服务体验革命四、营销场景&#xff1a;精准投放与创意生成五、研…

设备驱动与文件系统:01 I/O与显示器

操作系统设备驱动学习之旅——以显示器驱动为例 从这一节开始&#xff0c;我要学习操作系统的第四个部分&#xff0c;就是i o设备的驱动。今天要讲的是第26讲&#xff0c;内容围绕i o设备中的显示器展开&#xff0c;探究显示器是如何被驱动的&#xff0c;也就是操作系统怎样让…