Skip to content

Commit 9a6d155

Browse files
cuiweixieeric
authored andcommitted
maps,runtime: improve maps.Clone
name old time/op new time/op delta MapClone-10 65.8ms ± 7% 10.3ms ± 2% -84.30% (p=0.000 n=10+9) name old alloc/op new alloc/op delta MapClone-10 40.2MB ± 0% 40.5MB ± 0% +0.57% (p=0.000 n=10+9) name old allocs/op new allocs/op delta MapClone-10 20.0 ± 0% 23.0 ± 0% +15.00% (p=0.000 n=10+10) Updates golang#58740. Change-Id: I148501e723cb2124f02045400e7ceb36af0871c8 Reviewed-on: https://go-review.googlesource.com/c/go/+/471400 Reviewed-by: Heschi Kreinick <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Keith Randall <[email protected]> Run-TryBot: xie cui <[email protected]> Reviewed-by: Keith Randall <[email protected]>
1 parent 2b0d31c commit 9a6d155

File tree

4 files changed

+206
-5
lines changed

4 files changed

+206
-5
lines changed

src/maps/maps.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,17 @@ func EqualFunc[M1 ~map[K]V1, M2 ~map[K]V2, K comparable, V1, V2 any](m1 M1, m2 M
5353
return true
5454
}
5555

56+
// clone is implemented in the runtime package.
57+
func clone(m any) any
58+
5659
// Clone returns a copy of m. This is a shallow clone:
5760
// the new keys and values are set using ordinary assignment.
5861
func Clone[M ~map[K]V, K comparable, V any](m M) M {
5962
// Preserve nil in case it matters.
6063
if m == nil {
6164
return nil
6265
}
63-
r := make(M, len(m))
64-
for k, v := range m {
65-
r[k] = v
66-
}
67-
return r
66+
return clone(m).(M)
6867
}
6968

7069
// Copy copies all key/value pairs in src adding them to dst.

src/maps/maps.s

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// need this empty asm file to enable linkname.

src/maps/maps_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,52 @@ func TestDeleteFunc(t *testing.T) {
179179
t.Errorf("DeleteFunc result = %v, want %v", mc, want)
180180
}
181181
}
182+
183+
var n map[int]int
184+
185+
func BenchmarkMapClone(b *testing.B) {
186+
var m = make(map[int]int)
187+
for i := 0; i < 1000000; i++ {
188+
m[i] = i
189+
}
190+
b.ResetTimer()
191+
for i := 0; i < b.N; i++ {
192+
n = Clone(m)
193+
}
194+
}
195+
196+
func TestCloneWithDelete(t *testing.T) {
197+
var m = make(map[int]int)
198+
for i := 0; i < 32; i++ {
199+
m[i] = i
200+
}
201+
for i := 8; i < 32; i++ {
202+
delete(m, i)
203+
}
204+
m2 := Clone(m)
205+
if len(m2) != 8 {
206+
t.Errorf("len2(m2) = %d, want %d", len(m2), 8)
207+
}
208+
for i := 0; i < 8; i++ {
209+
if m2[i] != m[i] {
210+
t.Errorf("m2[%d] = %d, want %d", i, m2[i], m[i])
211+
}
212+
}
213+
}
214+
215+
func TestCloneWithMapAssign(t *testing.T) {
216+
var m = make(map[int]int)
217+
const N = 25
218+
for i := 0; i < N; i++ {
219+
m[i] = i
220+
}
221+
m2 := Clone(m)
222+
if len(m2) != N {
223+
t.Errorf("len2(m2) = %d, want %d", len(m2), N)
224+
}
225+
for i := 0; i < N; i++ {
226+
if m2[i] != m[i] {
227+
t.Errorf("m2[%d] = %d, want %d", i, m2[i], m[i])
228+
}
229+
}
230+
}

