Go語言Map的底層原理

概念

map 又稱字典,是一種常用的數據結構,核心特征包含下述三點:

(1)存儲基于 key-value 對映射的模式;

(2)基于 key 維度實現存儲數據的去重;

(3)讀、寫、刪操作控制,時間復雜度 O(1).

****key 的類型要求
map 中,key 的數據類型必須為可比較的類型,切片、map、func不可比較

指針類型是可以比較的。

如果是結構體會怎么樣?

結構體中的所有字段的類型都必須是可比較的類型的才能作為map的key

type student struct {name stringage  int
}func TestMap(t *testing.T) {var m map[student]stringm = map[student]string{student{"Jane", 20}: "Jane",}t.Log(m)
}

同理數組也是一樣。

遍歷

在執行 map 遍歷操作時,獲取的 key-value 對并沒有一個固定的順序,因此前后兩次遍歷順序可能存在差異.

并發沖突

map 不是并發安全的數據結構,倘若存在并發讀寫行為,會拋出 fatal error.

具體規則是:

(1)并發讀沒有問題;

(2)并發讀寫中的“寫”是廣義上的,包含寫入、更新、刪除等操作;

(3)讀的時候發現其他 goroutine 在并發寫,拋出 fatal error;

(4)寫的時候發現其他 goroutine 在并發寫,拋出 fatal error.

fatal("concurrent?map?read?and?map?write")
fatal("concurrent?map?writes")

需要關注,此處并發讀寫會引發 fatal error,是一種比 panic 更嚴重的錯誤,無法使用 recover 操作捕獲.

核心原理

map 又稱為 hash map,在算法上基于 hash 實現 key 的映射和尋址;在數據結構上基于桶數組實現 key-value 對的存儲.

以一組 key-value 對寫入 map 的流程為例進行簡述:

(1)通過哈希方法取得 key 的 hash 值;

(2)hash 值對桶數組長度取模,確定其所屬的桶;

(3)在桶中插入 key-value 對.

hash 的性質,保證了相同的 key 必然產生相同的 hash 值,因此能映射到相同的桶中,通過桶內遍歷的方式鎖定對應的 key-value 對.

因此,只要在宏觀流程上,控制每個桶中 key-value 對的數量,就能保證 map 的幾項操作都限制為常數級別的時間復雜度.

Hash

hash 譯作散列,是一種將任意長度的輸入壓縮到某一固定長度的輸出摘要的過程,由于這種轉換屬于壓縮映射,輸入空間遠大于輸出空間(這里的壓縮是將無限的輸入空間壓縮成有限的輸出域),因此不同輸入可能會映射成相同的輸出結果. 此外,hash在壓縮過程中會存在部分信息的遺失,因此這種映射關系具有不可逆的特質.

(1)hash 的可重入性:相同的 key,必然產生相同的 hash 值;

(2)hash 的離散性:只要兩個 key 不相同,不論其相似度的高低,產生的 hash 值會在整個輸出域內均勻地離散化;

(3)hash 的單向性:企圖通過 hash 值反向映射回 key 是無跡可尋的.

(4)hash 沖突:由于輸入域(key)無窮大,輸出域(hash 值)有限,因此必然存在不同 key 映射到相同 hash 值的情況,稱之為 hash 沖突.

可能會感到有點沖突,但(2)和(4)并不相矛盾,離散型好的哈希函數只是減少沖突的概率,并不能完全避免。

桶數組

map中,會通過長度為2的整數次冪的桶數組進行 key-value 對的存儲:

(1)每個桶固定可以存放8個key-value對

(2)倘若超過 8 個 key-value 對打到桶數組的同一個索引當中,此時會通過創建桶鏈表的方式來化解這一問題。

在這里插入圖片描述

1.:hash沖突不同的key,可能會存在相同的hash值

2:不同的hash值通過對桶長度進行取模之后,也有可能會被打到同一個桶中。

綜上面兩點:不同的 key-value 可能被映射到 map 的同一個桶當中。

拉鏈法解決hash沖突

拉鏈法中,將命中同一個桶的元素通過鏈表的形式進行鏈接,因此很便于動態擴展.

Go map 的做法(拉鏈法的優化):

  • 每個 bucket 固定容納 8 個 key-value
  • 如果滿了,就通過 overflow 字段掛一個溢出桶
  • 實際上是一個 結構化拉鏈法(struct-based chaining)

