Skip to content

Commit 220168a

Browse files
committed
runtime: implement growing hashmaps
Add support for growing hashmaps beyond their initial size.
1 parent 935c92e commit 220168a

File tree

3 files changed

+67
-19
lines changed

3 files changed

+67
-19
lines changed

src/runtime/hashmap.go

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,20 @@ func hashmapLen(m *hashmap) int {
8989
// Set a specified key to a given value. Grow the map if necessary.
9090
//go:nobounds
9191
func hashmapSet(m *hashmap, key unsafe.Pointer, value unsafe.Pointer, hash uint32, keyEqual func(x, y unsafe.Pointer, n uintptr) bool) {
92+
tophash := hashmapTopHash(hash)
93+
94+
if m.buckets == nil {
95+
// No bucket was allocated yet, do so now.
96+
m.buckets = unsafe.Pointer(hashmapInsertIntoNewBucket(m, key, value, tophash))
97+
return
98+
}
99+
92100
numBuckets := uintptr(1) << m.bucketBits
93101
bucketNumber := (uintptr(hash) & (numBuckets - 1))
94102
bucketSize := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*8 + uintptr(m.valueSize)*8
95103
bucketAddr := uintptr(m.buckets) + bucketSize*bucketNumber
96104
bucket := (*hashmapBucket)(unsafe.Pointer(bucketAddr))
97-
98-
tophash := hashmapTopHash(hash)
105+
var lastBucket *hashmapBucket
99106

100107
// See whether the key already exists somewhere.
101108
var emptySlotKey unsafe.Pointer
@@ -104,9 +111,9 @@ func hashmapSet(m *hashmap, key unsafe.Pointer, value unsafe.Pointer, hash uint3
104111
for bucket != nil {
105112
for i := uintptr(0); i < 8; i++ {
106113
slotKeyOffset := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*uintptr(i)
107-
slotKey := unsafe.Pointer(bucketAddr + slotKeyOffset)
114+
slotKey := unsafe.Pointer(uintptr(unsafe.Pointer(bucket)) + slotKeyOffset)
108115
slotValueOffset := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*8 + uintptr(m.valueSize)*uintptr(i)
109-
slotValue := unsafe.Pointer(bucketAddr + slotValueOffset)
116+
slotValue := unsafe.Pointer(uintptr(unsafe.Pointer(bucket)) + slotValueOffset)
110117
if bucket.tophash[i] == 0 && emptySlotKey == nil {
111118
// Found an empty slot, store it for if we couldn't find an
112119
// existing slot.
@@ -115,24 +122,45 @@ func hashmapSet(m *hashmap, key unsafe.Pointer, value unsafe.Pointer, hash uint3
115122
emptySlotTophash = &bucket.tophash[i]
116123
}
117124
if bucket.tophash[i] == tophash {
118-
// Could be an existing value that's the same.
125+
// Could be an existing key that's the same.
119126
if keyEqual(key, slotKey, uintptr(m.keySize)) {
120127
// found same key, replace it
121128
memcpy(slotValue, value, uintptr(m.valueSize))
122129
return
123130
}
124131
}
125132
}
133+
lastBucket = bucket
126134
bucket = bucket.next
127135
}
128-
if emptySlotKey != nil {
129-
m.count++
130-
memcpy(emptySlotKey, key, uintptr(m.keySize))
131-
memcpy(emptySlotValue, value, uintptr(m.valueSize))
132-
*emptySlotTophash = tophash
136+
if emptySlotKey == nil {
137+
// Add a new bucket to the bucket chain.
138+
// TODO: rebalance if necessary to avoid O(n) insert and lookup time.
139+
lastBucket.next = (*hashmapBucket)(hashmapInsertIntoNewBucket(m, key, value, tophash))
133140
return
134141
}
135-
panic("todo: hashmap: grow bucket")
142+
m.count++
143+
memcpy(emptySlotKey, key, uintptr(m.keySize))
144+
memcpy(emptySlotValue, value, uintptr(m.valueSize))
145+
*emptySlotTophash = tophash
146+
}
147+
148+
// hashmapInsertIntoNewBucket creates a new bucket, inserts the given key and
149+
// value into the bucket, and returns a pointer to this bucket.
150+
func hashmapInsertIntoNewBucket(m *hashmap, key, value unsafe.Pointer, tophash uint8) *hashmapBucket {
151+
bucketBufSize := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*8 + uintptr(m.valueSize)*8
152+
bucketBuf := alloc(bucketBufSize)
153+
// Insert into the first slot, which is empty as it has just been allocated.
154+
slotKeyOffset := unsafe.Sizeof(hashmapBucket{})
155+
slotKey := unsafe.Pointer(uintptr(bucketBuf) + slotKeyOffset)
156+
slotValueOffset := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*8
157+
slotValue := unsafe.Pointer(uintptr(bucketBuf) + slotValueOffset)
158+
m.count++
159+
memcpy(slotKey, key, uintptr(m.keySize))
160+
memcpy(slotValue, value, uintptr(m.valueSize))
161+
bucket := (*hashmapBucket)(bucketBuf)
162+
bucket.tophash[0] = tophash
163+
return bucket
136164
}
137165

138166
// Get the value of a specified key, or zero the value if not found.

testdata/map.go

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@ func main() {
5050

5151
// test preallocated map
5252
squares := make(map[int]int, 200)
53-
for i := 0; i < 100; i++ {
54-
squares[i] = i*i
55-
for j := 0; j <= i; j++ {
56-
if v := squares[j]; v != j*j {
57-
println("unexpected value read back from squares map:", j, v)
58-
}
59-
}
60-
}
53+
testBigMap(squares, 100)
6154
println("tested preallocated map")
55+
56+
// test growing maps
57+
squares = make(map[int]int, 0)
58+
testBigMap(squares, 10)
59+
squares = make(map[int]int, 20)
60+
testBigMap(squares, 40)
61+
println("tested growing of a map")
6262
}
6363

6464
func readMap(m map[string]int, key string) {
@@ -73,3 +73,22 @@ func lookup(m map[string]int, key string) {
7373
value, ok := m[key]
7474
println("lookup with comma-ok:", key, value, ok)
7575
}
76+
77+
func testBigMap(squares map[int]int, n int) {
78+
for i := 0; i < n; i++ {
79+
if len(squares) != i {
80+
println("unexpected length:", len(squares), "at i =", i)
81+
}
82+
squares[i] = i*i
83+
for j := 0; j <= i; j++ {
84+
if v, ok := squares[j]; !ok || v != j*j {
85+
if !ok {
86+
println("key not found in squares map:", j)
87+
} else {
88+
println("unexpected value read back from squares map:", j, v)
89+
}
90+
return
91+
}
92+
}
93+
}
94+
}

testdata/map.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,4 @@ true false 0
5555
4321
5656
5555
5757
tested preallocated map
58+
tested growing of a map

0 commit comments

Comments
 (0)