определение
в голангеmap
обычно используетсяhashtable, базовая реализацияhmap
, поддерживает несколькоbucket
массив, обычно каждыйbucket
Сохраните 8 комплектов кв пар, если
Более 8 (при возникновении конфликта хэшей) он будет вextra
в полевой структуреoverflow
, использование метода цепных адресов было расширено.
Первый взглядhmap
Структура:
type hmap struct {
count int // 元素的个数
flags uint8 // 标记读写状态,主要是做竞态检测,避免并发读写
B uint8 // 可以容纳 2 ^ N 个bucket
noverflow uint16 // 溢出的bucket个数
hash0 uint32 // hash 因子
buckets unsafe.Pointer // 指向数组buckets的指针
oldbuckets unsafe.Pointer // growing 时保存原buckets的指针
nevacuate uintptr // growing 时已迁移的个数
extra *mapextra
}
type mapextra struct {
overflow *[]*bmap
oldoverflow *[]*bmap
nextOverflow *bmap
}
bucket
структура:
// A bucket for a Go map.
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 // 记录着每个key的高8个bits
// Followed by bucketCnt keys and then bucketCnt elems.
// NOTE: packing all the keys together and then all the elems together makes the
// code a bit more complicated than alternating key/elem/key/elem/... but it allows
// us to eliminate padding which would be needed for, e.g., map[int64]int8.
// Followed by an overflow pointer.
}
вkv
Пары организованы в формате ключ0/ключ1/ключ2/...значение0/значение1/значение2/.... Хотя их сложнее сохранить, чем пару ключ/значение, она избегает чтения с фиксированной длиной из-за требования ЦП Выравнивание байтов, приводящее к неиспользуемому пространству.
Инициализировать && Вставить
package main
func main() {
a := map[string]int{"one": 1, "two": 2, "three": 3}
_ = a["one"]
}
Инициализировать карту из 3 ключей/значений
TEXT main.main(SB) /Users/such/gomodule/runtime/main.go
=> main.go:3 0x10565fb* 4881ec70010000 sub rsp, 0x170
main.go:3 0x1056602 4889ac2468010000 mov qword ptr [rsp+0x168], rbp
main.go:3 0x105660a 488dac2468010000 lea rbp, ptr [rsp+0x168]
main.go:4 0x105664b 488b6d00 mov rbp, qword ptr [rbp]
main.go:4 0x105666d e8de9cfeff call $runtime.fastrand
main.go:4 0x1056672 488b442450 mov rax, qword ptr [rsp+0x50]
main.go:4 0x1056677 8400 test byte ptr [rax], al
main.go:4 0x10566c6 48894c2410 mov qword ptr [rsp+0x10], rcx
main.go:4 0x10566cb 4889442418 mov qword ptr [rsp+0x18], rax
main.go:4 0x10566d0 e80b8efbff call $runtime.mapassign_faststr
main.go:4 0x1056726 48894c2410 mov qword ptr [rsp+0x10], rcx
main.go:4 0x105672b 4889442418 mov qword ptr [rsp+0x18], rax
main.go:4 0x1056730 e8ab8dfbff call $runtime.mapassign_faststr
main.go:4 0x1056786 4889442410 mov qword ptr [rsp+0x10], rax
main.go:4 0x105678b 48894c2418 mov qword ptr [rsp+0x18], rcx
main.go:4 0x1056790 e84b8dfbff call $runtime.mapassign_faststr
(Части опущены) Видно, что объявление вызывается три раза подрядcall $runtime.mapassign_faststr
Добавить пару ключ-значение
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if h == nil {
panic(plainError("assignment to entry in nil map"))
}
if raceenabled {
callerpc := getcallerpc()
pc := funcPC(mapassign)
racewritepc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
// 看到这里,发现和之前 slice 声明时一样,都会做竞态检测
if msanenabled {
msanread(key, t.key.size)
}
// 这里就是并发读写map时,panic的地方
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
// t 是 map 的类型,因此在编译时,可以确定key的类型,继而确定hash算法。
alg := t.key.alg
hash := alg.hash(key, uintptr(h.hash0))
// 设置flag为writing
h.flags ^= hashWriting
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again: // 重新计算bucket的hash
bucket := hash & bucketMask(h.B)
if h.growing() {
growWork(t, h, bucket)
}
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
top := tophash(hash)
var inserti *uint8
var insertk unsafe.Pointer
var elem unsafe.Pointer
bucketloop:
// 遍历找到bucket
for {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
if isEmpty(b.tophash[i]) && inserti == nil {
inserti = &b.tophash[i]
insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
}
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
// equal 方法也是根据不同的数据类型,在编译时确定
if !alg.equal(key, k) {
continue
}
// map 中已经存在 key,修改 key 对应的 value
if t.needkeyupdate() {
typedmemmove(t.key, k, key)
}
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
goto done
}
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
// Did not find mapping for key. Allocate new cell & add entry.
// If we hit the max load factor or we have too many overflow buckets,
// and we're not already in the middle of growing, start growing.
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again // Growing the table invalidates everything, so try again
}
if inserti == nil
// 如果没有找到插入的node,即当前所有桶都已放满
newb := h.newoverflow(t, b)
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
elem = add(insertk, bucketCnt*uintptr(t.keysize))
}
// store new key/elem at insert position
if t.indirectkey() {
kmem := newobject(t.key)
*(*unsafe.Pointer)(insertk) = kmem
insertk = kmem
}
if t.indirectelem() {
vmem := newobject(t.elem)
*(*unsafe.Pointer)(elem) = vmem
}
typedmemmove(t.key, insertk, key)
*inserti = top
h.count++
done:
// 再次检查(双重校验锁的思路)是否并发写
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
h.flags &^= hashWriting
if t.indirectelem() {
elem = *((*unsafe.Pointer)(elem))
}
return elem
}
найти
TEXT main.main(SB) /Users/such/gomodule/runtime/main.go
=> main.go:6 0x10567a9* 488d0550e10000 lea rax, ptr [rip+0xe150]
main.go:6 0x10567c5 4889442410 mov qword ptr [rsp+0x10], rax
main.go:6 0x10567ca 48c744241803000000 mov qword ptr [rsp+0x18], 0x3
main.go:6 0x10567d3 e89885fbff call $runtime.mapaccess1_faststr
При поиске ключа в карте среда выполнения вызываетmapaccess1
метод, аналогичный при добавлении
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if raceenabled && h != nil {
callerpc := getcallerpc()
pc := funcPC(mapaccess1)
racereadpc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
if msanenabled && h != nil {
msanread(key, t.key.size)
}
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.key.alg.hash(key, 0) // see issue 23734
}
return unsafe.Pointer(&zeroVal[0])
}
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}
alg := t.key.alg
hash := alg.hash(key, uintptr(h.hash0))
m := bucketMask(h.B)
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
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
}
}
top := tophash(hash)
bucketloop:
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
// 如果找到 key,就返回 key 指向的 value 指针的值,
// 在计算 ptr 的时候,初始位置当前bmap, 偏移量 offset,是一个 bmap 结构体的大小,但对于amd64架构,
// 还需要考虑字节对齐,即 8 字节对齐(dataOffset)+ 8个key的大小 + i (当前索引) 个value的大小
if alg.equal(key, k) {
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
if t.indirectelem() {
e = *((*unsafe.Pointer)(e))
}
return e
}
}
}
// 如果未找到的话,返回零对象的引用的指针
return unsafe.Pointer(&zeroVal[0])
}
В пакете карты есть аналогичный метод,mapaccess2
проверено в_, ok := a["one"]
Обычно он используется для определения того, существует ключ или нет. На самом деле это видно по возвращаемому значению функции.
Growing
Как и в случае со слайсами, когда элементы карты продолжают расти, каждое ведро в крайних случаях будет иметь много переполнения, вырождаясь в связанный список, требующий повторного хеширования. Общее расширениеh.count > loadFactor(2^B)
.
Коэффициент загрузки обычно: емкость/количество бакетов, коэффициент загрузки golang loadFactorNum/loadFactorDen = 6,5, почему бы не выбрать 1, как и dictentry Redis, может сохранять только набор пар ключ-значение, в golang бакет можно сохранить под нормальные обстоятельства 8 наборов пар ключ-значение;
Тогда почему выбрано значение 6,5, автор приводит набор данных.
loadFactor | %overflow | bytes/entry | hitprobe | missprobe |
---|---|---|---|---|
4.00 | 2.13 | 20.77 | 3.00 | 4.00 |
4.50 | 4.05 | 17.30 | 3.25 | 4.50 |
5.00 | 6.85 | 14.77 | 3.50 | 5.00 |
5.50 | 10.55 | 12.94 | 3.75 | 5.50 |
6.00 | 15.27 | 11.67 | 4.00 | 6.00 |
6.50 | 20.90 | 10.79 | 4.25 | 6.50 |
7.00 | 27.14 | 10.15 | 4.50 | 7.00 |
7.50 | 34.03 | 9.73 | 4.75 | 7.50 |
8.00 | 41.10 | 9.40 | 5.00 | 8.00 |
loadFactor: коэффициент нагрузки;
%overflow: скорость переполнения, доля сегментов переполнения;
байт/запись: соотношение байтов, занимаемых каждой парой ключ/значение;
hitprobe: найти среднее количество поисков существующего ключа;
missprobe: найти среднее количество поисков несуществующего ключа;
Обычно, когда коэффициент загрузки > 6,5, это среднее количество пар ключ-значение, хранящихся в каждом сегменте.
Расширение (миграция) происходит при наличии более 6,5 или количестве переполнений > 2^15. Делится на два случая:
Первый тип: поскольку карта постоянно вставляется и удаляется, хранилище ключей и значений в корзине неравномерно, а использование памяти очень низкое, поэтому требуется миграция. (Примечание: количество ведер не будет увеличено)
Второе: правда, из-за расширения, вызванного чрезмерным коэффициентом нагрузки, ковш увеличен в два раза по сравнению с первоначальным ковшом.
Независимо от того, какая из вышеперечисленных перефразировок называетсяhashGrow
метод:
- Определите указатель на массив ведер в исходном hmap
- Создайте массив ведер и установите его как поле ведра hmap
- Укажите oldoverflow в extra на переполнение и переполнение на ноль
- Если он растет, начните добавочную миграцию, в
growWork
Метод заключается в переносе ключа/значения в корзину. - После завершения всех миграций освободите память
Уведомление:Когда golang rehash, как Redis, он использует прогрессивный rehash, Вместо того, чтобы мигрировать все сегменты за один раз, миграция ключей распределяется на каждую вставку или удаление. Все ключи/значения в ведре мигрируют в релиз oldbucket и extra.oldoverflow (по возможности не используйте map для хранения большого количества данных; лучше объявить cap за один раз при инициализации, чтобы избежать частое расширение)
Удалить
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
...省略
search:
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
if t.indirectkey() {
*(*unsafe.Pointer)(k) = nil
} else if t.key.ptrdata != 0 {
memclrHasPointers(k, t.key.size)
}
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
if t.indirectelem() {
*(*unsafe.Pointer)(e) = nil
} else if t.elem.ptrdata != 0 {
memclrHasPointers(e, t.elem.size)
} else {
memclrNoHeapPointers(e, t.elem.size)
}
b.tophash[i] = emptyOne
if i == bucketCnt-1 {
if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
goto notLast
}
} else {
if b.tophash[i+1] != emptyRest {
goto notLast
}
}
for {
b.tophash[i] = emptyRest
if i == 0 {
if b == bOrig {
break // beginning of initial bucket, we're done.
}
// Find previous bucket, continue at its last entry.
c := b
for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
}
i = bucketCnt - 1
} else {
i--
}
if b.tophash[i] != emptyOne {
break
}
}
notLast:
h.count--
break search
}
}
...
}
ключ и значение, если это тип значения, напрямую установить его в nil, если это указатель, очистить n байтов от позиции ptr;
Потом при удалении просто ставим отметку пустой на позиции соответствующей tophash(b.tophash[i] = emptyOne
), нет реального освобождения места в памяти, т.к. частое применение и освобождение места в памяти дорого обходится, если очень хочется освободить, то можно полагаться только на GC;
Если ведро заканчивается несколькими маркерами emptyOne, в конце концов, он устанавливается на маркер emptyRest. И emptyOne, и emptyRest являются пустыми маркерами. Разница между emptyRest заключается в том, что маркер пуст в старшем бите индекса и ведре переполнения.
Следует учитывать, чтобы уменьшить количество поисков, когда операции вставки и удаления должны найти место для последующего повторного использования.
предположение
Проведите две группы экспериментов, первая группа: добавьте k/v после предварительного распределения общей емкости карты; другая группа: инициализируйте карту с нулевой емкостью и добавьте ее.
package main
import "testing"
var count int = 100000
func addition(m map[int]int) map[int]int {
for i := 0; i < count; i++ {
m[i] = i
}
return m
}
func BenchmarkGrows(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
m := make(map[int]int)
addition(m)
}
}
func BenchmarkNoGrows(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
m := make(map[int]int, count)
addition(m)
}
}
$ go test -bench=. ./
goos: darwin
goarch: amd64
# benchmark名字 -CPU数 执行次数 平均执行时间ns
BenchmarkGrows-4 200 8298505 ns/op
BenchmarkNoGrows-4 300 4627118 ns/op
PASS
ok _/Users/such/gomodule/runtime 4.401s
Среднее время выполнения обращений с предопределенной мощностью на 80 % быстрее, чем с неопределенной емкостью ---Копирование и повторное хеширование данных дорого обходится при масштабировании!
Посмотрите на количество выделений памяти:
$ go test -bench=. -benchmem ./
goos: darwin
goarch: amd64
# benchmark名字 -CPU数 执行次数 平均执行时间ns 每次分配内存大小 每次内存分配次数
BenchmarkGrows-4 200 9265553 ns/op 5768155 B/op 4010 allocs/op
BenchmarkNoGrows-4 300 4855000 ns/op 2829115 B/op 1678 allocs/op
PASS
ok _/Users/such/gomodule/runtime 4.704s
Два метода выполняются одинаковое количество раз, а количество GC удваивается.
func main() {
for i := 0; i < 5; i++ {
n := make(map[int]int, count)
addition(n)
//m := make(map[int]int)
//addition(m)
}
}
// 第一组,预分配
$ go build -o growth && GODEBUG=gctrace=1 ./growth
gc 1 @0.006s 0%: 0.002+0.091+0.015 ms clock, 0.011+0.033/0.011/0.088+0.060 ms cpu, 5->5->2 MB, 6 MB goal, 4 P
gc 2 @0.012s 0%: 0.001+0.041+0.002 ms clock, 0.007+0.032/0.007/0.033+0.009 ms cpu, 5->5->2 MB, 6 MB goal, 4 P
gc 3 @0.017s 0%: 0.002+0.090+0.010 ms clock, 0.008+0.035/0.006/0.084+0.041 ms cpu, 5->5->2 MB, 6 MB goal, 4 P
gc 4 @0.022s 0%: 0.001+0.056+0.008 ms clock, 0.007+0.026/0.003/0.041+0.034 ms cpu, 5->5->2 MB, 6 MB goal, 4 P
// 第二组,未分配
$ go build -o growth && GODEBUG=gctrace=1 ./growth
gc 1 @0.005s 0%: 0.001+0.10+0.001 ms clock, 0.007+0.076/0.004/0.13+0.007 ms cpu, 5->5->3 MB, 6 MB goal, 4 P
gc 2 @0.012s 0%: 0.002+0.071+0.010 ms clock, 0.008+0.016/0.010/0.075+0.040 ms cpu, 5->5->0 MB, 7 MB goal, 4 P
gc 3 @0.015s 0%: 0.001+0.13+0.009 ms clock, 0.007+0.006/0.037/0.082+0.036 ms cpu, 4->5->3 MB, 5 MB goal, 4 P
gc 4 @0.021s 0%: 0.001+0.13+0.009 ms clock, 0.007+0.040/0.007/0.058+0.038 ms cpu, 6->6->1 MB, 7 MB goal, 4 P
gc 5 @0.024s 0%: 0.001+0.084+0.001 ms clock, 0.005+0.036/0.006/0.052+0.006 ms cpu, 4->4->3 MB, 5 MB goal, 4 P
gc 6 @0.030s 0%: 0.002+0.075+0.001 ms clock, 0.008+0.056/0.004/0.072+0.007 ms cpu, 6->6->1 MB, 7 MB goal, 4 P
gc 7 @0.033s 0%: 0.013+0.11+0.003 ms clock, 0.053+0.047/0.013/0.075+0.012 ms cpu, 4->4->3 MB, 5 MB goal, 4 P
gc 8 @0.041s 0%: 0.002+0.073+0.024 ms clock, 0.008+0.033/0.010/0.067+0.097 ms cpu, 6->6->1 MB, 7 MB goal, 4 P
gc 9 @0.043s 0%: 0.001+0.067+0.001 ms clock, 0.006+0.046/0.003/0.070+0.006 ms cpu, 4->4->3 MB, 5 MB goal, 4 P
Есть карта на 10млн кв, тест при каких обстоятельствах память восстановится
package main
var count = 10000000
var dict = make(map[int]int, count)
func addition() {
for i := 0; i < count; i++ {
dict[i] = i
}
}
func clear() {
for k := range dict {
delete(dict, k)
}
//dict = nil
}
func main() {
addition()
clear()
debug.FreeOSMemory()
}
$ go build -o clear && GODEBUG=gctrace=1 ./clear
gc 1 @0.007s 0%: 0.006+0.12+0.015 ms clock, 0.025+0.037/0.038/0.12+0.061 ms cpu, 306->306->306 MB, 307 MB goal, 4 P
gc 2 @0.963s 0%: 0.004+1.0+0.025 ms clock, 0.017+0/0.96/0.48+0.10 ms cpu, 307->307->306 MB, 612 MB goal, 4 P
gc 3 @1.381s 0%: 0.004+0.081+0.003 ms clock, 0.018+0/0.051/0.086+0.013 ms cpu, 309->309->306 MB, 612 MB goal, 4 P (forced)
scvg-1: 14 MB released
scvg-1: inuse: 306, idle: 77, sys: 383, released: 77, consumed: 306 (MB)
Удалены все kvs, размер кучи не изменился (цель)
func clear() {
for k := range dict {
delete(dict, k)
}
dict = nil
}
$ go build -o clear && GODEBUG=gctrace=1 ./clear
gc 1 @0.006s 0%: 0.004+0.12+0.010 ms clock, 0.019+0.035/0.016/0.17+0.043 ms cpu, 306->306->306 MB, 307 MB goal, 4 P
gc 2 @0.942s 0%: 0.003+1.0+0.010 ms clock, 0.012+0/0.85/0.54+0.043 ms cpu, 307->307->306 MB, 612 MB goal, 4 P
gc 3 @1.321s 0%: 0.003+0.072+0.002 ms clock, 0.013+0/0.050/0.090+0.010 ms cpu, 309->309->0 MB, 612 MB goal, 4 P (forced)
scvg-1: 319 MB released
scvg-1: inuse: 0, idle: 383, sys: 383, released: 383, consumed: 0 (MB)
После очистки установите его на ноль, чтобы освободить память. (Он запускает runtime.GC() каждые 2 минуты, а очистка освобождает память каждые 5 минут. На самом деле, нет необходимости слишком беспокоиться о том, действительно ли он освобожден. На самом деле он не освобождается для возможного повторного использования позже.Но иногда, когда требуется настоящее освобождение, знайте, что делать, чтобы решить проблему.)
Reference
Карта:gowave.org/двуспальная кровать/runtime…Ориентир:Dave.Cheney.net/2013/06/30/…
Трассировка:dave.cheney.net/tag/godebug
FreeOsMemory:golang.org/pkg/runtime…