src/runtime/map.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,3 +1423,151 @@ var zeroVal [maxZero]byte
14231423
// map init function to this symbol. Defined in assembly so as to avoid
14241424
// complications with instrumentation (coverage, etc).
14251425
func mapinitnoop()
1426+
1427+
// mapclone for implementing maps.Clone
1428+
//
1429+
//go:linkname mapclone maps.clone
1430+
func mapclone(m any) any {
1431+
e := efaceOf(&m)
1432+
e.data = unsafe.Pointer(mapclone2((*maptype)(unsafe.Pointer(e._type)), (*hmap)(e.data)))
1433+
return m
1434+
}
1435+
1436+
// moveToBmap moves a bucket from src to dst. It returns the destination bucket or new destination bucket if it overflows
1437+
// and the pos that the next key/value will be written, if pos == bucketCnt means needs to written in overflow bucket.
1438+
func moveToBmap(t *maptype, h *hmap, dst *bmap, pos int, src *bmap) (*bmap, int) {
1439+
for i := 0; i < bucketCnt; i++ {
1440+
if isEmpty(src.tophash[i]) {
1441+
continue
1442+
}
1443+
1444+
for ; pos < bucketCnt; pos++ {
1445+
if isEmpty(dst.tophash[pos]) {
1446+
break
1447+
}
1448+
}
1449+
1450+
if pos == bucketCnt {
1451+
dst = h.newoverflow(t, dst)
1452+
pos = 0
1453+
}
1454+
1455+
srcK := add(unsafe.Pointer(src), dataOffset+uintptr(i)*uintptr(t.keysize))
1456+
srcEle := add(unsafe.Pointer(src), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(i)*uintptr(t.elemsize))
1457+
dstK := add(unsafe.Pointer(dst), dataOffset+uintptr(pos)*uintptr(t.keysize))
1458+
dstEle := add(unsafe.Pointer(dst), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(pos)*uintptr(t.elemsize))
1459+
1460+
dst.tophash[pos] = src.tophash[i]
1461+
if t.indirectkey() {
1462+
*(*unsafe.Pointer)(dstK) = *(*unsafe.Pointer)(srcK)
1463+
} else {
1464+
typedmemmove(t.key, dstK, srcK)
1465+
}
1466+
if t.indirectelem() {
1467+
*(*unsafe.Pointer)(dstEle) = *(*unsafe.Pointer)(srcEle)
1468+
} else {
1469+
typedmemmove(t.elem, dstEle, srcEle)
1470+
}
1471+
pos++
1472+
h.count++
1473+
}
1474+
return dst, pos
1475+
}
1476+
1477+
func mapclone2(t *maptype, src *hmap) *hmap {
1478+
dst := makemap(t, src.count, nil)
1479+
dst.hash0 = src.hash0
1480+
dst.nevacuate = 0
1481+
//flags do not need to be copied here, just like a new map has no flags.
1482+
1483+
if src.count == 0 {
1484+
return dst
1485+
}
1486+
1487+
if src.flags&hashWriting != 0 {
1488+
fatal("concurrent map clone and map write")
1489+
}
1490+
1491+
if src.B == 0 {
1492+
dst.buckets = newobject(t.bucket)
1493+
dst.count = src.count
1494+
typedmemmove(t.bucket, dst.buckets, src.buckets)
1495+
return dst
1496+
}
1497+
1498+
//src.B != 0
1499+
if dst.B == 0 {
1500+
dst.buckets = newobject(t.bucket)
1501+
}
1502+
dstArraySize := int(bucketShift(dst.B))
1503+
srcArraySize := int(bucketShift(src.B))
1504+
for i := 0; i < dstArraySize; i++ {
1505+
dstBmap := (*bmap)(add(dst.buckets, uintptr(i*int(t.bucketsize))))
1506+
pos := 0
1507+
for j := 0; j < srcArraySize; j += dstArraySize {
1508+
srcBmap := (*bmap)(add(src.buckets, uintptr((i+j)*int(t.bucketsize))))
1509+
for srcBmap != nil {
1510+
dstBmap, pos = moveToBmap(t, dst, dstBmap, pos, srcBmap)
1511+
srcBmap = srcBmap.overflow(t)
1512+
}
1513+
}
1514+
}
1515+
1516+
if src.oldbuckets == nil {
1517+
return dst
1518+
}
1519+
1520+
oldB := src.B
1521+
srcOldbuckets := src.oldbuckets
1522+
if !src.sameSizeGrow() {
1523+
oldB--
1524+
}
1525+
oldSrcArraySize := int(bucketShift(oldB))
1526+
1527+
for i := 0; i < oldSrcArraySize; i++ {
1528+
srcBmap := (*bmap)(add(srcOldbuckets, uintptr(i*int(t.bucketsize))))
1529+
if evacuated(srcBmap) {
1530+
continue
1531+
}
1532+
1533+
if oldB >= dst.B { // main bucket bits in dst is less than oldB bits in src
1534+
dstBmap := (*bmap)(add(dst.buckets, uintptr(i)&bucketMask(dst.B)))
1535+
for dstBmap.overflow(t) != nil {
1536+
dstBmap = dstBmap.overflow(t)
1537+
}
1538+
pos := 0
1539+
for srcBmap != nil {
1540+
dstBmap, pos = moveToBmap(t, dst, dstBmap, pos, srcBmap)
1541+
srcBmap = srcBmap.overflow(t)
1542+
}
1543+
continue
1544+
}
1545+
1546+
for srcBmap != nil {
1547+
// move from oldBlucket to new bucket
1548+
for i := uintptr(0); i < bucketCnt; i++ {
1549+
if isEmpty(srcBmap.tophash[i]) {
1550+
continue
1551+
}
1552+
1553+
if src.flags&hashWriting != 0 {
1554+
fatal("concurrent map clone and map write")
1555+
}
1556+
1557+
srcK := add(unsafe.Pointer(srcBmap), dataOffset+i*uintptr(t.keysize))
1558+
if t.indirectkey() {
1559+
srcK = *((*unsafe.Pointer)(srcK))
1560+
}
1561+
1562+
srcEle := add(unsafe.Pointer(srcBmap), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
1563+
if t.indirectelem() {
1564+
srcEle = *((*unsafe.Pointer)(srcEle))
1565+
}
1566+
dstEle := mapassign(t, dst, srcK)
1567+
typedmemmove(t.elem, dstEle, srcEle)
1568+
}
1569+
srcBmap = srcBmap.overflow(t)
1570+
}
1571+
}
1572+
return dst
1573+
}

0 commit comments

Comments
 (0)