開放尋址法解決hash沖突

所有元素都存在 哈希表本身。如果發生沖突,就按照某種“探測序列”尋找下一個可用位置。

常見策略:

  • 線性探測(index+1
  • 二次探測(index+1^2, +2^2…)
  • 雙重哈希(用另一個哈希函數重新計算偏移)
方法優點
拉鏈法簡單常用;無需預先為元素分配內存。
開放尋址法無需額外的指針用于鏈接元素;內存地址完全連續,可以基于局部性原理,充分利用CPU高速緩存。

Go 的 map 實現確實結合了 開放尋址法拉鏈法 的思想,但它并不是標準意義上的鏈表拉鏈法,而是桶鏈表 + 定長數組 + 溢出桶機制的混合方案。

(1) 每個桶是一個結構體(Go 源碼中為bmap),包含:

一個 tophash 數組(快速判斷 key 匹配),數組長度為8,也就是說一個桶中可以存儲8個Key-Value.

當桶滿了,不是在桶內繼續鏈表擴展 key-value,而是通過一個 溢出指針數組

指向額外的桶(overflow bucket)

所以Go的map實際上是:哈希桶數組+每桶最多存儲8個kv+桶裝滿后追加溢出桶形成“桶鏈表”

// A bucket for a Go map.
type bmap struct {// tophash generally contains the top byte of the hash value// for each key in this bucket. If tophash[0] < minTopHash,// tophash[0] is a bucket evacuation state instead.tophash [abi.OldMapBucketCount]uint8// Followed by bucketCnt keys and then bucketCnt elems.// NOTE: packing all the keys together and then all the elems together makes the// code a bit more complicated than alternating key/elem/key/elem/... but it allows// us to eliminate padding which would be needed for, e.g., map[int64]int8.// Followed by an overflow pointer.
}

(2)key 命中一個桶后,在桶的 8 個位置中尋找空位插入,這就是類似開放尋址法的本地桶內查找

(3)如果桶的 8 個位置都被占滿,則找下一個溢出桶,重復第(2)步

(4)如果遍歷所有溢出桶都沒有空位,則新建溢出桶并插入

Go map 擴容機制

為什么要擴容?

如果桶數組的長度一直保持不變,那么隨著key-value對的增長,當一個桶下掛載的key-value達到一定的量級,時間復雜度上升,無法滿足訴求。

因此,為了將操作的時間復雜度維持在 O(1),map 會在滿足一定條件時觸發擴容,以控制每個桶的平均負載在常量級別。

map 擴容機制的核心點包括:

(1)擴容分為增量擴容和等量擴容;

(2)增量擴容:當 map 的負載因子(key 數量 / 桶數量)大于 6.5 時,會觸發增量擴容,將桶數組長度擴大為原值的兩倍。

(3) 等量擴容:當桶內溢出桶數量大于等于 2^B 時( B 為桶數組長度的指數,B 最大取 15),發生等量擴容,桶的長度保持為原值;等量擴容用于處理 hash 分布不均(熱點 key)導致的溢出桶爆炸問題,此時不會改變桶數量,而是重新散列所有 key。

(4)采用漸進擴容的方式,當桶被實際操作到時,由使用者負責完成數據遷移,避免因為一次性的全量數據遷移引發性能抖動。漸進遷移通過“讀寫操作觸發搬遷”的方式,把遷移成本分攤到用戶的操作中,避免瞬時卡頓。

漸進擴容的核心流程

擴容時,Go map 并不會馬上把所有舊桶遷移完,而是:

  1. 分配一份新的 bucket 數組(oldbuckets 和 newbuckets 并存);
  2. 設置 oldbuckets 指針指向舊桶;
  3. 在后續的 每次寫入操作(插入/刪除)中順便“搬一兩個舊桶”的數據到新桶中
  4. 遷移的數據會被重新計算 hash 值并分配到新桶;
  5. 同時維護一個 nevacuate 指針記錄遷移進度,當 nevacuate 指向了所有舊桶的末尾,說明所有桶都搬遷完畢。
  6. 當所有舊桶都遷移完畢后,map 會把舊桶釋放,指針 oldbuckets 清空。才切換為新桶數組。

在 map 漸進擴容過程中,如何判斷一個舊桶的 key 應該遷移到新桶的哪個桶中?

在 Go 的 map 中,擴容時新桶數量 = 舊桶數量 × 2(增量擴容)

因此:一個舊桶中的 key-value 對只可能被遷移到兩個新桶中的一個。

判斷規則是:對于舊桶中每個 key,根據 hash 值重新計算位置,看是落到原桶 b,還是 b + oldBucketCount

因為舊桶的索引是 h & (oldBucketCount - 1),即取 hash 的低 B-1 位。

擴容后只是多了一位(第 B 位),也就是說:

  • 如果這第 B 位是 0:落到原桶 b;
  • 如果這第 B 位是 1:落到新桶 b + oldBucketCount。

Go map的桶(bucket)結構是:

  • 每個桶可以存放 最多 8 個 key-value 對
  • 為了加快桶內查找效率,Go 把 每個 key 的 hash 值的“高 8 位” 存進一個 tophash 數組(長度為 8,對應每個 key);
  • 而 hash 值的 低若干位(具體取決于桶數量) 用于決定 key 屬于哪個桶。

數據結構

hmap

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
}

