Permalink
Browse files

cmd/compile: specialize map creation for small hint sizes

Handle make(map[any]any) and make(map[any]any, hint) where
hint <= BUCKETSIZE special to allow for faster map initialization
and to improve binary size by using runtime calls with fewer arguments.

Given hint is smaller or equal to BUCKETSIZE in which case
overLoadFactor(hint, 0)  is false and no buckets would be allocated by makemap:
* If hmap needs to be allocated on the stack then only hmap's hash0
  field needs to be initialized and no call to makemap is needed.
* If hmap needs to be allocated on the heap then a new special
  makehmap function will allocate hmap and intialize hmap's
  hash0 field.

Reduces size of the godoc by ~36kb.

AMD64
name         old time/op    new time/op    delta
NewEmptyMap    16.6ns ± 2%     5.5ns ± 2%  -66.72%  (p=0.000 n=10+10)
NewSmallMap    64.8ns ± 1%    56.5ns ± 1%  -12.75%  (p=0.000 n=9+10)

Updates #6853

Change-Id: I624e90da6775afaa061178e95db8aca674f44e9b
Reviewed-on: https://go-review.googlesource.com/61190
Run-TryBot: Martin Möhrmann <moehrmann@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
  • Loading branch information...
martisch committed Sep 2, 2017
1 parent 1e83f88 commit fbfc2031a673c95700e46ddf56404a0f648fc8a9

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -91,9 +91,12 @@ func panicnildottype(want *byte)
func ifaceeq(tab *uintptr, x, y unsafe.Pointer) (ret bool)
func efaceeq(typ *uintptr, x, y unsafe.Pointer) (ret bool)

func fastrand() uint32

// *byte is really *runtime.Type
func makemap64(mapType *byte, hint int64, mapbuf *any) (hmap map[any]any)
func makemap(mapType *byte, hint int, mapbuf *any) (hmap map[any]any)
func makemap_small() (hmap map[any]any)
func mapaccess1(mapType *byte, hmap map[any]any, key *any) (val *any)
func mapaccess1_fast32(mapType *byte, hmap map[any]any, key any) (val *any)
func mapaccess1_fast64(mapType *byte, hmap map[any]any, key any) (val *any)
@@ -252,8 +252,8 @@ func hmap(t *types.Type) *types.Type {
makefield("flags", types.Types[TUINT8]),
makefield("B", types.Types[TUINT8]),
makefield("noverflow", types.Types[TUINT16]),
makefield("hash0", types.Types[TUINT32]),
makefield("buckets", types.NewPtr(bmap)), // Used in walk.go for makemap.
makefield("hash0", types.Types[TUINT32]), // Used in walk.go for OMAKEMAP.
makefield("buckets", types.NewPtr(bmap)), // Used in walk.go for OMAKEMAP.
makefield("oldbuckets", types.NewPtr(bmap)),
makefield("nevacuate", types.Types[TUINTPTR]),
makefield("extra", types.Types[TUNSAFEPTR]),
@@ -1574,6 +1574,12 @@ func (s *state) expr(n *Node) *ssa.Value {
return v
}

// map <--> *hmap
if to.Etype == TMAP && from.IsPtr() &&
to.MapType().Hmap == from.Elem() {
return v
}

dowidth(from)
dowidth(to)
if from.Width != to.Width {
@@ -927,6 +927,14 @@ func convertop(src *types.Type, dst *types.Type, why *string) Op {
return OCONVNOP
}

// src is map and dst is a pointer to corresponding hmap.
// This rule is needed for the implementation detail that
// go gc maps are implemented as a pointer to a hmap struct.
if src.Etype == TMAP && dst.IsPtr() &&
src.MapType().Hmap == dst.Elem() {
return OCONVNOP
}

return 0
}

@@ -1467,28 +1467,63 @@ opswitch:
na = typecheck(na, Etop)
init.Append(na)
}
} else {
// h = nil
h = nodnil()
}

// When hint fits into int, use makemap instead of
// makemap64, which is faster and shorter on 32 bit platforms.
fnname := "makemap64"
argtype := types.Types[TINT64]
if Isconst(hint, CTINT) && hint.Val().U.(*Mpint).CmpInt64(BUCKETSIZE) <= 0 {
// Handling make(map[any]any) and
// make(map[any]any, hint) where hint <= BUCKETSIZE
// special allows for faster map initialization and
// improves binary size by using calls with fewer arguments.
// For hint <= BUCKETSIZE overLoadFactor(hint, 0) is false
// and no buckets will be allocated by makemap. Therefore,
// no buckets need to be allocated in this code path.
if n.Esc == EscNone {
// Only need to initialize h.hash0 since
// hmap h has been allocated on the stack already.
// h.hash0 = fastrand()
rand := mkcall("fastrand", types.Types[TUINT32], init)
hashsym := hmapType.Field(4).Sym // hmap.hash0 see reflect.go:hmap
a := nod(OAS, nodSym(ODOT, h, hashsym), rand)
a = typecheck(a, Etop)
a = walkexpr(a, init)
init.Append(a)
n = nod(OCONVNOP, h, nil)
n.Type = t
n = typecheck(n, Erv)
} else {
// Call runtime.makehmap to allocate an
// hmap on the heap and initialize hmap's hash0 field.
fn := syslook("makemap_small")
fn = substArgTypes(fn, t.Key(), t.Val())
n = mkcall1(fn, n.Type, init)
}
} else {
if n.Esc != EscNone {
h = nodnil()
}
// Map initialization with a variable or large hint is
// more complicated. We therefore generate a call to
// runtime.makemap to intialize hmap and allocate the
// map buckets.

// Type checking guarantees that TIDEAL hint is positive and fits in an int.
// See checkmake call in TMAP case of OMAKE case in OpSwitch in typecheck1 function.
// The case of hint overflow when converting TUINT or TUINTPTR to TINT
// will be handled by the negative range checks in makemap during runtime.
if hint.Type.IsKind(TIDEAL) || maxintval[hint.Type.Etype].Cmp(maxintval[TUINT]) <= 0 {
fnname = "makemap"
argtype = types.Types[TINT]
}
// When hint fits into int, use makemap instead of
// makemap64, which is faster and shorter on 32 bit platforms.
fnname := "makemap64"
argtype := types.Types[TINT64]

fn := syslook(fnname)
fn = substArgTypes(fn, hmapType, t.Key(), t.Val())
n = mkcall1(fn, n.Type, init, typename(n.Type), conv(hint, argtype), h)
// Type checking guarantees that TIDEAL hint is positive and fits in an int.
// See checkmake call in TMAP case of OMAKE case in OpSwitch in typecheck1 function.
// The case of hint overflow when converting TUINT or TUINTPTR to TINT
// will be handled by the negative range checks in makemap during runtime.
if hint.Type.IsKind(TIDEAL) || maxintval[hint.Type.Etype].Cmp(maxintval[TUINT]) <= 0 {
fnname = "makemap"
argtype = types.Types[TINT]
}

fn := syslook(fnname)
fn = substArgTypes(fn, hmapType, t.Key(), t.Val())
n = mkcall1(fn, n.Type, init, typename(n.Type), conv(hint, argtype), h)
}

case OMAKESLICE:
l := n.Left
@@ -377,11 +377,16 @@ func (rw *RWMutex) Unlock() {
rw.rw.unlock()
}

func MapBuckets(m map[int]int) int {
func MapBucketsCount(m map[int]int) int {
h := *(**hmap)(unsafe.Pointer(&m))
return 1 << h.B
}

func MapBucketsPointerIsNil(m map[int]int) bool {
h := *(**hmap)(unsafe.Pointer(&m))
return h.buckets == nil
}

func LockOSCounts() (external, internal uint32) {
g := getg()
if g.m.lockedExt+g.m.lockedInt == 0 {
@@ -281,7 +281,16 @@ func makemap64(t *maptype, hint int64, h *hmap) *hmap {
return makemap(t, int(hint), h)
}

// makemap implements a Go map creation make(map[k]v, hint)
// makehmap_small implements Go map creation for make(map[k]v) and
// make(map[k]v, hint) when hint is known to be at most bucketCnt
// at compile time and the map needs to be allocated on the heap.
func makemap_small() *hmap {
h := new(hmap)
h.hash0 = fastrand()
return h
}

// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
@@ -596,33 +596,132 @@ func TestIgnoreBogusMapHint(t *testing.T) {
}
}

var mapSink map[int]int

var mapBucketTests = [...]struct {
n int // n is the number of map elements
noescape int // number of expected buckets for non-escaping map
escape int // number of expected buckets for escaping map
}{
{-(1 << 30), 1, 1},
{-1, 1, 1},
{0, 1, 1},
{1, 1, 1},
{8, 1, 1},
{9, 2, 2},
{13, 2, 2},
{14, 4, 4},
{26, 4, 4},
}

func TestMapBuckets(t *testing.T) {
// Test that maps of different sizes have the right number of buckets.
// Non-escaping maps with small buckets (like map[int]int) never
// have a nil bucket pointer due to starting with preallocated buckets
// on the stack. Escaping maps start with a non-nil bucket pointer if
// hint size is above bucketCnt and thereby have more than one bucket.
// These tests depend on bucketCnt and loadFactor* in hashmap.go.
for _, tt := range [...]struct {
n, b int
}{
{8, 1},
{9, 2},
{13, 2},
{14, 4},
{26, 4},
} {
m := map[int]int{}
for i := 0; i < tt.n; i++ {
m[i] = i
t.Run("mapliteral", func(t *testing.T) {
for _, tt := range mapBucketTests {
localMap := map[int]int{}
if runtime.MapBucketsPointerIsNil(localMap) {
t.Errorf("no escape: buckets pointer is nil for non-escaping map")
}
for i := 0; i < tt.n; i++ {
localMap[i] = i
}
if got := runtime.MapBucketsCount(localMap); got != tt.noescape {
t.Errorf("no escape: n=%d want %d buckets, got %d", tt.n, tt.noescape, got)
}
escapingMap := map[int]int{}
if count := runtime.MapBucketsCount(escapingMap); count > 1 && runtime.MapBucketsPointerIsNil(escapingMap) {
t.Errorf("escape: buckets pointer is nil for n=%d buckets", count)
}
for i := 0; i < tt.n; i++ {
escapingMap[i] = i
}
if got := runtime.MapBucketsCount(escapingMap); got != tt.escape {
t.Errorf("escape n=%d want %d buckets, got %d", tt.n, tt.escape, got)
}
mapSink = escapingMap
}
if got := runtime.MapBuckets(m); got != tt.b {
t.Errorf("no hint n=%d want %d buckets, got %d", tt.n, tt.b, got)
})
t.Run("nohint", func(t *testing.T) {
for _, tt := range mapBucketTests {
localMap := make(map[int]int)
if runtime.MapBucketsPointerIsNil(localMap) {
t.Errorf("no escape: buckets pointer is nil for non-escaping map")
}
for i := 0; i < tt.n; i++ {
localMap[i] = i
}
if got := runtime.MapBucketsCount(localMap); got != tt.noescape {
t.Errorf("no escape: n=%d want %d buckets, got %d", tt.n, tt.noescape, got)
}
escapingMap := make(map[int]int)
if count := runtime.MapBucketsCount(escapingMap); count > 1 && runtime.MapBucketsPointerIsNil(escapingMap) {
t.Errorf("escape: buckets pointer is nil for n=%d buckets", count)
}
for i := 0; i < tt.n; i++ {
escapingMap[i] = i
}
if got := runtime.MapBucketsCount(escapingMap); got != tt.escape {
t.Errorf("escape: n=%d want %d buckets, got %d", tt.n, tt.escape, got)
}
mapSink = escapingMap
}
m = make(map[int]int, tt.n)
for i := 0; i < tt.n; i++ {
m[i] = i
})
t.Run("makemap", func(t *testing.T) {
for _, tt := range mapBucketTests {
localMap := make(map[int]int, tt.n)
if runtime.MapBucketsPointerIsNil(localMap) {
t.Errorf("no escape: buckets pointer is nil for non-escaping map")
}
for i := 0; i < tt.n; i++ {
localMap[i] = i
}
if got := runtime.MapBucketsCount(localMap); got != tt.noescape {
t.Errorf("no escape: n=%d want %d buckets, got %d", tt.n, tt.noescape, got)
}
escapingMap := make(map[int]int, tt.n)
if count := runtime.MapBucketsCount(escapingMap); count > 1 && runtime.MapBucketsPointerIsNil(escapingMap) {
t.Errorf("escape: buckets pointer is nil for n=%d buckets", count)
}
for i := 0; i < tt.n; i++ {
escapingMap[i] = i
}
if got := runtime.MapBucketsCount(escapingMap); got != tt.escape {
t.Errorf("escape: n=%d want %d buckets, got %d", tt.n, tt.escape, got)
}
mapSink = escapingMap
}
if got := runtime.MapBuckets(m); got != tt.b {
t.Errorf("hint n=%d want %d buckets, got %d", tt.n, tt.b, got)
})
t.Run("makemap64", func(t *testing.T) {
for _, tt := range mapBucketTests {
localMap := make(map[int]int, int64(tt.n))
if runtime.MapBucketsPointerIsNil(localMap) {
t.Errorf("no escape: buckets pointer is nil for non-escaping map")
}
for i := 0; i < tt.n; i++ {
localMap[i] = i
}
if got := runtime.MapBucketsCount(localMap); got != tt.noescape {
t.Errorf("no escape: n=%d want %d buckets, got %d", tt.n, tt.noescape, got)
}
escapingMap := make(map[int]int, tt.n)
if count := runtime.MapBucketsCount(escapingMap); count > 1 && runtime.MapBucketsPointerIsNil(escapingMap) {
t.Errorf("escape: buckets pointer is nil for n=%d buckets", count)
}
for i := 0; i < tt.n; i++ {
escapingMap[i] = i
}
if got := runtime.MapBucketsCount(escapingMap); got != tt.escape {
t.Errorf("escape: n=%d want %d buckets, got %d", tt.n, tt.escape, got)
}
mapSink = escapingMap
}
}
})

}

func benchmarkMapPop(b *testing.B, n int) {
@@ -76,7 +76,7 @@ import "fmt"
import "runtime"
var gslice []string
func main() {
mapvar := make(map[string]string,5)
mapvar := make(map[string]string, 13)
mapvar["abc"] = "def"
mapvar["ghi"] = "jkl"
strvar := "abc"
@@ -198,8 +198,10 @@ func testGdbPython(t *testing.T, cgo bool) {
t.Fatalf("info goroutines failed: %s", bl)
}

printMapvarRe := regexp.MustCompile(`\Q = map[string]string = {["abc"] = "def", ["ghi"] = "jkl"}\E$`)
if bl := blocks["print mapvar"]; !printMapvarRe.MatchString(bl) {
printMapvarRe1 := regexp.MustCompile(`\Q = map[string]string = {["abc"] = "def", ["ghi"] = "jkl"}\E$`)
printMapvarRe2 := regexp.MustCompile(`\Q = map[string]string = {["ghi"] = "jkl", ["abc"] = "def"}\E$`)
if bl := blocks["print mapvar"]; !printMapvarRe1.MatchString(bl) &&
!printMapvarRe2.MatchString(bl) {
t.Fatalf("print mapvar failed: %s", bl)
}

@@ -644,7 +644,7 @@ func useT40(*T40)

func newT40() *T40 {
ret := T40{}
ret.m = make(map[int]int) // ERROR "live at call to makemap: &ret$"
ret.m = make(map[int]int, 42) // ERROR "live at call to makemap: &ret$"
return &ret
}

@@ -656,7 +656,7 @@ func bad40() {

func good40() {
ret := T40{}
ret.m = make(map[int]int) // ERROR "live at call to makemap: .autotmp_[0-9]+ ret$"
ret.m = make(map[int]int) // ERROR "live at call to fastrand: .autotmp_[0-9]+ ret$"
t := &ret
printnl() // ERROR "live at call to printnl: .autotmp_[0-9]+ ret$"
useT40(t) // ERROR "live at call to useT40: .autotmp_[0-9]+ ret$"
@@ -23,7 +23,7 @@ type T40 struct {

func newT40() *T40 {
ret := T40{}
ret.m = make(map[int]int) // ERROR "live at call to makemap: &ret$"
ret.m = make(map[int]int, 42) // ERROR "live at call to makemap: &ret$"
return &ret
}

@@ -35,7 +35,7 @@ func bad40() {

func good40() {
ret := T40{}
ret.m = make(map[int]int) // ERROR "live at call to makemap: .autotmp_[0-9]+ ret$"
ret.m = make(map[int]int, 42) // ERROR "live at call to makemap: .autotmp_[0-9]+ ret$"
t := &ret
printnl() // ERROR "live at call to printnl: .autotmp_[0-9]+ ret$"
useT40(t) // ERROR "live at call to useT40: .autotmp_[0-9]+ ret$"

0 comments on commit fbfc203

Please sign in to comment.