L'essence de l'entretien linguistique GO : quel est le processus d'expansion de la carte ?

Le but de l'utilisation d'une table de hachage est de trouver rapidement la clé cible. Cependant, à mesure que de plus en plus de clés sont ajoutées à la carte, la probabilité de collisions de clés augmente. Les 8 cellules du compartiment se rempliront progressivement et l'efficacité de la recherche, de l'insertion et de la suppression des clés deviendra de moins en moins efficace. La situation la plus idéale est qu'un compartiment ne contienne qu'une seule clé, afin que O(1)l'efficacité puisse être atteinte, mais cela consomme trop d'espace et le coût de l'échange d'espace contre du temps est trop élevé.

Le langage Go utilise un compartiment pour charger les clés 8. Après avoir localisé un certain compartiment, vous devez à nouveau localiser la clé spécifique, qui utilise en fait du temps pour l'espace.

Bien sûr, cela doit être fait dans une certaine mesure, sinon toutes les clés tomberont dans le même seau, dégénérant directement en une liste chaînée, et l'efficacité des diverses opérations tombera directement à O(n), ce qui n'est pas acceptable.

Il faut donc un indicateur pour mesurer la situation décrite précédemment, et c’est tout 装载因子. Le code source de Go définit ceci装载因子 :

loadFactor := count / (2^B)

count est le nombre d'éléments dans la carte et 2 ^ B représente le nombre de compartiments.

Parlons du timing de déclenchement de l'expansion de la carte : lors de l'insertion d'une nouvelle clé dans la carte, une détection de condition sera effectuée. Si les deux conditions suivantes sont remplies, l'expansion sera déclenchée :

  1. Le facteur de charge dépasse le seuil. Le seuil défini dans le code source est de 6,5.
  2. Le nombre de seaux de débordement est trop grand : lorsque B est inférieur à 15, c'est-à-dire que le nombre total de seaux 2^B est inférieur à 2^15, si le nombre de seaux de débordement dépasse 2^B ; lorsque B >= 15 , c'est-à-dire que le nombre total de compartiments 2^B est supérieur ou égal à 2^15, si le nombre de compartiments de débordement dépasse 2^15.

Grâce au langage assembleur, vous pouvez trouver la fonction dans le code source correspondant à l'opération d'affectation mapassign.Le code source correspondant à la condition d'expansion est le suivant :

// src/runtime/hashmap.go/mapassign

// 触发扩容时机
if !h.growing() && (overLoadFactor(int64(h.count), h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
		hashGrow(t, h)
	}

// 装载因子超过 6.5
func overLoadFactor(count int64, B uint8) bool {
	return count >= bucketCnt && float32(count) >= loadFactor*float32((uint64(1)<<B))
}

// overflow buckets 太多
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
	if B < 16 {
		return noverflow >= uint16(1)<<B
	}
	return noverflow >= 1<<15
}

expliquer:

Point 1 : Nous savons que chaque seau comporte 8 postes vacants. S'il n'y a pas de débordement et que tous les seaux sont pleins, le facteur de charge calculé est de 8. Par conséquent, lorsque le facteur de charge dépasse 6,5, cela indique que de nombreux compartiments sont presque pleins et que l'efficacité de la recherche et de l'insertion devient faible. Une expansion est nécessaire à ce moment-là.

Point 2 : Ceci est un complément au point 1. C'est-à-dire que lorsque le facteur de chargement est relativement faible, l'efficacité de recherche et d'insertion de la carte est également très faible à ce moment-là, et cette situation ne peut pas être reconnue au point 1. Le phénomène de surface est que le numérateur pour calculer le facteur de charge est relativement petit, c'est-à-dire que le nombre total d'éléments dans la carte est petit, mais le nombre de compartiments est grand (le nombre de compartiments réellement alloués est grand, y compris un grand nombre de seaux de trop-plein).

Il n'est pas difficile d'en imaginer la raison : l'insertion et la suppression constantes d'éléments. De nombreux éléments ont été insérés en premier, ce qui a entraîné la création de nombreux compartiments, mais le facteur de charge n'a pas atteint la valeur critique au point 1, et aucune extension de capacité n'a été déclenchée pour atténuer cette situation. Ensuite, la suppression d'éléments réduit le nombre total d'éléments, puis insère de nombreux éléments, ce qui entraîne la création de nombreux buckets de débordement, mais cela ne viole pas les dispositions du point 1. Que pouvez-vous me faire ? Il y a trop de seaux de débordement, ce qui entraînera la dispersion des clés et une efficacité de recherche et d'insertion horriblement faible, c'est pourquoi le deuxième point est introduit. C'est comme une ville vide, avec de nombreuses maisons mais peu d'habitants, tous dispersés, ce qui rend difficile la recherche de personnes.

