Skip to content

Commit c12b185

Browse files
vkuzmin-ubermdempsky
authored andcommitted
cmd/compile: avoid mapaccess at m[k]=append(m[k]..
Currently rvalue m[k] is transformed during walk into: tmp1 := *mapaccess(m, k) tmp2 := append(tmp1, ...) *mapassign(m, k) = tmp2 However, this is suboptimal, as we could instead produce just: tmp := mapassign(m, k) *tmp := append(*tmp, ...) Optimization is possible only if during Order it may tell that m[k] is exactly the same at left and right part of assignment. It doesn't work: 1) m[f(k)] = append(m[f(k)], ...) 2) sink, m[k] = sink, append(m[k]...) 3) m[k] = append(..., m[k],...) Benchmark: name old time/op new time/op delta MapAppendAssign/Int32/256-8 33.5ns ± 3% 22.4ns ±10% -33.24% (p=0.000 n=16+18) MapAppendAssign/Int32/65536-8 68.2ns ± 6% 48.5ns ±29% -28.90% (p=0.000 n=20+20) MapAppendAssign/Int64/256-8 34.3ns ± 4% 23.3ns ± 5% -32.23% (p=0.000 n=17+18) MapAppendAssign/Int64/65536-8 65.9ns ± 7% 61.2ns ±19% -7.06% (p=0.002 n=18+20) MapAppendAssign/Str/256-8 116ns ±12% 79ns ±16% -31.70% (p=0.000 n=20+19) MapAppendAssign/Str/65536-8 134ns ±15% 111ns ±45% -16.95% (p=0.000 n=19+20) name old alloc/op new alloc/op delta MapAppendAssign/Int32/256-8 47.0B ± 0% 46.0B ± 0% -2.13% (p=0.000 n=19+18) MapAppendAssign/Int32/65536-8 27.0B ± 0% 20.7B ±30% -23.33% (p=0.000 n=20+20) MapAppendAssign/Int64/256-8 47.0B ± 0% 46.0B ± 0% -2.13% (p=0.000 n=20+17) MapAppendAssign/Int64/65536-8 27.0B ± 0% 27.0B ± 0% ~ (all equal) MapAppendAssign/Str/256-8 94.0B ± 0% 78.0B ± 0% -17.02% (p=0.000 n=20+16) MapAppendAssign/Str/65536-8 54.0B ± 0% 54.0B ± 0% ~ (all equal) Fixes #24364 Updates #5147 Change-Id: Id257d052b75b9a445b4885dc571bf06ce6f6b409 Reviewed-on: https://go-review.googlesource.com/100838 Reviewed-by: Matthew Dempsky <mdempsky@google.com> Run-TryBot: Matthew Dempsky <mdempsky@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org>
1 parent e22d241 commit c12b185

File tree

7 files changed

+621
-4
lines changed

7 files changed

+621
-4
lines changed

src/cmd/compile/internal/gc/order.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,14 @@ func (o *Order) mapAssign(n *Node) {
438438
if n.Left.Op == OINDEXMAP {
439439
// Make sure we evaluate the RHS before starting the map insert.
440440
// We need to make sure the RHS won't panic. See issue 22881.
441-
n.Right = o.cheapExpr(n.Right)
441+
if n.Right.Op == OAPPEND {
442+
s := n.Right.List.Slice()[1:]
443+
for i, n := range s {
444+
s[i] = o.cheapExpr(n)
445+
}
446+
} else {
447+
n.Right = o.cheapExpr(n.Right)
448+
}
442449
}
443450
o.out = append(o.out, n)
444451

src/cmd/compile/internal/gc/typecheck.go

+16-3
Original file line numberDiff line numberDiff line change
@@ -3231,8 +3231,21 @@ func checkassignlist(stmt *Node, l Nodes) {
32313231
}
32323232
}
32333233

3234-
// Check whether l and r are the same side effect-free expression,
3235-
// so that it is safe to reuse one instead of computing both.
3234+
// samesafeexpr checks whether it is safe to reuse one of l and r
3235+
// instead of computing both. samesafeexpr assumes that l and r are
3236+
// used in the same statement or expression. In order for it to be
3237+
// safe to reuse l or r, they must:
3238+
// * be the same expression
3239+
// * not have side-effects (no function calls, no channel ops);
3240+
// however, panics are ok
3241+
// * not cause inappropriate aliasing; e.g. two string to []byte
3242+
// conversions, must result in two distinct slices
3243+
//
3244+
// The handling of OINDEXMAP is subtle. OINDEXMAP can occur both
3245+
// as an lvalue (map assignment) and an rvalue (map access). This is
3246+
// currently OK, since the only place samesafeexpr gets used on an
3247+
// lvalue expression is for OSLICE and OAPPEND optimizations, and it
3248+
// is correct in those settings.
32363249
func samesafeexpr(l *Node, r *Node) bool {
32373250
if l.Op != r.Op || !eqtype(l.Type, r.Type) {
32383251
return false
@@ -3253,7 +3266,7 @@ func samesafeexpr(l *Node, r *Node) bool {
32533266
// Allow only numeric-ish types. This is a bit conservative.
32543267
return issimple[l.Type.Etype] && samesafeexpr(l.Left, r.Left)
32553268

3256-
case OINDEX:
3269+
case OINDEX, OINDEXMAP:
32573270
return samesafeexpr(l.Left, r.Left) && samesafeexpr(l.Right, r.Right)
32583271

32593272
case OLITERAL:

src/cmd/compile/internal/gc/walk.go

+11
Original file line numberDiff line numberDiff line change
@@ -670,9 +670,20 @@ opswitch:
670670
case OAS, OASOP:
671671
init.AppendNodes(&n.Ninit)
672672

673+
// Recognize m[k] = append(m[k], ...) so we can reuse
674+
// the mapassign call.
675+
mapAppend := n.Left.Op == OINDEXMAP && n.Right.Op == OAPPEND
676+
if mapAppend && !samesafeexpr(n.Left, n.Right.List.First()) {
677+
Fatalf("not same expressions: %v != %v", n.Left, n.Right.List.First())
678+
}
679+
673680
n.Left = walkexpr(n.Left, init)
674681
n.Left = safeexpr(n.Left, init)
675682

683+
if mapAppend {
684+
n.Right.List.SetFirst(n.Left)
685+
}
686+
676687
if n.Op == OASOP {
677688
// Rewrite x op= y into x = x op y.
678689
n.Right = nod(n.SubOp(), n.Left, n.Right)

src/runtime/map_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,24 @@ func TestMapOperatorAssignment(t *testing.T) {
114114
}
115115
}
116116

117+
var sinkAppend bool
118+
119+
func TestMapAppendAssignment(t *testing.T) {
120+
m := make(map[int][]int, 0)
121+
122+
m[0] = nil
123+
m[0] = append(m[0], 12345)
124+
m[0] = append(m[0], 67890)
125+
sinkAppend, m[0] = !sinkAppend, append(m[0], 123, 456)
126+
a := []int{7, 8, 9, 0}
127+
m[0] = append(m[0], a...)
128+
129+
want := []int{12345, 67890, 123, 456, 7, 8, 9, 0}
130+
if got := m[0]; !reflect.DeepEqual(got, want) {
131+
t.Errorf("got %v, want %v", got, want)
132+
}
133+
}
134+
117135
// Maps aren't actually copied on assignment.
118136
func TestAlias(t *testing.T) {
119137
m := make(map[int]int, 0)
@@ -839,6 +857,16 @@ func benchmarkMapOperatorAssignInt32(b *testing.B, n int) {
839857
}
840858
}
841859

860+
func benchmarkMapAppendAssignInt32(b *testing.B, n int) {
861+
a := make(map[int32][]int)
862+
b.ReportAllocs()
863+
b.ResetTimer()
864+
for i := 0; i < b.N; i++ {
865+
key := int32(i & (n - 1))
866+
a[key] = append(a[key], i)
867+
}
868+
}
869+
842870
func benchmarkMapDeleteInt32(b *testing.B, n int) {
843871
a := make(map[int32]int, n)
844872
b.ResetTimer()
@@ -868,6 +896,16 @@ func benchmarkMapOperatorAssignInt64(b *testing.B, n int) {
868896
}
869897
}
870898

899+
func benchmarkMapAppendAssignInt64(b *testing.B, n int) {
900+
a := make(map[int64][]int)
901+
b.ReportAllocs()
902+
b.ResetTimer()
903+
for i := 0; i < b.N; i++ {
904+
key := int64(i & (n - 1))
905+
a[key] = append(a[key], i)
906+
}
907+
}
908+
871909
func benchmarkMapDeleteInt64(b *testing.B, n int) {
872910
a := make(map[int64]int, n)
873911
b.ResetTimer()
@@ -908,6 +946,20 @@ func benchmarkMapOperatorAssignStr(b *testing.B, n int) {
908946
}
909947
}
910948

949+
func benchmarkMapAppendAssignStr(b *testing.B, n int) {
950+
k := make([]string, n)
951+
for i := 0; i < len(k); i++ {
952+
k[i] = strconv.Itoa(i)
953+
}
954+
a := make(map[string][]string)
955+
b.ReportAllocs()
956+
b.ResetTimer()
957+
for i := 0; i < b.N; i++ {
958+
key := k[i&(n-1)]
959+
a[key] = append(a[key], key)
960+
}
961+
}
962+
911963
func benchmarkMapDeleteStr(b *testing.B, n int) {
912964
i2s := make([]string, n)
913965
for i := 0; i < n; i++ {
@@ -949,6 +1001,12 @@ func BenchmarkMapOperatorAssign(b *testing.B) {
9491001
b.Run("Str", runWith(benchmarkMapOperatorAssignStr, 1<<8, 1<<16))
9501002
}
9511003

1004+
func BenchmarkMapAppendAssign(b *testing.B) {
1005+
b.Run("Int32", runWith(benchmarkMapAppendAssignInt32, 1<<8, 1<<16))
1006+
b.Run("Int64", runWith(benchmarkMapAppendAssignInt64, 1<<8, 1<<16))
1007+
b.Run("Str", runWith(benchmarkMapAppendAssignStr, 1<<8, 1<<16))
1008+
}
1009+
9521010
func BenchmarkMapDelete(b *testing.B) {
9531011
b.Run("Int32", runWith(benchmarkMapDeleteInt32, 100, 1000, 10000))
9541012
b.Run("Int64", runWith(benchmarkMapDeleteInt64, 100, 1000, 10000))

0 commit comments

Comments
 (0)