Skip to content

Commit 94e44a9

Browse files
committed
runtime: preallocate some overflow buckets
When allocating a non-small array of buckets for a map, also preallocate some overflow buckets. The estimate of the number of overflow buckets is based on a simulation of putting mid=(low+high)/2 elements into a map, where low is the minimum number of elements needed to reach this value of b (according to overLoadFactor), and high is the maximum number of elements possible to put in this value of b (according to overLoadFactor). This estimate is surprisingly reliable and accurate. The number of overflow buckets needed is quadratic, for a fixed value of b. Using this mid estimate means that we will overallocate a few too many overflow buckets when the actual number of elements is near low, and underallocate significantly too few overflow buckets when the actual number of elements is near high. The mechanism introduced in this CL can be re-used for other overflow bucket optimizations. For example, given an initial size hint, we could estimate quite precisely the number of overflow buckets. This is #19931. We could also change from "non-nil means end-of-list" to "pointer-to-hmap.buckets means end-of-list", and then create a linked list of reusable overflow buckets when they are freed by map growth. That is #19992. We could also use a similar mechanism to do bulk allocation of overflow buckets. All these uses can co-exist with only the one additional pointer in mapextra, given a little care. name old time/op new time/op delta MapPopulate/1-8 60.1ns ± 2% 60.3ns ± 2% ~ (p=0.278 n=19+20) MapPopulate/10-8 577ns ± 1% 578ns ± 1% ~ (p=0.140 n=20+20) MapPopulate/100-8 8.06µs ± 1% 8.19µs ± 1% +1.67% (p=0.000 n=20+20) MapPopulate/1000-8 104µs ± 1% 104µs ± 1% ~ (p=0.317 n=20+20) MapPopulate/10000-8 891µs ± 1% 888µs ± 1% ~ (p=0.101 n=19+20) MapPopulate/100000-8 8.61ms ± 1% 8.58ms ± 0% -0.34% (p=0.009 n=20+17) name old alloc/op new alloc/op delta MapPopulate/1-8 0.00B 0.00B ~ (all equal) MapPopulate/10-8 179B ± 0% 179B ± 0% ~ (all equal) MapPopulate/100-8 3.33kB ± 0% 3.38kB ± 0% +1.48% (p=0.000 n=20+16) MapPopulate/1000-8 55.5kB ± 0% 53.4kB ± 0% -3.84% (p=0.000 n=19+20) MapPopulate/10000-8 432kB ± 0% 428kB ± 0% -1.06% (p=0.000 n=19+20) MapPopulate/100000-8 3.65MB ± 0% 3.62MB ± 0% -0.70% (p=0.000 n=20+20) name old allocs/op new allocs/op delta MapPopulate/1-8 0.00 0.00 ~ (all equal) MapPopulate/10-8 1.00 ± 0% 1.00 ± 0% ~ (all equal) MapPopulate/100-8 18.0 ± 0% 17.0 ± 0% -5.56% (p=0.000 n=20+20) MapPopulate/1000-8 96.0 ± 0% 72.6 ± 1% -24.38% (p=0.000 n=20+20) MapPopulate/10000-8 625 ± 0% 319 ± 0% -48.86% (p=0.000 n=20+20) MapPopulate/100000-8 6.23k ± 0% 4.00k ± 0% -35.79% (p=0.000 n=20+20) Change-Id: I01f41cb1374bdb99ccedbc00d04fb9ae43daa204 Reviewed-on: https://go-review.googlesource.com/40979 Run-TryBot: Josh Bleecher Snyder <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Keith Randall <[email protected]>
1 parent 2abd91e commit 94e44a9

File tree

1 file changed

+67
-4
lines changed

1 file changed

+67
-4
lines changed

src/runtime/hashmap.go

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ type mapextra struct {
130130
// overflow[1] contains overflow buckets for hmap.oldbuckets.
131131
// The indirection allows to store a pointer to the slice in hiter.
132132
overflow [2]*[]*bmap
133+
134+
// nextOverflow holds a pointer to a free overflow bucket.
135+
nextOverflow *bmap
133136
}
134137

135138
// A bucket for a Go map.
@@ -205,7 +208,24 @@ func (h *hmap) incrnoverflow() {
205208
}
206209

