Commit a0f57c3f authored by Martin Möhrmann's avatar Martin Möhrmann

cmd/compile: avoid string allocations when map key is struct or array literal

x = map[string(byteslice)] is already optimized by the compiler to avoid a
string allocation. This CL generalizes this optimization to:

x = map[T1{ ... Tn{..., string(byteslice), ...} ... }]
where T1 to Tn is a nesting of struct and array literals.

Found in a hot code path that used a struct of strings made from []byte
slices to make a map lookup.

There are no uses of the more generalized optimization in the standard library.
Passes toolstash -cmp.

MapStringConversion/32/simple    21.9ns ± 2%    21.9ns ± 3%      ~     (p=0.995 n=17+20)
MapStringConversion/32/struct    28.8ns ± 3%    22.0ns ± 2%   -23.80%  (p=0.000 n=20+20)
MapStringConversion/32/array     28.5ns ± 2%    21.9ns ± 2%   -23.14%  (p=0.000 n=19+16)
MapStringConversion/64/simple    21.0ns ± 2%    21.1ns ± 3%      ~     (p=0.072 n=19+18)
MapStringConversion/64/struct    72.4ns ± 3%    21.3ns ± 2%   -70.53%  (p=0.000 n=20+20)
MapStringConversion/64/array     72.8ns ± 1%    21.0ns ± 2%   -71.13%  (p=0.000 n=17+19)

name                           old allocs/op  new allocs/op  delta
MapStringConversion/32/simple      0.00           0.00           ~     (all equal)
MapStringConversion/32/struct      0.00           0.00           ~     (all equal)
MapStringConversion/32/array       0.00           0.00           ~     (all equal)
MapStringConversion/64/simple      0.00           0.00           ~     (all equal)
MapStringConversion/64/struct      1.00 ± 0%      0.00       -100.00%  (p=0.000 n=20+20)
MapStringConversion/64/array       1.00 ± 0%      0.00       -100.00%  (p=0.000 n=20+20)

