Skip to content

Commit aee71dd

Browse files
committed
cmd/compile: optimize map-clearing range idiom
replace map clears of the form: for k := range m { delete(m, k) } (where m is map with key type that is reflexive for ==) with a new runtime function that clears the maps backing array with a memclr and reinitializes the hmap struct. Map key types that for example contain floats are not replaced by this optimization since NaN keys cannot be deleted from maps using delete. name old time/op new time/op delta GoMapClear/Reflexive/1 92.2ns ± 1% 47.1ns ± 2% -48.89% (p=0.000 n=9+9) GoMapClear/Reflexive/10 108ns ± 1% 48ns ± 2% -55.68% (p=0.000 n=10+10) GoMapClear/Reflexive/100 303ns ± 2% 110ns ± 3% -63.56% (p=0.000 n=10+10) GoMapClear/Reflexive/1000 3.58µs ± 3% 1.23µs ± 2% -65.49% (p=0.000 n=9+10) GoMapClear/Reflexive/10000 28.2µs ± 3% 10.3µs ± 2% -63.55% (p=0.000 n=9+10) GoMapClear/NonReflexive/1 121ns ± 2% 124ns ± 7% ~ (p=0.097 n=10+10) GoMapClear/NonReflexive/10 137ns ± 2% 139ns ± 3% +1.53% (p=0.033 n=10+10) GoMapClear/NonReflexive/100 331ns ± 3% 334ns ± 2% ~ (p=0.342 n=10+10) GoMapClear/NonReflexive/1000 3.64µs ± 3% 3.64µs ± 2% ~ (p=0.887 n=9+10) GoMapClear/NonReflexive/10000 28.1µs ± 2% 28.4µs ± 3% ~ (p=0.247 n=10+10) Fixes #20138 Change-Id: I181332a8ef434a4f0d89659f492d8711db3f3213 Reviewed-on: https://go-review.googlesource.com/110055 Reviewed-by: Keith Randall <[email protected]>
1 parent cd1976d commit aee71dd

File tree

8 files changed

+437
-110
lines changed

8 files changed

+437
-110
lines changed

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

Lines changed: 83 additions & 81 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cmd/compile/internal/gc/builtin/runtime.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ func mapdelete_fast32(mapType *byte, hmap map[any]any, key any)
122122
func mapdelete_fast64(mapType *byte, hmap map[any]any, key any)
123123
func mapdelete_faststr(mapType *byte, hmap map[any]any, key any)
124124
func mapiternext(hiter *any)
125+
func mapclear(mapType *byte, hmap map[any]any)
125126

126127
// *byte is really *runtime.Type
127128
func makechan64(chanType *byte, size int64) (hchan chan any)

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,8 @@ func (o *Order) stmt(n *Node) {
695695

696696
t := o.markTemp()
697697
n.Right = o.expr(n.Right, nil)
698+
699+
orderBody := true
698700
switch n.Type.Etype {
699701
default:
700702
Fatalf("orderstmt range %v", n.Type)
@@ -721,6 +723,14 @@ func (o *Order) stmt(n *Node) {
721723
n.Right = o.copyExpr(r, r.Type, false)
722724

723725
case TMAP:
726+
if isMapClear(n) {
727+
// Preserve the body of the map clear pattern so it can
728+
// be detected during walk. The loop body will not be used
729+
// when optimizing away the range loop to a runtime call.
730+
orderBody = false
731+
break
732+
}
733+
724734
// copy the map value in case it is a map literal.
725735
// TODO(rsc): Make tmp = literal expressions reuse tmp.
726736
// For maps tmp is just one word so it hardly matters.
@@ -732,7 +742,9 @@ func (o *Order) stmt(n *Node) {
732742
prealloc[n] = o.newTemp(hiter(n.Type), true)
733743
}
734744
o.exprListInPlace(n.List)
735-
orderBlock(&n.Nbody)
745+
if orderBody {
746+
orderBlock(&n.Nbody)
747+
}
736748
o.out = append(o.out, n)
737749
o.cleanTemp(t)
738750

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@ func cheapComputableIndex(width int64) bool {
154154
// Node n may also be modified in place, and may also be
155155
// the returned node.
156156
func walkrange(n *Node) *Node {
157+
if isMapClear(n) {
158+
m := n.Right
159+
lno := setlineno(m)
160+
n = mapClear(m)
161+
lineno = lno
162+
return n
163+
}
164+
157165
// variable name conventions:
158166
// ohv1, hv1, hv2: hidden (old) val 1, 2
159167
// ha, hit: hidden aggregate, iterator
@@ -449,6 +457,69 @@ func walkrange(n *Node) *Node {
449457
return n
450458
}
451459

460+
// isMapClear checks if n is of the form:
461+
//
462+
// for k := range m {
463+
// delete(m, k)
464+
// }
465+
//
466+
// where == for keys of map m is reflexive.
467+
func isMapClear(n *Node) bool {
468+
if Debug['N'] != 0 || instrumenting {
469+
return false
470+
}
471+
472+
if n.Op != ORANGE || n.Type.Etype != TMAP || n.List.Len() != 1 {
473+
return false
474+
}
475+
476+
k := n.List.First()
477+
if k == nil || k.isBlank() {
478+
return false
479+
}
480+
481+
// Require k to be a new variable name.
482+
if k.Name == nil || k.Name.Defn != n {
483+
return false
484+
}
485+
486+
if n.Nbody.Len() != 1 {
487+
return false
488+
}
489+
490+
stmt := n.Nbody.First() // only stmt in body
491+
if stmt == nil || stmt.Op != ODELETE {
492+
return false
493+
}
494+
495+
m := n.Right
496+
if !samesafeexpr(stmt.List.First(), m) || !samesafeexpr(stmt.List.Second(), k) {
497+
return false
498+
}
499+
500+
// Keys where equality is not reflexive can not be deleted from maps.
501+
if !isreflexive(m.Type.Key()) {
502+
return false
503+
}
504+
505+
return true
506+
}
507+
508+
// mapClear constructs a call to runtime.mapclear for the map m.
509+
func mapClear(m *Node) *Node {
510+
t := m.Type
511+
512+
// instantiate mapclear(typ *type, hmap map[any]any)
513+
fn := syslook("mapclear")
514+
fn = substArgTypes(fn, t.Key(), t.Elem())
515+
n := mkcall1(fn, nil, nil, typename(t), m)
516+
517+
n = typecheck(n, Etop)
518+
n = walkstmt(n)
519+
520+
return n
521+
}
522+
452523
// Lower n into runtime·memclr if possible, for
453524
// fast zeroing of slices and arrays (issue 5373).
454525
// Look for instances of

0 commit comments

Comments
 (0)