Skip to content

Commit

Permalink
cmd/compile: optimize map-clearing range idiom
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
martisch committed May 8, 2018
1 parent cd1976d commit aee71dd
Show file tree
Hide file tree
Showing 8 changed files with 437 additions and 110 deletions.
164 changes: 83 additions & 81 deletions src/cmd/compile/internal/gc/builtin.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/cmd/compile/internal/gc/builtin/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ func mapdelete_fast32(mapType *byte, hmap map[any]any, key any)
func mapdelete_fast64(mapType *byte, hmap map[any]any, key any)
func mapdelete_faststr(mapType *byte, hmap map[any]any, key any)
func mapiternext(hiter *any)
func mapclear(mapType *byte, hmap map[any]any)

// *byte is really *runtime.Type
func makechan64(chanType *byte, size int64) (hchan chan any)
Expand Down
14 changes: 13 additions & 1 deletion src/cmd/compile/internal/gc/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,8 @@ func (o *Order) stmt(n *Node) {

t := o.markTemp()
n.Right = o.expr(n.Right, nil)

orderBody := true
switch n.Type.Etype {
default:
Fatalf("orderstmt range %v", n.Type)
Expand All @@ -721,6 +723,14 @@ func (o *Order) stmt(n *Node) {
n.Right = o.copyExpr(r, r.Type, false)

case TMAP:
if isMapClear(n) {
// Preserve the body of the map clear pattern so it can
// be detected during walk. The loop body will not be used
// when optimizing away the range loop to a runtime call.
orderBody = false
break
}

// copy the map value in case it is a map literal.
// TODO(rsc): Make tmp = literal expressions reuse tmp.
// For maps tmp is just one word so it hardly matters.
Expand All @@ -732,7 +742,9 @@ func (o *Order) stmt(n *Node) {
prealloc[n] = o.newTemp(hiter(n.Type), true)
}
o.exprListInPlace(n.List)
orderBlock(&n.Nbody)
if orderBody {
orderBlock(&n.Nbody)
}
o.out = append(o.out, n)
o.cleanTemp(t)

Expand Down
71 changes: 71 additions & 0 deletions src/cmd/compile/internal/gc/range.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ func cheapComputableIndex(width int64) bool {
// Node n may also be modified in place, and may also be
// the returned node.
func walkrange(n *Node) *Node {
if isMapClear(n) {
m := n.Right
lno := setlineno(m)
n = mapClear(m)
lineno = lno
return n
}

// variable name conventions:
// ohv1, hv1, hv2: hidden (old) val 1, 2
// ha, hit: hidden aggregate, iterator
Expand Down Expand Up @@ -449,6 +457,69 @@ func walkrange(n *Node) *Node {
return n
}

// isMapClear checks if n is of the form:
//
// for k := range m {
// delete(m, k)
// }
//
// where == for keys of map m is reflexive.
func isMapClear(n *Node) bool {
if Debug['N'] != 0 || instrumenting {
return false
}

if n.Op != ORANGE || n.Type.Etype != TMAP || n.List.Len() != 1 {
return false
}

k := n.List.First()
if k == nil || k.isBlank() {
return false
}

// Require k to be a new variable name.
if k.Name == nil || k.Name.Defn != n {
return false
}

if n.Nbody.Len() != 1 {
return false
}

stmt := n.Nbody.First() // only stmt in body
if stmt == nil || stmt.Op != ODELETE {
return false
}

m := n.Right
if !samesafeexpr(stmt.List.First(), m) || !samesafeexpr(stmt.List.Second(), k) {
return false
}

// Keys where equality is not reflexive can not be deleted from maps.
if !isreflexive(m.Type.Key()) {
return false
}

return true
}

// mapClear constructs a call to runtime.mapclear for the map m.
func mapClear(m *Node) *Node {
t := m.Type

// instantiate mapclear(typ *type, hmap map[any]any)
fn := syslook("mapclear")
fn = substArgTypes(fn, t.Key(), t.Elem())
n := mkcall1(fn, nil, nil, typename(t), m)

n = typecheck(n, Etop)
n = walkstmt(n)

return n
}

// Lower n into runtime·memclr if possible, for
// fast zeroing of slices and arrays (issue 5373).
// Look for instances of
Expand Down
Loading

0 comments on commit aee71dd

Please sign in to comment.