Подробное объяснение исходного кода карты golang

Go

В этой статье будет в основном проанализирован принцип реализации карты в golang, а также обсуждены распространенные проблемы при использовании. Версия golang, которая выполняет анализ, — 1.9.

Карта в golang реализована с помощью hashmap в качестве нижнего слоя.В исходном коде github есть два связанных кода:runtime/hashmap.goОпределяет базовую структуру и методы карты,runtime/hashmap_fast.goПредоставляет некоторые функции для быстрого управления картами.

карта базовой структуры данных

Базовая структура карты — hmap (сокращение от hashmap).Основной элемент представляет собой массив, состоящий из нескольких сегментов (сегментов, структура которых — bmap).Каждый сегмент может хранить несколько элементов (обычно 8).Ключ хэшируется с помощью алгоритмов сгруппированы в разные ведра. Когда в ведре необходимо хранить более 8 элементов, hmap будет использовать дополнительное переполнение для расширения ведра. Ниже представлена ​​структура hmap.

type hmap struct {
	count     int // # 元素个数
	flags     uint8
	B         uint8  // 说明包含2^B个bucket
	noverflow uint16 // 溢出的bucket的个数
	hash0     uint32 // hash种子
 
	buckets    unsafe.Pointer // buckets的数组指针
	oldbuckets unsafe.Pointer // 结构扩容的时候用于复制的buckets数组
	nevacuate  uintptr        // 搬迁进度(已经搬迁的buckets数量)
 
	extra *mapextra
}

Кроме того, есть не только overflow, но и oldoverflow (для расширения) и nextoverflow (адрес prealloc).

Структура ведра (bmap) следующая

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 [bucketCnt]uint8
	// Followed by bucketCnt keys and then bucketCnt values.
	// NOTE: packing all the keys together and then all the values together makes the
	// code a bit more complicated than alternating key/value/key/value/... but it allows
	// us to eliminate padding which would be needed for, e.g., map[int64]int8.
	// Followed by an overflow pointer.
}

  • Tophash используется для записи старших 8 битов 8 значений хеш-функции ключа, чтобы можно было быстрее найти соответствующий ключ, и нет необходимости каждый раз оценивать соответствие ключа.
  • Обратите внимание на комментарии в следующих строках, у hmap не только один tophash, но за ним следуют 8 наборов пар kv и указатель переполнения, так что переполнение может стать структурой связанного списка. Но эти две структуры не определены явно, а доступны напрямую через арифметику указателей.
  • Форма хранения kv: "key0key1key2key3...key7val1val2val3...val7". Преимущество этого заключается в том, что он экономит место для заполнения, когда длина ключа и значения различаются. Как и в приведенном выше примере, в map[int64]int8 4 соседних int8 могут храниться в одном блоке памяти. Если используется память с чередованием kv, каждый int8 будет заполняться, чтобы занять отдельную единицу памяти (для повышения скорости адресации).

Структура hmap почти такая, как показано на рисунке.

доступ к карте

Возьмем, к примеру, mapaccess1, какой-то нерелевантный код изменяется.

func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
	// do some race detect things
	// do some memory sanitizer thins
 
	if h == nil || h.count == 0 {
		return unsafe.Pointer(&zeroVal[0])
	}
	if h.flags&hashWriting != 0 {  // 检测是否并发写,map不是gorountine安全的
		throw("concurrent map read and map write")
	}
	alg := t.key.alg  // 哈希算法 alg -> algorithm
	hash := alg.hash(key, uintptr(h.hash0))
	m := bucketMask(h.B)
	b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
        // 如果老的bucket没有被移动完,那么去老的bucket中寻找 (增长部分下一节介绍)
	if c := h.oldbuckets; c != nil {
		if !h.sameSizeGrow() {
			// There used to be half as many buckets; mask down one more power of two.
			m >>= 1
		}
		oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
		if !evacuated(oldb) {
			b = oldb
		}
	}
        // 寻找过程:不断比对tophash和key
	top := tophash(hash)
	for ; b != nil; b = b.overflow(t) {
		for i := uintptr(0); i < bucketCnt; i++ {
			if b.tophash[i] != top {
				continue
			}
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
			if t.indirectkey {
				k = *((*unsafe.Pointer)(k))
			}
			if alg.equal(key, k) {
				v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
				if t.indirectvalue {
					v = *((*unsafe.Pointer)(v))
				}
				return v
			}
		}
	}
        return unsafe.Pointer(&zeroVal[0])
}

рост карты

По мере увеличения количества элементов поиск определенного ключа в цепочке сегментов становится неэффективным, поэтому, когда количество вставленных элементов/сегментов достигает определенного порога (в настоящее время установлено значение 6,5, экспериментальное значение), карта будет расширена, подробности см. в кодефункция hashGrow. Сначала создайте массив сегментов, длина которого в два раза превышает исходную длину.

newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger)
, а затем замените исходный сегмент, и исходный сегмент будет перемещен на указатель oldbucket.

После завершения расширения каждый хэш соответствует двум сегментам (одному новому и одному старому). Старое ведро не будет немедленно перенесено в новое ведро, но при доступе к ведру будет вызван метод GrowWork для переноса, который перехэширует элементы из старого ведра в новое ведро. По мере доступа все старые ведра будут постепенно перемещаться в ведро.

Но вот проблема: что делать, если миграция после последнего расширения не закончилась, когда требуется расширение? В коде мы можем видеть много тегов «опять», которые будут продолжать мигрировать, а следующее расширение будет выполнено после завершения миграции.

 

Распространенные проблемы при использовании

В: Будет ли освобождена память при удалении элементов на карте?

A: Нет, операция удаления только очищает соответствующий tophash[i], не освобождая память. Чтобы освободить память, вы можете только дождаться разыменования указателя системой gc.

 

В: Как использовать карту одновременно?

A: Карта не безопасна для горутин, поэтому она будет паниковать, когда на карту будут записываться несколько горутин. Карта чтения и записи с несколькими горунтами должна быть заблокирована (RWMutex) или использовать sync.Map (новое в 1.9, эта вещь будет представлена ​​в следующей статье, короче говоря, это не рекомендуется).

 

В: Безопасен ли итератор карты?

A: Удаление карты на самом деле не является удалением, поэтому оно не влияет на итератор и является безопасным.