207210
func (h *hmap) newoverflow(t *maptype, b *bmap) *bmap {
208-
ovf := (*bmap)(newobject(t.bucket))
211+
var ovf *bmap
212+
if h.extra != nil && h.extra.nextOverflow != nil {
213+
// We have preallocated overflow buckets available.
214+
// See makeBucketArray for more details.
215+
ovf = h.extra.nextOverflow
216+
if ovf.overflow(t) == nil {
217+
// We're not at the end of the preallocated overflow buckets. Bump the pointer.
218+
h.extra.nextOverflow = (*bmap)(add(unsafe.Pointer(ovf), uintptr(t.bucketsize)))
219+
} else {
220+
// This is the last preallocated overflow bucket.
221+
// Reset the overflow pointer on this bucket,
222+
// which was set to a non-nil sentinel value.
223+
ovf.setoverflow(t, nil)
224+
h.extra.nextOverflow = nil
225+
}
226+
} else {
227+
ovf = (*bmap)(newobject(t.bucket))
228+
}
209229
h.incrnoverflow()
210230
if t.bucket.kind&kindNoPointers != 0 {
211231
h.createOverflow()
@@ -287,8 +307,14 @@ func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {
287307
// if B == 0, the buckets field is allocated lazily later (in mapassign)
288308
// If hint is large zeroing this memory could take a while.
289309
buckets := bucket
310+
var extra *mapextra
290311
if B != 0 {
291-
buckets = newarray(t.bucket, 1<<B)
312+
var nextOverflow *bmap
313+
buckets, nextOverflow = makeBucketArray(t, B)
314+
if nextOverflow != nil {
315+
extra = new(mapextra)
316+
extra.nextOverflow = nextOverflow
317+
}
292318
}
293319

294320
// initialize Hmap
@@ -297,7 +323,7 @@ func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {
297323
}
298324
h.count = 0
299325
h.B = B
300-
h.extra = nil
326+
h.extra = extra
301327
h.flags = 0
302328
h.hash0 = fastrand()
303329
h.buckets = buckets
@@ -883,6 +909,36 @@ next:
883909
goto next
884910
}
885911

912+
func makeBucketArray(t *maptype, b uint8) (buckets unsafe.Pointer, nextOverflow *bmap) {
913+
base := uintptr(1 << b)
914+
nbuckets := base
915+
// For small b, overflow buckets are unlikely.
916+
// Avoid the overhead of the calculation.
917+
if b >= 4 {
918+
// Add on the estimated number of overflow buckets
919+
// required to insert the median number of elements
920+
// used with this value of b.
921+
nbuckets += 1 << (b - 4)
922+
sz := t.bucket.size * nbuckets
923+
up := roundupsize(sz)
924+
if up != sz {
925+
nbuckets = up / t.bucket.size
926+
}
927+
}
928+
buckets = newarray(t.bucket, int(nbuckets))
929+
if base != nbuckets {
930+
// We preallocated some overflow buckets.
931+
// To keep the overhead of tracking these overflow buckets to a minimum,
932+
// we use the convention that if a preallocated overflow bucket's overflow
933+
// pointer is nil, then there are more available by bumping the pointer.
934+
// We need a safe non-nil pointer for the last overflow bucket; just use buckets.
935+
nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
936+
last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
937+
last.setoverflow(t, (*bmap)(buckets))
938+
}
939+
return buckets, nextOverflow
940+
}
941+
886942
func hashGrow(t *maptype, h *hmap) {
887943
// If we've hit the load factor, get bigger.
888944
// Otherwise, there are too many overflow buckets,
@@ -893,7 +949,8 @@ func hashGrow(t *maptype, h *hmap) {
893949
h.flags |= sameSizeGrow
894950
}
895951
oldbuckets := h.buckets
896-
newbuckets := newarray(t.bucket, 1<<(h.B+bigger))
952+
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger)
953+
897954
flags := h.flags &^ (iterator | oldIterator)
898955
if h.flags&iterator != 0 {
899956
flags |= oldIterator
@@ -914,6 +971,12 @@ func hashGrow(t *maptype, h *hmap) {
914971
h.extra.overflow[1] = h.extra.overflow[0]
915972
h.extra.overflow[0] = nil
916973
}
974+
if nextOverflow != nil {
975+
if h.extra == nil {
976+
h.extra = new(mapextra)
977+
}
978+
h.extra.nextOverflow = nextOverflow
979+
}
917980

918981
// the actual copying of the hash table data is done incrementally
919982
// by growWork() and evacuate().

0 commit comments

Comments
 (0)