Pour les conditions 1 et 2, une expansion se produira. Toutefois, les stratégies d’expansion ne sont pas les mêmes : après tout, les scénarios permettant de faire face à ces deux conditions sont différents.

Pour la condition 1, il y a trop d'éléments et trop peu de buckets. C'est très simple : ajoutez 1 à B, et le nombre maximum de buckets (2^B) devient directement le double du nombre de buckets d'origine. Il y a donc des seaux nouveaux et anciens. Notez qu'à l'heure actuelle, les éléments se trouvent tous dans l'ancien bucket et n'ont pas été migrés vers le nouveau bucket. De plus, le nouveau compartiment n'a qu'un nombre maximum qui est le double du nombre maximum d'origine (2 ^ B) (2 ^ B * 2).

Concernant la condition 2, il n'y a en réalité pas beaucoup d'éléments, mais le nombre de seaux de débordement est particulièrement important, indiquant que de nombreux seaux ne sont pas pleins. La solution consiste à ouvrir un nouvel espace de compartiment et à déplacer les éléments de l'ancien compartiment vers le nouveau compartiment afin que les clés du même compartiment soient disposées plus étroitement. De cette façon, la clé du seau de trop-plein peut être déplacée vers le seau. Le résultat est d'économiser de l'espace, d'améliorer l'utilisation du compartiment et l'efficacité de la recherche et de l'insertion de cartes augmentera naturellement.

Concernant la solution à la condition 2, le blog de Cao Da a également proposé une situation extrême : si les hachages de clés insérés dans la carte sont tous identiques, ils tomberont dans le même bucket. S'il y en a plus de 8, un bucket de débordement sera généré. , et le résultat sera Cela entraînera trop de compartiments de débordement. Le déplacement des éléments ne peut en fait pas résoudre le problème, car à ce moment-là, la table de hachage entière a dégénéré en une liste chaînée et l'efficacité opérationnelle est devenue O(n).

Voyons comment se fait l’expansion. Étant donné que l'expansion de la carte nécessite le déplacement des clés/valeurs d'origine vers de nouvelles adresses mémoire, si un grand nombre de clés/valeurs doivent être déplacées, les performances seront grandement affectées. Par conséquent, l'expansion de la carte Go adopte une méthode dite « progressive » : les clés d'origine ne seront pas déplacées en même temps, et seulement 2 buckets seront déplacés au maximum à chaque fois.

La fonction mentionnée ci-dessus hashGrow()ne "déplace" pas réellement, elle alloue simplement de nouveaux compartiments et suspend les anciens compartiments dans le champ oldbuckets. L'action réelle de déplacement des buckets se trouve dans growWork()la fonction, et growWork()l'action d'appeler la fonction se trouve dans les fonctions mapassign et mapdelete. Autrement dit, lorsqu'une clé est insérée, modifiée ou supprimée, une tentative de déplacement des compartiments est effectuée. Vérifiez d'abord si les oldbuckets ont été déplacés. Plus précisément, vérifiez si les oldbuckets sont nuls.

Examinons d'abord hashGrow()le travail effectué par la fonction, puis comment la migration spécifique des buckets est effectuée.

func hashGrow(t *maptype, h *hmap) {
	// B+1 相当于是原来 2 倍的空间
	bigger := uint8(1)

	// 对应条件 2
	if !overLoadFactor(int64(h.count), h.B) {
		// 进行等量的内存扩容,所以 B 不变
		bigger = 0
		h.flags |= sameSizeGrow
	}
	// 将老 buckets 挂到 buckets 上
	oldbuckets := h.buckets
	// 申请新的 buckets 空间
	newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger)

	flags := h.flags &^ (iterator | oldIterator)
	if h.flags&iterator != 0 {
		flags |= oldIterator
	}
	// 提交 grow 的动作
	h.B += bigger
	h.flags = flags
	h.oldbuckets = oldbuckets
	h.buckets = newbuckets
	// 搬迁进度为 0
	h.nevacuate = 0
	// overflow buckets 数为 0
	h.noverflow = 0

	// ……
}

La raison principale est qu'un nouvel espace de compartiment a été demandé et que les indicateurs correspondants ont été traités : par exemple, l'indicateur de nevacuer est défini sur 0, indiquant que la progression actuelle de la relocalisation est de 0.