Change-Id: I483b4d84d8d74b1025b62c954da9a365e79b7a3a
Reviewed-on: https://go-review.googlesource.com/c/116275Reviewed-by: 's avatarMatthew Dempsky <mdempsky@google.com>
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent a1ca4893
......@@ -233,6 +233,45 @@ func (o *Order) mapKeyTemp(t *types.Type, n *Node) *Node {
return n
}
// mapKeyReplaceStrConv replaces OARRAYBYTESTR by OARRAYBYTESTRTMP
// in n to avoid string allocations for keys in map lookups.
// Returns a bool that signals if a modification was made.
//
// For:
// x = m[string(k)]
// x = m[T1{... Tn{..., string(k), ...}]
// where k is []byte, T1 to Tn is a nesting of struct and array literals,
// the allocation of backing bytes for the string can be avoided
// by reusing the []byte backing array. These are special cases
// for avoiding allocations when converting byte slices to strings.
// It would be nice to handle these generally, but because
// []byte keys are not allowed in maps, the use of string(k)
// comes up in important cases in practice. See issue 3512.
func mapKeyReplaceStrConv(n *Node) bool {
var replaced bool
switch n.Op {
case OARRAYBYTESTR:
n.Op = OARRAYBYTESTRTMP
replaced = true
case OSTRUCTLIT:
for _, elem := range n.List.Slice() {
if mapKeyReplaceStrConv(elem.Left) {
replaced = true
}
}
case OARRAYLIT:
for _, elem := range n.List.Slice() {
if elem.Op == OKEY {
elem = elem.Right
}
if mapKeyReplaceStrConv(elem) {
replaced = true
}
}
}
return replaced
}
type ordermarker int
// Marktemp returns the top of the temporary variable stack.
......@@ -580,10 +619,9 @@ func (o *Order) stmt(n *Node) {
r.Left = o.expr(r.Left, nil)
r.Right = o.expr(r.Right, nil)
// See case OINDEXMAP below.
if r.Right.Op == OARRAYBYTESTR {
r.Right.Op = OARRAYBYTESTRTMP
}
// See similar conversion for OINDEXMAP below.
_ = mapKeyReplaceStrConv(r.Right)
r.Right = o.mapKeyTemp(r.Left.Type, r.Right)
o.okAs2(n)
o.cleanTemp(t)
......@@ -1042,25 +1080,18 @@ func (o *Order) expr(n, lhs *Node) *Node {
n.Right = o.expr(n.Right, nil)
needCopy := false
if !n.IndexMapLValue() && instrumenting {
// Race detector needs the copy so it can
// call treecopy on the result.
needCopy = true
}
// For x = m[string(k)] where k is []byte, the allocation of
// backing bytes for the string can be avoided by reusing
// the []byte backing array. This is a special case that it
// would be nice to handle more generally, but because
// there are no []byte-keyed maps, this specific case comes
// up in important cases in practice. See issue 3512.
// Nothing can change the []byte we are not copying before
// the map index, because the map access is going to
// be forced to happen immediately following this
// conversion (by the ordercopyexpr a few lines below).
if !n.IndexMapLValue() && n.Right.Op == OARRAYBYTESTR {
n.Right.Op = OARRAYBYTESTRTMP
needCopy = true
if !n.IndexMapLValue() {
// Enforce that any []byte slices we are not copying
// can not be changed before the map index by forcing
// the map index to happen immediately following the
// conversions. See copyExpr a few lines below.
needCopy = mapKeyReplaceStrConv(n.Right)
if instrumenting {
// Race detector needs the copy so it can
// call treecopy on the result.
needCopy = true
}
}
n.Right = o.mapKeyTemp(n.Left.Type, n.Right)
......
......@@ -370,3 +370,37 @@ func BenchmarkGoMapClear(b *testing.B) {
}
})
}
func BenchmarkMapStringConversion(b *testing.B) {
for _, length := range []int{32, 64} {
b.Run(strconv.Itoa(length), func(b *testing.B) {
bytes := make([]byte, length)
b.Run("simple", func(b *testing.B) {
b.ReportAllocs()
m := make(map[string]int)
m[string(bytes)] = 0
for i := 0; i < b.N; i++ {
_ = m[string(bytes)]
}
})
b.Run("struct", func(b *testing.B) {
b.ReportAllocs()
type stringstruct struct{ s string }
m := make(map[stringstruct]int)
m[stringstruct{string(bytes)}] = 0
for i := 0; i < b.N; i++ {
_ = m[stringstruct{string(bytes)}]
}
})
b.Run("array", func(b *testing.B) {
b.ReportAllocs()
type stringarray [1]string
m := make(map[stringarray]int)
m[stringarray{string(bytes)}] = 0
for i := 0; i < b.N; i++ {
_ = m[stringarray{string(bytes)}]
}
})
})
}
}
......@@ -135,7 +135,8 @@ func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte) {
// and otherwise intrinsified by the compiler.
//
// Some internal compiler optimizations use this function.
// - Used for m[string(k)] lookup where m is a string-keyed map and k is a []byte.
// - Used for m[T1{... Tn{..., string(k), ...} ...}] and m[string(k)]
// where k is []byte, T1 to Tn is a nesting of struct and array literals.
// - Used for "<"+string(b)+">" concatenation where b is []byte.
// - Used for string(b)=="foo" comparison where b is []byte.
func slicebytetostringtmp(b []byte) string {
......
......@@ -37,6 +37,35 @@ func AccessString2(m map[string]int) bool {
return ok
}
// ------------------- //
// String Conversion //
// ------------------- //
func LookupStringConversionSimple(m map[string]int, bytes []byte) int {
// amd64:-`.*runtime\.slicebytetostring\(`
return m[string(bytes)]
}
func LookupStringConversionStructLit(m map[struct{ string }]int, bytes []byte) int {
// amd64:-`.*runtime\.slicebytetostring\(`
return m[struct{ string }{string(bytes)}]
}
func LookupStringConversionArrayLit(m map[[2]string]int, bytes []byte) int {
// amd64:-`.*runtime\.slicebytetostring\(`
return m[[2]string{string(bytes), string(bytes)}]
}
func LookupStringConversionNestedLit(m map[[1]struct{ s [1]string }]int, bytes []byte) int {
// amd64:-`.*runtime\.slicebytetostring\(`
return m[[1]struct{ s [1]string }{struct{ s [1]string }{s: [1]string{string(bytes)}}}]
}
func LookupStringConversionKeyedArrayLit(m map[[2]string]int, bytes []byte) int {
// amd64:-`.*runtime\.slicebytetostring\(`
return m[[2]string{0: string(bytes)}]
}
// ------------------- //
// Map Clear //
// ------------------- //
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment