Skip to content

Commit

Permalink
maps,runtime: improve maps.Keys
Browse files Browse the repository at this point in the history
name     old time/op    new time/op    delta
Keys-10    8.65ms ± 0%    7.06ms ± 0%  -18.41%  (p=0.000 n=9+9)

name     old alloc/op   new alloc/op   delta
Keys-10    58.2kB ± 1%    47.4kB ± 2%  -18.54%  (p=0.000 n=10+10)

name     old allocs/op  new allocs/op  delta
Keys-10      0.00           0.00          ~     (all equal)

Change-Id: Ia7061c37be89e906e79bc3ef3bb1ef0d7913f9f6
Reviewed-on: https://go-review.googlesource.com/c/go/+/481435
Reviewed-by: Keith Randall <[email protected]>
TryBot-Result: Gopher Robot <[email protected]>
Reviewed-by: Keith Randall <[email protected]>
Reviewed-by: Heschi Kreinick <[email protected]>
Run-TryBot: xie cui <[email protected]>
  • Loading branch information
cuiweixie authored and randall77 committed May 19, 2023
1 parent 251daf4 commit f283cba
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 4 deletions.
4 changes: 3 additions & 1 deletion src/go/build/deps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ var depsRules = `
internal/goexperiment, internal/goos,
internal/goversion, internal/nettrace, internal/platform,
log/internal,
maps, unicode/utf8, unicode/utf16, unicode,
unicode/utf8, unicode/utf16, unicode,
unsafe;
# slices depends on unsafe for overlapping check.
Expand All @@ -57,6 +57,8 @@ var depsRules = `
internal/goarch, unsafe
< internal/abi;
unsafe < maps;
# RUNTIME is the core runtime group of packages, all of them very light-weight.
internal/abi, internal/cpu, internal/goarch,
internal/coverage/rtcov, internal/godebugs, internal/goexperiment,
Expand Down
15 changes: 12 additions & 3 deletions src/maps/maps.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@
// Package maps defines various functions useful with maps of any type.
package maps

import "unsafe"

// keys is implemented in the runtime package.
//
//go:noescape
func keys(m any, slice unsafe.Pointer)

// Keys returns the keys of the map m.
// The keys will be in an indeterminate order.
func Keys[M ~map[K]V, K comparable, V any](m M) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
keys(m, unsafe.Pointer(&r))
return r
}

func keysForBenchmarking[M ~map[K]V, K comparable, V any](m M, s []K) {
keys(m, unsafe.Pointer(&s))
}

// Values returns the values of the map m.
// The values will be in an indeterminate order.
func Values[M ~map[K]V, K comparable, V any](m M) []V {
Expand Down
32 changes: 32 additions & 0 deletions src/maps/maps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ func TestKeys(t *testing.T) {
if !slices.Equal(got2, want) {
t.Errorf("Keys(%v) = %v, want %v", m2, got2, want)
}

// test for oldbucket code path
// We grow from 128 to 256 buckets at size 832 (6.5 * 128).
// Then we have to evacuate 128 buckets, which means we'll be done evacuation at 832+128=960 elements inserted.
// so 840 is a good number to test for oldbucket code path.
var want3 []int
var m = make(map[int]int)
for i := 0; i < 840; i++ {
want3 = append(want3, i)
m[i] = i
}

got3 := Keys(m)
sort.Ints(got3)
if !slices.Equal(got3, want3) {
t.Errorf("Keys(%v) = %v, want %v", m, got3, want3)
}
}

func TestValues(t *testing.T) {
Expand Down Expand Up @@ -216,3 +233,18 @@ func TestCloneWithMapAssign(t *testing.T) {
}
}
}

var keysArr []int

func BenchmarkKeys(b *testing.B) {
m := make(map[int]int, 1000000)
for i := 0; i < 1000000; i++ {
m[i] = i
}
b.ResetTimer()

slice := make([]int, 0, len(m))
for i := 0; i < b.N; i++ {
keysForBenchmarking(m, slice)
}
}
64 changes: 64 additions & 0 deletions src/runtime/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -1593,3 +1593,67 @@ func mapclone2(t *maptype, src *hmap) *hmap {
}
return dst
}

// keys for implementing maps.keys
//
//go:linkname keys maps.keys
func keys(m any, p unsafe.Pointer) {
e := efaceOf(&m)
t := (*maptype)(unsafe.Pointer(e._type))
h := (*hmap)(e.data)

if h == nil || h.count == 0 {
return
}
s := (*slice)(p)
r := int(fastrand())
offset := uint8(r >> h.B & (bucketCnt - 1))
if h.B == 0 {
copyKeys(t, h, (*bmap)(h.buckets), s, offset)
return
}
arraySize := int(bucketShift(h.B))
buckets := h.buckets
for i := 0; i < arraySize; i++ {
bucket := (i + r) & (arraySize - 1)
b := (*bmap)(add(buckets, uintptr(bucket)*uintptr(t.BucketSize)))
copyKeys(t, h, b, s, offset)
}

if h.growing() {
oldArraySize := int(h.noldbuckets())
for i := 0; i < oldArraySize; i++ {
bucket := (i + r) & (oldArraySize - 1)
b := (*bmap)(add(h.oldbuckets, uintptr(bucket)*uintptr(t.BucketSize)))
if evacuated(b) {
continue
}
copyKeys(t, h, b, s, offset)
}
}
return
}

func copyKeys(t *maptype, h *hmap, b *bmap, s *slice, offset uint8) {
for b != nil {
for i := uintptr(0); i < bucketCnt; i++ {
offi := (i + uintptr(offset)) & (bucketCnt - 1)
if isEmpty(b.tophash[offi]) {
continue
}
if h.flags&hashWriting != 0 {
fatal("concurrent map read and map write")
}
k := add(unsafe.Pointer(b), dataOffset+offi*uintptr(t.KeySize))
if t.IndirectKey() {
k = *((*unsafe.Pointer)(k))
}
if s.len >= s.cap {
fatal("concurrent map read and map write")
}
typedmemmove(t.Key, add(s.array, uintptr(s.len)*uintptr(t.KeySize)), k)
s.len++
}
b = b.overflow(t)
}
}

0 comments on commit f283cba

Please sign in to comment.