Il convient de mentionner h.flagsle traitement de :

flags := h.flags &^ (iterator | oldIterator)
if h.flags&iterator != 0 {
	flags |= oldIterator
}

Ici, nous devons d'abord parler de l'opérateur : &^. C'est ce qu'on appelle 按位置 0un opérateur. Par exemple:

x = 01010011
y = 01010100
z = x &^ y = 00000011

Si le bit y vaut 1, alors le bit correspondant de z sera 0, sinon le bit correspondant à z aura la même valeur que le bit correspondant à x.

Ainsi, le code ci-dessus qui fonctionne sur les drapeaux signifie : effacez d'abord les bits correspondants de l'itérateur et de l'ancien itérateur dans h.flags à 0, puis si le bit de l'itérateur s'avère être 1, puis transférez-le vers le bit oldIterator, de sorte que l'oldIterator le bit d'indicateur devient 1. Le sous-texte est le suivant : les buckets portent désormais le nom oldBuckets et l'indicateur correspondant doit également y être transféré.

Plusieurs drapeaux sont les suivants :

// 可能有迭代器使用 buckets
iterator     = 1
// 可能有迭代器使用 oldbuckets
oldIterator  = 2
// 有协程正在向 map 中写入 key
hashWriting  = 4
// 等量扩容(对应条件 2)
sameSizeGrow = 8

Jetons un coup d'œil à la fonction growWork() qui effectue réellement le travail de relocalisation.

func growWork(t *maptype, h *hmap, bucket uintptr) {
	// 确认搬迁老的 bucket 对应正在使用的 bucket
	evacuate(t, h, bucket&h.oldbucketmask())

	// 再搬迁一个 bucket,以加快搬迁进程
	if h.growing() {
		evacuate(t, h, h.nevacuate)
	}
}

La fonction h.growing() est très simple :

func (h *hmap) growing() bool {
	return h.oldbuckets != nil
}

S'il oldbucketsn'est pas vide, cela signifie que la relocalisation n'est pas terminée et que la relocalisation doit se poursuivre.

bucket&h.oldbucketmask()Cette ligne de code, comme mentionné dans les commentaires du code source, sert à confirmer que le bucket déplacé est le bucket que nous utilisons. oldbucketmask()La fonction renvoie le bucketmask de la carte avant expansion.

Le soi-disant bucketmask est utilisé pour ET la valeur de hachage calculée par la clé avec le bucketmask, et le résultat est le bucket dans lequel la clé doit tomber. Par exemple, B = 5, alors les 5 bits inférieurs du masque de compartiment sont 11111, et les bits restants sont 0, et la valeur de hachage est associée à un ET, ce qui signifie que seuls les 5 bits inférieurs de la valeur de hachage déterminent dans quel compartiment la clé appartient. dans.

Ensuite, nous concentrons tous nos efforts sur la relocalisation de la fonction clé évacuer. Le code source est publié ci-dessous, ne soyez pas nerveux, j'ajouterai des commentaires détaillés, et vous pourrez certainement le comprendre à travers les commentaires. J'expliquerai le processus de déménagement en détail plus tard.

Le code source est le suivant :