(1)count:map 中的 key-value 總數;

(2)flags:map 狀態標識,可以標識出 map 是否被 goroutine 并發讀寫;

(3)B:桶數組長度的指數,桶數組長度為 2^B;

(4)noverflow:map 中溢出桶的數量;

(5)hash0:hash 隨機因子,生成 key 的 hash 值時會使用到;

(6)buckets:桶數組;

(7)oldbuckets:擴容過程中老的桶數組;

(8)nevacuate:擴容時的進度標識,index 小于 nevacuate 的桶都已經由老桶轉移到新桶中;

(9)extra:預申請的溢出桶.

mapextra

這個mapextra是hmap中的一個“輔助字段”,專門用于管理溢出桶(overflow buckets)

type mapextra struct {// If both key and elem do not contain pointers and are inline, then we mark bucket// type as containing no pointers. This avoids scanning such maps.// However, bmap.overflow is a pointer. In order to keep overflow buckets// alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.// overflow and oldoverflow are only used if key and elem do not contain pointers.// overflow contains overflow buckets for hmap.buckets.// oldoverflow contains overflow buckets for hmap.oldbuckets.// The indirection allows to store a pointer to the slice in hiter.overflow    *[]*bmapoldoverflow *[]*bmap// nextOverflow holds a pointer to a free overflow bucket.nextOverflow *bmap
}

在 map 初始化時,倘若容量過大,會提前申請好一批溢出桶,以供后續使用,這部分溢出桶存放在 hmap.mapextra 當中:

(1)mapextra.overflow:供桶數組 buckets 使用的溢出桶;

(2)mapextra.oldoverFlow: 擴容流程中,供老桶數組 oldBuckets 使用的溢出桶;

(3)mapextra.nextOverflow:下一個可用的溢出桶.

Go 會需要一個“溢出桶”來存放新插入的數據,這時候它有一個“拿溢出桶”的優先順序

  1. 優先從 mapextra.nextOverflow:這個是預先分配好的一批溢出桶
  2. 如果 nextOverflow 沒桶了,就:去 overflow[] 列表里找是否有空閑的桶(可能是之前用過又回收的)
  3. 如果都沒有,才會向 Go 的內存分配器(heap)重新申請一個新的溢出桶

bmap

// A bucket for a Go map.
type bmap struct {// tophash generally contains the top byte of the hash value// for each key in this bucket. If tophash[0] < minTopHash,// tophash[0] is a bucket evacuation state instead.tophash [abi.OldMapBucketCount]uint8// Followed by bucketCnt keys and then bucketCnt elems.// NOTE: packing all the keys together and then all the elems together makes the// code a bit more complicated than alternating key/elem/key/elem/... but it allows// us to eliminate padding which would be needed for, e.g., map[int64]int8.// Followed by an overflow pointer.
}

(1)bmap 就是 map 中的桶,可以存儲 8 組 key-value 對的數據,以及一個指向下一個溢出桶的指針;

(2)每組 key-value 對數據包含 key 高 8 位 hash 值 tophash,key 和 val 三部分;

(3)在代碼層面只展示了 tophash 部分,但由于 tophash、key 和 val 的數據長度固定,因此可以通過內存地址偏移的方式尋找到后續的 key 數組、val 數組以及溢出桶指針;

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/82551.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/82551.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/82551.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

