ir a lo básico implementación interna de 08 mapas

En comparación con el corte, la implementación interna del tipo de mapa es mucho más compleja. El tiempo de ejecución de Go utiliza una tabla hash para implementar el tipo de mapa abstracto. El tiempo de ejecución implementa todas las funciones de las operaciones de mapas, incluida la búsqueda, inserción, eliminación, recorrido, etc. Durante la fase de compilación, el compilador Go reescribirá la operación del mapa a nivel sintáctico en la llamada de función correspondiente en tiempo de ejecución.

La siguiente es la correspondencia aproximada:

// $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")

La siguiente figura es un diagrama esquemático del tipo de mapa implementado en la capa de tiempo de ejecución.

Insertar descripción de la imagen aquí

1. Estado inicial

En la figura, podemos ver que la correspondencia uno a uno con las variables de tipo de mapa en el nivel de sintaxis es la instancia del tipo runtime.hmap. hmap es el encabezado del tipo de mapa, que puede entenderse como el descriptor del tipo de mapa y almacena toda la información requerida para operaciones posteriores de tipo de mapa.

● count: el número de elementos en el mapa actual; cuando se utiliza la función incorporada len en una variable de tipo de mapa, la función len devuelve el valor count.

● flags: el indicador de estado del mapa actual. Actualmente, se definen 4 valores de estado: iterator, oldIterator, hashWriting y SameSizeGrow.

● B: El valor de B es el logaritmo de base 2 del número de depósitos, es decir, 2^B = número de depósitos.

● noverflow: número aproximado de depósitos de desbordamiento.

● hash0: el valor inicial de la función hash.

● depósitos: puntero a la matriz de depósitos.

● oldbuckets: puntero a la matriz de depósitos anterior durante la fase de expansión del mapa.

● nevacuar: actúa como un contador de progreso de expansión durante la fase de expansión del mapa. Todos los depósitos con números de índice menores que nevacuate han completado las operaciones de migración y drenaje de datos.

● extra: campo opcional. Si existe un depósito de desbordamiento y la clave y el valor están insertados porque no contienen punteros, este campo almacenará todos los punteros al depósito de desbordamiento para garantizar que el depósito de desbordamiento esté siempre disponible (no recolectado basura).

Los depósitos se utilizan en realidad para almacenar datos de pares clave-valor . Cada depósito almacena elementos con el mismo valor de bits bajo del valor Hash. El número predeterminado de elementos es BUCKETSIZ (el valor es 8 , en $GOROOT/src/ Definido en cmd /compile/internal/gc/reflect.go, consistente con la constante bucketCnt en runtime/map.go).

Cuando las 8 ranuras vacías de un depósito (como los depósitos [0]) están todas llenas y el mapa aún no ha alcanzado las condiciones de expansión, se establecerá un depósito de desbordamiento en tiempo de ejecución y el depósito de desbordamiento se colgará en el depósito superior ( como depósitos [0]), los dos depósitos forman una estructura de lista vinculada y la existencia de esta estructura durará hasta la próxima expansión del mapa.

Cada depósito consta de tres partes: área de tophash, área de almacenamiento de claves y área de almacenamiento de valores.

1) área de tofash

Al insertar un dato en el mapa o consultar datos por clave del mapa, el tiempo de ejecución utilizará una función hash para codificar la clave y obtener un código hash de valor hash. Este código hash es muy crítico: el código hash se "divide en dos" en tiempo de ejecución, donde el valor en el área de orden inferior se usa para seleccionar el depósito y el valor en el área de orden superior se usa para determinar la ubicación del clave en un determinado cubo . Consulte la figura siguiente para conocer este proceso.

Insertar descripción de la imagen aquí

Por lo tanto, el área tophash de cada cubo se utiliza para localizar rápidamente la posición de la clave, evitando así la costosa operación de comparar clave por clave, especialmente cuando la clave es de tipo cadena con un tamaño grande, esta es una idea de intercambiando espacio por tiempo.