func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
	// 定位老的 bucket 地址
	b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
	// 结果是 2^B,如 B = 5,结果为32
	newbit := h.noldbuckets()
	// key 的哈希函数
	alg := t.key.alg
	// 如果 b 没有被搬迁过
	if !evacuated(b) {
		var (
			// 表示bucket 移动的目标地址
			x, y   *bmap
			// 指向 x,y 中的 key/val
			xi, yi int
			// 指向 x,y 中的 key
			xk, yk unsafe.Pointer
			// 指向 x,y 中的 value
			xv, yv unsafe.Pointer
		)
		// 默认是等 size 扩容,前后 bucket 序号不变
		// 使用 x 来进行搬迁
		x = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))
		xi = 0
		xk = add(unsafe.Pointer(x), dataOffset)
		xv = add(xk, bucketCnt*uintptr(t.keysize))、

		// 如果不是等 size 扩容,前后 bucket 序号有变
		// 使用 y 来进行搬迁
		if !h.sameSizeGrow() {
			// y 代表的 bucket 序号增加了 2^B
			y = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))
			yi = 0
			yk = add(unsafe.Pointer(y), dataOffset)
			yv = add(yk, bucketCnt*uintptr(t.keysize))
		}

		// 遍历所有的 bucket,包括 overflow buckets
		// b 是老的 bucket 地址
		for ; b != nil; b = b.overflow(t) {
			k := add(unsafe.Pointer(b), dataOffset)
			v := add(k, bucketCnt*uintptr(t.keysize))

			// 遍历 bucket 中的所有 cell
			for i := 0; i < bucketCnt; i, k, v = i+1, add(k, uintptr(t.keysize)), add(v, uintptr(t.valuesize)) {
				// 当前 cell 的 top hash 值
				top := b.tophash[i]
				// 如果 cell 为空,即没有 key
				if top == empty {
					// 那就标志它被"搬迁"过
					b.tophash[i] = evacuatedEmpty
					// 继续下个 cell
					continue
				}
				// 正常不会出现这种情况
				// 未被搬迁的 cell 只可能是 empty 或是
				// 正常的 top hash(大于 minTopHash)
				if top < minTopHash {
					throw("bad map state")
				}

				k2 := k
				// 如果 key 是指针,则解引用
				if t.indirectkey {
					k2 = *((*unsafe.Pointer)(k2))
				}

				// 默认使用 X,等量扩容
				useX := true
				// 如果不是等量扩容
				if !h.sameSizeGrow() {
					// 计算 hash 值,和 key 第一次写入时一样
					hash := alg.hash(k2, uintptr(h.hash0))

					// 如果有协程正在遍历 map
					if h.flags&iterator != 0 {
						// 如果出现 相同的 key 值,算出来的 hash 值不同
						if !t.reflexivekey && !alg.equal(k2, k2) {
							// 只有在 float 变量的 NaN() 情况下会出现
							if top&1 != 0 {
								// 第 B 位置 1
								hash |= newbit
							} else {
								// 第 B 位置 0
								hash &^= newbit
							}
							// 取高 8 位作为 top hash 值
							top = uint8(hash >> (sys.PtrSize*8 - 8))
							if top < minTopHash {
								top += minTopHash
							}
						}
					}

					// 取决于新哈希值的 oldB+1 位是 0 还是 1
					// 详细看后面的文章
					useX = hash&newbit == 0
				}

				// 如果 key 搬到 X 部分
				if useX {
					// 标志老的 cell 的 top hash 值,表示搬移到 X 部分
					b.tophash[i] = evacuatedX
					// 如果 xi 等于 8,说明要溢出了
					if xi == bucketCnt {
						// 新建一个 bucket
						newx := h.newoverflow(t, x)
						x = newx
						// xi 从 0 开始计数
						xi = 0
						// xk 表示 key 要移动到的位置
						xk = add(unsafe.Pointer(x), dataOffset)
						// xv 表示 value 要移动到的位置
						xv = add(xk, bucketCnt*uintptr(t.keysize))
					}
					// 设置 top hash 值
					x.tophash[xi] = top
					// key 是指针
					if t.indirectkey {
						// 将原 key(是指针)复制到新位置
						*(*unsafe.Pointer)(xk) = k2 // copy pointer
					} else {
						// 将原 key(是值)复制到新位置
						typedmemmove(t.key, xk, k) // copy value
					}
					// value 是指针,操作同 key
					if t.indirectvalue {
						*(*unsafe.Pointer)(xv) = *(*unsafe.Pointer)(v)
					} else {
						typedmemmove(t.elem, xv, v)
					}

					// 定位到下一个 cell
					xi++
					xk = add(xk, uintptr(t.keysize))
					xv = add(xv, uintptr(t.valuesize))
				} else { // key 搬到 Y 部分,操作同 X 部分
					// ……
					// 省略了这部分,操作和 X 部分相同
				}
			}
		}
		// 如果没有协程在使用老的 buckets,就把老 buckets 清除掉,帮助gc
		if h.flags&oldIterator == 0 {
			b = (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
			// 只清除bucket 的 key,value 部分,保留 top hash 部分,指示搬迁状态
			if t.bucket.kind&kindNoPointers == 0 {
				memclrHasPointers(add(unsafe.Pointer(b), dataOffset), uintptr(t.bucketsize)-dataOffset)
			} else {
				memclrNoHeapPointers(add(unsafe.Pointer(b), dataOffset), uintptr(t.bucketsize)-dataOffset)
			}
		}
	}

	// 更新搬迁进度
	// 如果此次搬迁的 bucket 等于当前进度
	if oldbucket == h.nevacuate {
		// 进度加 1
		h.nevacuate = oldbucket + 1
		// Experiments suggest that 1024 is overkill by at least an order of magnitude.
		// Put it in there as a safeguard anyway, to ensure O(1) behavior.
		// 尝试往后看 1024 个 bucket
		stop := h.nevacuate + 1024
		if stop > newbit {
			stop = newbit
		}
		// 寻找没有搬迁的 bucket
		for h.nevacuate != stop && bucketEvacuated(t, h, h.nevacuate) {
			h.nevacuate++
		}
		
		// 现在 h.nevacuate 之前的 bucket 都被搬迁完毕
		
		// 所有的 buckets 搬迁完毕
		if h.nevacuate == newbit {
			// 清除老的 buckets
			h.oldbuckets = nil
			// 清除老的 overflow bucket
			// 回忆一下:[0] 表示当前 overflow bucket
			// [1] 表示 old overflow bucket
			if h.extra != nil {
				h.extra.overflow[1] = nil
			}
			// 清除正在扩容的标志位
			h.flags &^= sameSizeGrow
		}
	}
}

