Comprensión profunda del mapa de Golang

¡Acostúmbrate a escribir juntos! Este es el cuarto día de mi participación en el "Nuevo plan diario de los Nuggets·Desafío de actualización de abril", haz clic para ver los detalles del evento .

El contenido de este artículo es el siguiente: después de leer este artículo, encontrará las siguientes preguntas de la entrevista relacionadas con Golang Map.

imagen.png

preguntas de entrevista

  1. El principio de implementación subyacente del mapa

  2. ¿Por qué la iteración sobre el mapa no está ordenada?

  3. ¿Cómo implementar el recorrido en orden del mapa?

  4. ¿Por qué los mapas de Go no son seguros para subprocesos?

  5. ¿Cómo se implementa un mapa seguro para subprocesos?

  6. Vaya a sync.map o native map, ¿cuál tiene mejor rendimiento y por qué?

  7. ¿Por qué el factor de carga de Go map es 6.5?

  8. ¿Cuál es la estrategia de expansión del mapa?

Principio de implementación

El mapa en Go es un puntero que ocupa 8 bytes y apunta a la estructura hmap; src/runtime/map.gola estructura subyacente del mapa se puede ver en el código fuente

La estructura subyacente de cada mapa es hmap, y hmap contiene varias matrices de cubos cuya estructura es bmap. La capa inferior de cada cubo adopta una estructura de lista enlazada. A continuación, echemos un vistazo más de cerca a la estructura del mapa.

imagen.png

estructura 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类型的指针。
 }
复制代码

estructura bmap

bmapEs lo que a menudo llamamos "cubos". Un cubo puede contener hasta 8 claves. La razón por la que estas claves caen en el mismo cubo es que después de que se les aplica un hash, el resultado del hash es "una clase". El posicionamiento de la clave se explica detalladamente en la consulta e inserción del mapa. En el depósito, los 8 bits superiores del valor hash calculado por la clave se utilizan para determinar dónde cae la clave en el depósito (hay como máximo 8 ubicaciones en un depósito).

// 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
  // 溢出桶
}
复制代码

La estructura de datos de la memoria del depósito se visualiza de la siguiente manera:

Tenga en cuenta que la clave y el valor se juntan por separado, no de key/value/key/value/...esta forma. El código fuente muestra que la ventaja de esto es que, en algunos casos, el campo de relleno se puede omitir para ahorrar espacio en la memoria.

imagen.png

estructura 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 中进行安全访问),而不是为了小部分情况(并发访问),导致大部分程序付出加锁代价(性能),决定了不支持。

Escenario: 2 corrutinas leen y escriben al mismo tiempo, el siguiente programa tendrá un error fatal: error fatal: escrituras de mapa simultáneas

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 {
    
  ;
 }
}
复制代码

Si desea lograr la seguridad de los subprocesos del mapa, hay dos formas:

Método 1: Usar bloqueo de lectura y escritura map+sync.RWMutex

func BenchmarkMapConcurrencySafeByMutex(b *testing.B) {
 var lock sync.Mutex //互斥锁
 m := make(map[int]int0)
 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)
}
复制代码

Método 2: utilice el proporcionado por golangsync.Map

sync.map está implementado con separación de lectura y escritura, y su idea es intercambiar espacio por tiempo. En comparación con la implementación de map+RWLock, se han realizado algunas optimizaciones: se puede acceder al mapa de lectura sin bloqueo y se operará preferentemente el mapa de lectura. Si solo se puede operar el mapa de lectura para cumplir con los requisitos (agregar, eliminar, modificar, buscar y recorrer), entonces no hay necesidad de ir a La operación del mapa de escritura (su lectura y escritura deben estar bloqueadas), por lo que en algunos escenarios específicos, la frecuencia de la competencia de bloqueo será mucho menor que la implementación de mapa+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)
}
复制代码

colisión de hash

Un mapa en golang es una colección de pares kv. La capa inferior usa una tabla hash y una lista enlazada se usa para resolver conflictos. Cuando ocurre un conflicto, en lugar de aplicar una estructura para que cada clave se encadene a través de una lista enlazada, se monta con bmap como la granularidad más pequeña, y un bmap puede contener 8 kv. En la selección de la función hash, cuando se inicia el programa, se detectará si la cpu admite aes, si es así, se utilizará el hash aes, de lo contrario, se utilizará el memhash.

Resumir

  1. el mapa es un tipo de referencia

  2. el recorrido del mapa no está ordenado

  3. el mapa no es seguro para subprocesos

  4. El método de resolución de colisión hash del mapa es el método de lista enlazada

  5. La expansión del mapa no necesariamente agrega espacio, también puede ordenar la memoria

  6. La migración del mapa se realiza paso a paso, y se realizará al menos una migración por cada asignación.

  7. Eliminar la clave en el mapa puede llevar a muchos kv vacíos, lo que dará lugar a operaciones de migración, si se puede evitar, intente evitarlo.

Supongo que te gusta

Origin juejin.im/post/7082735541438906399
Recomendado
Clasificación