2) área de almacenamiento de llaves

Debajo del área tophash hay un área de memoria continua que almacena todos los datos clave que transporta el depósito.

El tiempo de ejecución necesita conocer el tamaño de la clave al asignar el depósito. Entonces, ¿cómo sabe el tiempo de ejecución el tamaño de la clave? Cuando declaramos una variable de tipo de mapa, como var m map[string]int, el tiempo de ejecución de Go generará una instancia runtime.maptype para el tipo de mapa específico correspondiente a la variable (reutilizada si existe):

// $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
}

Esta instancia contiene toda la metainformación que necesitamos sobre el tipo de mapa. Como se mencionó anteriormente, el compilador reescribirá la operación del mapa a nivel sintáctico en la llamada de función correspondiente en tiempo de ejecución. Estas funciones de tiempo de ejecución tienen una característica común:

El primer parámetro es un parámetro de tipo puntero de tipo de mapa. El tiempo de ejecución de Go utiliza la información del parámetro maptype para determinar el tipo y tamaño de la clave. La función hash utilizada por el mapa también se almacena en maptype.key.alg.hash(key, hmap.hash0).

Al mismo tiempo, la existencia de maptype también permite que todos los tipos de mapas en Go compartan un conjunto de funciones de operación de mapas en tiempo de ejecución, en lugar de crear un conjunto de funciones de operación de mapas para cada tipo de mapa como C++, reduciendo así el espacio ocupado por el final. archivo binario.

Ejecute este programa:

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

Recibiremos el mensaje de pánico anterior. Si es solo lectura concurrente, no hay problema con el mapa.

La versión 1.9 de Go introduce el tipo sync.Map que admite seguridad de escritura concurrente, que se puede usar para reemplazar el mapa en escenarios de lectura y escritura concurrentes. Además, considerando que el mapa se puede expandir automáticamente, la posición del valor de los elementos de datos en el mapa puede cambiar durante este proceso, por lo que Go no permite obtener la dirección del valor en el mapa. Esta restricción entra en vigor durante la compilación. El código de ejemplo
es el siguiente:

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

Intente utilizar el parámetro cap para crear el mapa.

Del principio de expansión automática anterior, entendemos que si el mapa se crea inicialmente sin crear suficientes depósitos para hacer frente al escenario de uso del mapa, a medida que aumenta el número de elementos del mapa insertados, el mapa se expandirá con frecuencia y este proceso reducirá el Capacidad del mapa Acceso al rendimiento.

Por lo tanto, si es posible, es mejor hacer una estimación aproximada del tamaño de uso del mapa e
inicializar la instancia del mapa usando el parámetro cap.
La siguiente es la prueba comparativa de rendimiento de escritura del mapa y los resultados de la prueba usando el parámetro cap y no usando el parámetro 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
	}
}
}

Se puede ver que el rendimiento de escritura promedio de las instancias de mapa que utilizan el parámetro cap es 2 veces mayor que el de la instancia de mapa sin el parámetro cap.

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

Al igual que los sectores, el mapa es un tipo de datos importante proporcionado por el lenguaje Go y uno de los tipos más utilizados en la codificación diaria de Gopher. A través del estudio de este artículo, hemos dominado las operaciones básicas y los principios de implementación en tiempo de ejecución de map, y podemos usarlo en el día a día.

Al utilizar el mapa, debe comprender los siguientes puntos:

● No confiar en el orden de recorrido de los elementos del mapa;

● map no es seguro para subprocesos y no admite escritura simultánea;

● No intente obtener la dirección del elemento (valor) en el mapa;

● Intente utilizar parámetros de límite para crear mapas para mejorar el rendimiento promedio de acceso a mapas y reducir las pérdidas innecesarias causadas por la expansión frecuente.

Supongo que te gusta

Origin blog.csdn.net/hai411741962/article/details/132734175
Recomendado
Clasificación