Les commentaires du code de la fonction d'évacuation sont très clairs. Il est facile de comprendre l'ensemble du processus de relocalisation en regardant le code et les commentaires, alors soyez patient.

Le but de la relocalisation est de déplacer les anciens compartiments vers de nouveaux compartiments. D'après l'explication précédente, nous savons qu'en réponse à la condition 1, le nombre de nouveaux buckets est doublé comme avant, et qu'en réponse à la condition 2, le nombre de nouveaux buckets est égal au précédent.

Pour la condition 2, lors du déplacement d'anciens godets vers de nouveaux godets, puisque le nombre de godets reste inchangé, ils peuvent être déplacés par numéro de série. Par exemple, les godets initialement situés au n° 0 seront toujours placés dans les godets n° 0 après le déplacement. au nouvel emplacement.

Pour la condition 1, ce n’est pas si simple. Le hachage de la clé doit être recalculé pour déterminer sur quel bucket il tombe. Par exemple, il s'avère que B = 5. Après avoir calculé le hachage de la clé, il vous suffit de regarder ses 5 bits inférieurs pour déterminer dans quel compartiment elle appartient. Après expansion, B devient 6, il faut donc examiner un bit supplémentaire. Ses 6 bits inférieurs déterminent sur quel compartiment la clé tombe. C'est ce qu'on appelle rehash.

insérer la description de l'image ici

Par conséquent, le numéro de séquence de compartiment d'une clé avant et après la migration peut être le même que celui d'origine, ou il peut être 2 ^ B (valeur B d'origine) plus 2 ^ B (valeur B d'origine), selon que le 6ème bit de la valeur de hachage est 0 ou 1.

Clarifions une autre question : si B augmente de 1 après expansion, cela signifie que le nombre total de seaux est doublé, et le seau d'origine n°1 est une « fission » en deux seaux.

Par exemple, original B = 2, les 3 chiffres inférieurs des valeurs de hachage de 2 clés du compartiment n°1 sont respectivement : 010, 110. Puisque B = 2 à l'origine, les 2 bits inférieurs 10déterminent qu'ils tombent dans le compartiment n° 2. Maintenant, B devient 3, donc 010ils 110tombent respectivement dans les compartiments n° 2 et 6.

insérer la description de l'image ici

Après avoir compris cela, nous l'utiliserons plus tard lorsque nous parlerons d'itération de carte.

Parlons de quelques points clés de la fonction de relocalisation :

La fonction d'évacuation effectue uniquement le déplacement d'un compartiment à la fois, elle doit donc parcourir toutes les cellules de ce compartiment et copier les cellules avec des valeurs vers le nouvel emplacement. Le seau est également relié au seau de trop-plein, qui doit également être déplacé. Par conséquent, il y aura deux couches de boucles, la couche externe traverse le seau et le seau de débordement, et la couche interne traverse toutes les cellules du seau. De telles boucles sont omniprésentes dans le code source de la carte, vous devez donc les comprendre parfaitement.

Le code source mentionne les pièces X et Y, ce qui signifie en fait que si la capacité est multipliée par 2, le nombre de seaux sera 2 fois supérieur au nombre d'origine. La première moitié des seaux est appelée pièces X, et la seconde moitié de les seaux sont appelés pièces Y. La clé dans un compartiment peut être divisée et tomber dans deux compartiments, un dans la partie X et un dans la partie Y. Par conséquent, avant de déplacer une cellule, vous devez savoir à quelle partie appartient la clé de la cellule. C'est très simple. Recalculez le hachage de la clé dans la cellule et "attendez" un bit supplémentaire pour décider dans quelle partie elle appartient. Cela a été expliqué en détail auparavant.