循環神經網絡(RNN):原理、架構與實戰

循環神經網絡&#xff08;Recurrent Neural Network, RNN&#xff09;是一類專門處理序列數據的神經網絡&#xff0c;如時間序列、自然語言、音頻等。與前饋神經網絡不同&#xff0c;RNN 引入了循環結構&#xff0c;能夠捕捉序列中的時序信息&#xff0c;使模型在不同時間步之間…

java 項目登錄請求業務解耦模塊全面

登錄是統一的閘機&#xff1b; 密碼存在數據庫中&#xff0c;用的是密文&#xff0c;后端加密&#xff0c;和數據庫中做對比 1、UserController public class UserController{Autowiredprivate IuserService userservicepublic JsonResult login(Validated RequestBody UserLo…

【手寫數據庫核心揭秘系列】第9節 可重入的SQL解析器,不斷解析Structure Query Language,語言翻譯好幫手

可重入的SQL解析器 文章目錄 可重入的SQL解析器一、概述 二、可重入解析器 2.1 可重入設置 2.2 記錄狀態的數據結構 2.3 節點數據類型定義 2.4 頭文件引用 三、調整后的程序結構 四、總結 一、概述 現在就來修改之前sqlscanner.l和sqlgram.y程序,可以不斷輸入SQL語句,循環執…

微軟開源bitnet b1.58大模型,應用效果測評(問答、知識、數學、邏輯、分析)

微軟開源bitnet b1.58大模型,應用效果測評(問答、知識、數學、邏輯、分析) 目 錄 1. 前言... 2 2. 應用部署... 2 3. 應用效果... 3 1.1 問答方面... 3 1.2 知識方面... 4 1.3 數字運算... 6 1.4 邏輯方面... …

用HTML5+JavaScript實現漢字轉拼音工具

用HTML5JavaScript實現漢字轉拼音工具 前一篇博文&#xff08;https://blog.csdn.net/cnds123/article/details/148067680&#xff09;提到&#xff0c;當需要將拼音添加到漢字上面時&#xff0c;用python實現比HTML5JavaScript實現繁瑣。在這篇博文中用HTML5JavaScript實現漢…

鴻蒙OSUniApp 開發的動態背景動畫組件#三方框架 #Uniapp

使用 UniApp 開發的動態背景動畫組件 前言 在移動應用開發中&#xff0c;動態背景動畫不僅能提升界面美感&#xff0c;還能增強用戶的沉浸感和品牌辨識度。無論是登錄頁、首頁還是活動頁&#xff0c;恰到好處的動態背景都能讓產品脫穎而出。隨著鴻蒙&#xff08;HarmonyOS&am…

云原生技術架構技術探索

文章目錄 前言一、什么是云原生技術架構二、云原生技術架構的優勢三、云原生技術架構的應用場景結語 前言 在當今的技術領域&#xff0c;云原生技術架構正以一種勢不可擋的姿態席卷而來&#xff0c;成為了眾多開發者、企業和技術愛好者關注的焦點。那么&#xff0c;究竟什么是…

AWS之AI服務

目錄 一、AWS AI布局 ??1. 底層基礎設施與芯片?? ??2. AI訓練框架與平臺?? ??3. 大模型與應用層?? ??4. 超級計算與網絡?? ??與競品對比?? AI服務 ??1. 機器學習平臺?? ??2. 預訓練AI服務?? ??3. 邊緣與物聯網AI?? ??4. 數據與AI…

lwip_bind、lwip_listen 是阻塞函數嗎

在 lwIP 協議棧中&#xff0c;lwip_bind 和 lwip_listen 函數本質上是非阻塞的。 通常&#xff0c;bind和listen在大多數實現中都是非阻塞的&#xff0c;因為它們只是設置套接字的屬性&#xff0c;不需要等待外部事件。阻塞通常發生在接受連接&#xff08;accept&#xff09;、…

【后端高階面經:消息隊列篇】28、從零設計高可用消息隊列

一、消息隊列架構設計的核心目標與挑戰 設計高性能、高可靠的消息隊列需平衡功能性與非功能性需求,解決分布式系統中的典型問題。 1.1 核心設計目標 吞吐量:支持百萬級消息/秒處理,通過分區并行化實現橫向擴展。延遲:端到端延遲控制在毫秒級,適用于實時業務場景。可靠性…

