一緒に書く習慣をつけましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加して4日目です。クリックしてイベントの詳細をご覧ください。
この記事の内容は次のとおりです。この記事を読むと、次のGolangMap関連のインタビューの質問が見つかります。
面接の質問
-
マップの基本的な実装原則
-
マップを順序付けられていない状態で反復するのはなぜですか?
-
マップの順序どおりのトラバーサルを実装するにはどうすればよいですか?
-
Goマップがスレッドセーフではないのはなぜですか?
-
スレッドセーフマップはどのように実装されますか?
-
sync.mapまたはネイティブマップに移動します。どちらの方がパフォーマンスが高く、その理由は何ですか。
-
Goマップ6.5の負荷率はなぜですか?
-
マップ拡張戦略とは何ですか?
実装の原則
Goのマップは、hmap構造を指す、8バイトを占めるポインターです。src/runtime/map.go
マップの基礎となる構造は、ソースコードで確認できます。
各マップの基礎となる構造はhmapであり、hmapには、構造がbmapであるいくつかのバケット配列が含まれています。各バケットの最下層は、リンクリスト構造を採用しています。次に、マップの構造を詳しく見てみましょう。
hmap構造
// A header for a Go map.
type hmap struct {
count int
// 代表哈希表中的元素个数,调用len(map)时,返回的就是该字段值。
flags uint8
// 状态标志,下文常量中会解释四种状态位含义。
B uint8
// buckets(桶)的对数log_2
// 如果B=5,则buckets数组的长度 = 2^5=32,意味着有32个桶
noverflow uint16
// 溢出桶的大概数量
hash0 uint32
// 哈希种子
buckets unsafe.Pointer
// 指向buckets数组的指针,数组大小为2^B,如果元素个数为0,它为nil。
oldbuckets unsafe.Pointer
// 如果发生扩容,oldbuckets是指向老的buckets数组的指针,老的buckets数组大小是新的buckets的1/2;非扩容状态下,它为nil。
nevacuate uintptr
// 表示扩容进度,小于此地址的buckets代表已搬迁完成。
extra *mapextra
// 这个字段是为了优化GC扫描而设计的。当key和value均不包含指针,并且都可以inline时使用。extra是指向mapextra类型的指针。
}
复制代码
bmap構造
bmap
これは、私たちがよく「バケット」と呼ぶものです。バケットは最大8つのキーを保持できます。これらのキーが同じバケットに分類される理由は、ハッシュされた後、ハッシュ結果が「1つのクラス」になるためです。キーの位置マップのクエリと挿入で詳細に説明されています。バケットでは、キーによって計算されたハッシュ値の上位8ビットを使用して、キーがバケット内のどこにあるかを判別します(バケット内には最大8つの場所があります)。
// A bucket for a Go map.
type bmap struct {
tophash [bucketCnt]uint8
// len为8的数组
// 用来快速定位key是否在这个bmap中
// 桶的槽位数组,一个桶最多8个槽位,如果key所在的槽位在tophash中,则代表该key在这个桶中
}
//底层定义的常量
const (
bucketCntBits = 3
bucketCnt = 1 << bucketCntBits
// 一个桶最多8个位置
)
但这只是表面(src/runtime/hashmap.go)的结构,编译期间会给它加料,动态地创建一个新的结构:
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
// 溢出桶
}
复制代码
バケットメモリのデータ構造は次のように視覚化されます。
キーと値は、key/value/key/value/...
この。ソースコードは、これの利点は、メモリスペースを節約するためにパディングフィールドを省略できる場合があることを示しています。
mapextra構造
当 map 的 key 和 value 都不是指针,并且 size 都小于 128 字节的情况下,会把 bmap 标记为不含指针,这样可以避免 gc 时扫描整个 hmap。但是,我们看 bmap 其实有一个 overflow 的字段,是指针类型的,破坏了 bmap 不含指针的设想,这时会把 overflow 移动到 extra 字段来。
// mapextra holds fields that are not present on all maps.
type mapextra struct {
// 如果 key 和 value 都不包含指针,并且可以被 inline(<=128 字节)
// 就使用 hmap的extra字段 来存储 overflow buckets,这样可以避免 GC 扫描整个 map
// 然而 bmap.overflow 也是个指针。这时候我们只能把这些 overflow 的指针
// 都放在 hmap.extra.overflow 和 hmap.extra.oldoverflow 中了
// overflow 包含的是 hmap.buckets 的 overflow 的 buckets
// oldoverflow 包含扩容时的 hmap.oldbuckets 的 overflow 的 bucket
overflow *[]*bmap
oldoverflow *[]*bmap
nextOverflow *bmap
// 指向空闲的 overflow bucket 的指针
}
复制代码
主要特性
引用类型
map是个指针,底层指向hmap,所以是个引用类型
golang 有三个常用的高级类型slice、map、channel, 它们都是引用类型,当引用类型作为函数参数时,可能会修改原内容数据。
golang 中没有引用传递,只有值和指针传递。所以 map 作为函数实参传递时本质上也是值传递,只不过因为 map 底层数据结构是通过指针指向实际的元素存储空间,在被调函数中修改 map,对调用者同样可见,所以 map 作为函数实参传递时表现出了引用传递的效果。
因此,传递 map 时,如果想修改map的内容而不是map本身,函数形参无需使用指针
func TestSliceFn(t *testing.T) {
m := map[string]int{}
t.Log(m, len(m))
// map[a:1]
mapAppend(m, "b", 2)
t.Log(m, len(m))
// map[a:1 b:2] 2
}
func mapAppend(m map[string]int, key string, val int) {
m[key] = val
}
复制代码
共享存储空间
map 底层数据结构是通过指针指向实际的元素存储空间 ,这种情况下,对其中一个map的更改,会影响到其他map
func TestMapShareMemory(t *testing.T) {
m1 := map[string]int{}
m2 := m1
m1["a"] = 1
t.Log(m1, len(m1))
// map[a:1] 1
t.Log(m2, len(m2))
// map[a:1]
}
复制代码
遍历顺序随机
map 在没有被修改的情况下,使用 range 多次遍历 map 时输出的 key 和 value 的顺序可能不同。这是 Go 语言的设计者们有意为之,在每次 range 时的顺序被随机化,旨在提示开发者们,Go 底层实现并不保证 map 遍历顺序稳定,请大家不要依赖 range 遍历结果顺序。
map 本身是无序的,且遍历时顺序还会被随机化,如果想顺序遍历 map,需要对 map key 先排序,再按照 key 的顺序遍历 map。
func TestMapRange(t *testing.T) {
m := map[int]string{1: "a", 2: "b", 3: "c"}
t.Log("first range:")
// 默认无序遍历
for i, v := range m {
t.Logf("m[%v]=%v ", i, v)
}
t.Log("\nsecond range:")
for i, v := range m {
t.Logf("m[%v]=%v ", i, v)
}
// 实现有序遍历
var sl []int
// 把 key 单独取出放到切片
for k := range m {
sl = append(sl, k)
}
// 排序切片
sort.Ints(sl)
// 以切片中的 key 顺序遍历 map 就是有序的了
for _, k := range sl {
t.Log(k, m[k])
}
}
复制代码
非线程安全
map默认是并发不安全的,原因如下:
Go 官方在经过了长时间的讨论后,认为 Go map 更应适配典型使用场景(不需要从多个 goroutine 中进行安全访问),而不是为了小部分情况(并发访问),导致大部分程序付出加锁代价(性能),决定了不支持。
シナリオ:2つのコルーチンが同時に読み取りと書き込みを行う場合、次のプログラムに致命的なエラーが発生します:致命的なエラー:同時マップ書き込み
func main() {
m := make(map[int]int)
go func() {
//开一个协程写map
for i := 0; i < 10000; i++ {
m[i] = i
}
}()
go func() {
//开一个协程读map
for i := 0; i < 10000; i++ {
fmt.Println(m[i])
}
}()
//time.Sleep(time.Second * 20)
for {
;
}
}
复制代码
マップスレッドセーフを実現する場合は、次の2つの方法があります。
方法1:読み取り/書き込みロックを使用するmap
+sync.RWMutex
func BenchmarkMapConcurrencySafeByMutex(b *testing.B) {
var lock sync.Mutex //互斥锁
m := make(map[int]int, 0)
var wg sync.WaitGroup
for i := 0; i < b.N; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
lock.Lock()
defer lock.Unlock()
m[i] = i
}(i)
}
wg.Wait()
b.Log(len(m), b.N)
}
复制代码
方法2:golangが提供するものを使用するsync.Map
sync.mapは読み取りと書き込みの分離で実装され、そのアイデアは時間とスペースを交換することです。map + RWLockの実装と比較して、いくつかの最適化が行われました。読み取りマップはロックなしでアクセスでき、読み取りマップは優先的に操作されます。読み取りマップのみを操作して要件(追加、削除、変更、検索、トラバース)の場合、書き込みマップの操作に進む必要はありません(読み取りと書き込みはロックされている必要があります)。したがって、特定のシナリオでは、ロック競合の頻度は、 map+RWLock。
func BenchmarkMapConcurrencySafeBySyncMap(b *testing.B) {
var m sync.Map
var wg sync.WaitGroup
for i := 0; i < b.N; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
m.Store(i, i)
}(i)
}
wg.Wait()
b.Log(b.N)
}
复制代码
ハッシュ衝突
golangのマップは、kvペアのコレクションです。最下層はハッシュテーブルを使用し、リンクリストを使用して競合を解決します。競合が発生した場合、リンクリストを介して各キーをつなぎ合わせるための構造を適用する代わりに、最小の粒度としてbmapを使用してマウントされます。 1つのbmapは8kvを保持できます。ハッシュ関数の選択では、プログラムの起動時に、CPUがaesをサポートしているかどうかが検出されます。サポートされている場合は、aesハッシュが使用されます。サポートされていない場合は、memhashが使用されます。
要約する
-
マップは参照型です
-
マップトラバーサルは順序付けられていません
-
マップはスレッドセーフではありません
-
マップのハッシュ衝突解決法はリンクリスト法です
-
マップの拡張は必ずしもスペースを追加するわけではなく、メモリの並べ替えを行うだけの場合もあります
-
マップの移行は段階的に実行され、割り当てごとに少なくとも1つの移行が実行されます。
-
マップ内のキーを削除すると、多くの空のkvが発生し、移行操作が発生する可能性があります。回避できる場合は、回避するようにしてください。