Il y a un cas particulier : il y a une clé, et à chaque fois qu'un hachage est calculé dessus, le résultat est différent. Cette clé est math.NaN()le résultat de , ce qui signifie not a numberque le type est float64. Lorsqu'il est utilisé comme clé d'une carte, il rencontrera un problème lors de son déplacement : sa valeur de hachage recalculée est différente de la valeur de hachage calculée lors de son insertion initiale dans la carte !

Vous avez peut-être pensé qu'une des conséquences de ceci est que cette clé ne sera jamais obtenue par l'opération Get ! Lorsque j'utilise m[math.NaN()]l'instruction, je ne trouve pas le résultat. Cette clé n'aura une chance d'apparaître qu'en parcourant toute la carte. math.NaN()Par conséquent, n’importe quel nombre de clés peut être inséré dans une carte .

Lorsque la relocalisation rencontre math.NaN()la clé, seul le bit le plus bas du tophash est utilisé pour déterminer s'il est alloué à la partie X ou à la partie Y (si l'expansion est 2 fois le nombre de compartiments d'origine). Si le bit le plus bas du tophash est 0, il est alloué à la partie X ; s'il est 1, il est alloué à la partie Y.

Ceci est obtenu en exploitant la valeur tophash et la valeur de hachage nouvellement calculée :

if top&1 != 0 {
    // top hash 最低位为 1
    // 新算出来的 hash 值的 B 位置 1
	hash |= newbit
} else {
    // 新算出来的 hash 值的 B 位置 0
	hash &^= newbit
}

// hash 值的 B 位为 0,则搬迁到 x part
// 当 B = 5时,newbit = 32,二进制低 6 位为 10 0000
useX = hash&newbit == 0

En fait, je peux déplacer une telle clé vers n'importe quel compartiment. Bien sûr, je dois toujours la déplacer vers les deux compartiments dans l'image de fission ci-dessus. Mais il y a des avantages à faire cela. Je l'expliquerai en détail plus tard lorsque nous parlerons de l'itération de la carte. Pour l'instant, sachez simplement comment cela est alloué.

Après avoir déterminé le godet cible à déplacer, l'opération de relocalisation est plus facile à réaliser. Copiez la valeur clé/valeur source à l’emplacement correspondant de la destination.

Définissez le tophash de la clé dans les compartiments d'origine sur evacuatedXou evacuatedY, indiquant qu'elle a été déplacée vers la partie x ou la partie y de la nouvelle carte. Le tophash de la nouvelle carte prend normalement les 8 bits les plus élevés de la valeur de hachage de la clé.

Jetons un coup d’œil macro aux changements avant et après l’expansion de la figure.

Avant l'expansion, B = 2, il y a 4 compartiments au total et les lowbits représentent les bits faibles de la valeur de hachage. Supposons que nous ne prêtions pas attention aux autres compartiments et que nous nous concentrions sur le compartiment n°2. Et en supposant qu'il y ait trop de débordement, une expansion égale se déclenche (correspondant à la condition précédente 2).

insérer la description de l'image ici

Une fois l'expansion terminée, le compartiment de débordement disparaît et les clés sont concentrées dans un seul compartiment, qui est plus compact et améliore l'efficacité de la recherche.

insérer la description de l'image ici

Supposons qu'une expansion multipliée par 2 soit déclenchée. Une fois l'expansion terminée, les clés des anciens compartiments sont divisées en deux nouveaux compartiments. Un dans la partie x et un dans la partie y. La base est constituée des bits faibles du hachage. Dans la nouvelle carte, 0-3elle est appelée partie x 4-7et partie y.

insérer la description de l'image ici

Notez que les deux chiffres ci-dessus ignorent le déplacement des autres compartiments et représentent la situation une fois que tous les compartiments ont été déplacés. En fait, nous savons que la réinstallation est un processus « graduel » et ne se fera pas d’un seul coup. Par conséquent, pendant le processus de relocalisation, le pointeur oldbuckets pointera toujours vers l'ancien []bmap d'origine, et la valeur tophash de la clé qui a été déplacée sera une valeur d'état, indiquant la destination de relocalisation de la clé.

Je suppose que tu aimes

Origine blog.csdn.net/zy_dreamer/article/details/132799777
conseillé
Classement