【運維實戰】Linux 內存調優之進程內存深度監控

寫在前面 內容涉及 Linux 進程內存監控 監控方式包括傳統工具 ps/top/pmap ,以及 cgroup 內存子系統&#xff0c;proc 內存偽文件系統 監控內容包括進程內存使用情況&#xff0c; 內存全局數據統計&#xff0c;內存事件指標&#xff0c;以及進程內存段數據監控 監控進程的內…

決策樹 GBDT XGBoost LightGBM

一、決策樹 1. 決策樹有一個很強的假設&#xff1a; 信息是可分的&#xff0c;否則無法進行特征分支 2. 決策樹的種類&#xff1a; 2. ID3決策樹&#xff1a; ID3決策樹的數劃分標準是信息增益&#xff1a; 信息增益衡量的是通過某個特征進行數據劃分前后熵的變化量。但是&…

java基礎學習(十四)

文章目錄 4-1 面向過程與面向對象4-2 Java語言的基本元素&#xff1a;類和對象面向對象的思想概述 4-3 對象的創建和使用內存解析匿名對象 4-1 面向過程與面向對象 面向過程(POP) 與 面向對象(OOP) 二者都是一種思想&#xff0c;面向對象是相對于面向過程而言的。面向過程&…

TCP 三次握手,第三次握手報文丟失會發生什么?

文章目錄 RTO(Retransmission Timeout)注意 客戶端收到服務端的 SYNACK 報文后&#xff0c;會回給服務端一個 ACK 報文&#xff0c;之后處于 ESTABLISHED 狀態 因為第三次握手的 ACK 是對第二次握手中 SYN 的確認報文&#xff0c;如果第三次握手報文丟失了&#xff0c;服務端就…

deepseek告訴您http與https有何區別?

有用戶經常問什么是Http , 什么是Https &#xff1f; 兩者有什么區別&#xff0c;下面為大家介紹一下兩者的區別 一、什么是HTTP HTTP是一種無狀態的應用層協議&#xff0c;用于在客戶端瀏覽器和服務器之間傳輸網頁信息&#xff0c;默認使用80端口 二、HTTP協議的特點 HTTP協議…

openresty如何禁止海外ip訪問

前幾天&#xff0c;我有一個徒弟問我&#xff0c;如何禁止海外ip訪問他的網站系統&#xff1f;操作系統采用的是centos7.9&#xff0c;發布服務采用的是openresty。通過日志他發現&#xff0c;有很多類似以下數據 {"host":"172.30.7.95","clientip&q…

理解 Redis 事務-20 (MULTI、EXEC、DISCARD)

理解 Redis 事務&#xff1a;MULTI、EXEC、DISCARD Redis 事務允許你將一組命令作為一個單一的原子操作來執行。這意味著事務中的所有命令要么全部執行&#xff0c;要么全部不執行。這對于在需要一起執行多個操作時保持數據完整性至關重要。本課程將涵蓋 Redis 事務的基礎知識…

Milvus分區-分片-段結構詳解與最佳實踐

導讀&#xff1a;在構建大規模向量數據庫應用時&#xff0c;數據組織架構的設計往往決定了系統的性能上限。Milvus作為主流向量數據庫&#xff0c;其獨特的三層架構設計——分區、分片、段&#xff0c;為海量向量數據的高效存儲和檢索提供了堅實基礎。 本文通過圖書館管理系統的…

Kettle 遠程mysql 表導入到 hadoop hive

kettle 遠程mysql 表導入到 hadoop hive &#xff08;教學用 &#xff09; 文章目錄 kettle 遠程mysql 表導入到 hadoop hive創建 對象 執行 SQL 語句 -mysql 導出 CSV格式CSV 文件遠程上傳到 HDFS運行 SSH 命令遠程登錄 run SSH 并執行 hadoop fs -put 建表和加載數據總結 創…

Linux輸出命令——echo解析

摘要 全面解析Linux echo命令核心功能&#xff0c;涵蓋文本輸出、變量解析、格式控制及高級技巧&#xff0c;助力提升Shell腳本開發與終端操作效率。 一、核心功能與定位 作為Shell腳本開發的基礎工具&#xff0c;echo命令承擔著信息輸出與數據傳遞的重要角色。其主要功能包…