Skip to content

Commit a088e23

Browse files
committed
unique: add unique package and implement Make/Handle
This change adds the unique package for canonicalizing values, as described by the proposal in #62483. Fixes #62483. Change-Id: I1dc3d34ec12351cb4dc3838a8ea29a5368d59e99 Reviewed-on: https://go-review.googlesource.com/c/go/+/574355 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Ingo Oeser <[email protected]> Reviewed-by: David Chase <[email protected]>
1 parent 654c336 commit a088e23

File tree

12 files changed

+537
-1
lines changed

12 files changed

+537
-1
lines changed

api/next/62483.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pkg unique, func Make[$0 comparable]($0) Handle[$0] #62483
2+
pkg unique, method (Handle[$0]) Value() $0 #62483
3+
pkg unique, type Handle[$0 comparable] struct #62483

doc/next/6-stdlib/2-unique.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
### New unique package
2+
3+
The new [unique](/pkg/unique) package provides facilites for
4+
canonicalizing values (like "interning" or "hash-consing").
5+
6+
Any value of comparable type may be canonicalized with the new
7+
`Make[T]` function, which produces a reference to a canonical copy of
8+
the value in the form of a `Handle[T]`.
9+
Two `Handle[T]` are equal if and only if the values used to produce the
10+
handles are equal, allowing programs to deduplicate values and reduce
11+
their memory footprint.
12+
Comparing two `Handle[T]` values is efficient, reducing down to a simple
13+
pointer comparison.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<!-- This is a new package; covered in 6-stdlib/2-unique.md. -->

src/go/build/deps_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ var depsRules = `
164164
bufio, path, strconv
165165
< STR;
166166
167+
RUNTIME, internal/concurrent
168+
< unique;
169+
167170
# OS is basic OS access, including helpers (path/filepath, os/exec, etc).
168171
# OS includes string routines, but those must be layered above package os.
169172
# OS does not include reflection.

src/go/doc/comment/std.go

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/runtime/mgc.go

+22-1
Original file line numberDiff line numberDiff line change
@@ -1694,7 +1694,8 @@ func gcResetMarkState() {
16941694
// Hooks for other packages
16951695

16961696
var poolcleanup func()
1697-
var boringCaches []unsafe.Pointer // for crypto/internal/boring
1697+
var boringCaches []unsafe.Pointer // for crypto/internal/boring
1698+
var uniqueMapCleanup chan struct{} // for unique
16981699

16991700
//go:linkname sync_runtime_registerPoolCleanup sync.runtime_registerPoolCleanup
17001701
func sync_runtime_registerPoolCleanup(f func()) {
@@ -1706,6 +1707,18 @@ func boring_registerCache(p unsafe.Pointer) {
17061707
boringCaches = append(boringCaches, p)
17071708
}
17081709

1710+
//go:linkname unique_runtime_registerUniqueMapCleanup unique.runtime_registerUniqueMapCleanup
1711+
func unique_runtime_registerUniqueMapCleanup(f func()) {
1712+
// Start the goroutine in the runtime so it's counted as a system goroutine.
1713+
uniqueMapCleanup = make(chan struct{}, 1)
1714+
go func(cleanup func()) {
1715+
for {
1716+
<-uniqueMapCleanup
1717+
cleanup()
1718+
}
1719+
}(f)
1720+
}
1721+
17091722
func clearpools() {
17101723
// clear sync.Pools
17111724
if poolcleanup != nil {
@@ -1717,6 +1730,14 @@ func clearpools() {
17171730
atomicstorep(p, nil)
17181731
}
17191732

1733+
// clear unique maps
1734+
if uniqueMapCleanup != nil {
1735+
select {
1736+
case uniqueMapCleanup <- struct{}{}:
1737+
default:
1738+
}
1739+
}
1740+
17201741
// Clear central sudog cache.
17211742
// Leave per-P caches alone, they have strictly bounded size.
17221743
// Disconnect cached list before dropping it on the floor,

src/unique/clone.go

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright 2024 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+
package unique
6+
7+
import (
8+
"internal/abi"
9+
"unsafe"
10+
)
11+
12+
// clone makes a copy of value, and may update string values found in value
13+
// with a cloned version of those strings. The purpose of explicitly cloning
14+
// strings is to avoid accidentally giving a large string a long lifetime.
15+
//
16+
// Note that this will clone strings in structs and arrays found in value,
17+
// and will clone value if it itself is a string. It will not, however, clone
18+
// strings if value is of interface or slice type (that is, found via an
19+
// indirection).
20+
func clone[T comparable](value T, seq *cloneSeq) T {
21+
for _, offset := range seq.stringOffsets {
22+
ps := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&value)) + offset))
23+
*ps = cloneString(*ps)
24+
}
25+
return value
26+
}
27+
28+
// singleStringClone describes how to clone a single string.
29+
var singleStringClone = cloneSeq{stringOffsets: []uintptr{0}}
30+
31+
// cloneSeq describes how to clone a value of a particular type.
32+
type cloneSeq struct {
33+
stringOffsets []uintptr
34+
}
35+
36+
// makeCloneSeq creates a cloneSeq for a type.
37+
func makeCloneSeq(typ *abi.Type) cloneSeq {
38+
if typ == nil {
39+
return cloneSeq{}
40+
}
41+
if typ.Kind() == abi.String {
42+
return singleStringClone
43+
}
44+
var seq cloneSeq
45+
switch typ.Kind() {
46+
case abi.Struct:
47+
buildStructCloneSeq(typ, &seq, 0)
48+
case abi.Array:
49+
buildArrayCloneSeq(typ, &seq, 0)
50+
}
51+
return seq
52+
}
53+
54+
// buildStructCloneSeq populates a cloneSeq for an abi.Type that has Kind abi.Struct.
55+
func buildStructCloneSeq(typ *abi.Type, seq *cloneSeq, baseOffset uintptr) {
56+
styp := typ.StructType()
57+
for i := range styp.Fields {
58+
f := &styp.Fields[i]
59+
switch f.Typ.Kind() {
60+
case abi.String:
61+
seq.stringOffsets = append(seq.stringOffsets, baseOffset+f.Offset)
62+
case abi.Struct:
63+
buildStructCloneSeq(f.Typ, seq, baseOffset+f.Offset)
64+
case abi.Array:
65+
buildArrayCloneSeq(f.Typ, seq, baseOffset+f.Offset)
66+
}
67+
}
68+
}
69+
70+
// buildArrayCloneSeq populates a cloneSeq for an abi.Type that has Kind abi.Array.
71+
func buildArrayCloneSeq(typ *abi.Type, seq *cloneSeq, baseOffset uintptr) {
72+
atyp := typ.ArrayType()
73+
etyp := atyp.Elem
74+
offset := baseOffset
75+
for range atyp.Len {
76+
switch etyp.Kind() {
77+
case abi.String:
78+
seq.stringOffsets = append(seq.stringOffsets, offset)
79+
case abi.Struct:
80+
buildStructCloneSeq(etyp, seq, offset)
81+
case abi.Array:
82+
buildArrayCloneSeq(etyp, seq, offset)
83+
}
84+
offset += etyp.Size()
85+
align := uintptr(etyp.FieldAlign())
86+
offset = (offset + align - 1) &^ (align - 1)
87+
}
88+
}
89+
90+
// cloneString is a copy of strings.Clone, because we can't depend on the strings
91+
// package here. Several packages that might make use of unique, like net, explicitly
92+
// forbid depending on unicode, which strings depends on.
93+
func cloneString(s string) string {
94+
if len(s) == 0 {
95+
return ""
96+
}
97+
b := make([]byte, len(s))
98+
copy(b, s)
99+
return unsafe.String(&b[0], len(b))
100+
}

src/unique/clone_test.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2024 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+
package unique
6+
7+
import (
8+
"internal/abi"
9+
"internal/goarch"
10+
"reflect"
11+
"testing"
12+
)
13+
14+
func TestMakeCloneSeq(t *testing.T) {
15+
testCloneSeq[testString](t, cSeq(0))
16+
testCloneSeq[testIntArray](t, cSeq())
17+
testCloneSeq[testEface](t, cSeq())
18+
testCloneSeq[testStringArray](t, cSeq(0, 2*goarch.PtrSize, 4*goarch.PtrSize))
19+
testCloneSeq[testStringStruct](t, cSeq(0))
20+
testCloneSeq[testStringStructArrayStruct](t, cSeq(0, 2*goarch.PtrSize))
21+
testCloneSeq[testStruct](t, cSeq(8))
22+
}
23+
24+
func cSeq(stringOffsets ...uintptr) cloneSeq {
25+
return cloneSeq{stringOffsets: stringOffsets}
26+
}
27+
28+
func testCloneSeq[T any](t *testing.T, want cloneSeq) {
29+
typName := reflect.TypeFor[T]().Name()
30+
typ := abi.TypeOf(*new(T))
31+
t.Run(typName, func(t *testing.T) {
32+
got := makeCloneSeq(typ)
33+
if !reflect.DeepEqual(got, want) {
34+
t.Errorf("unexpected cloneSeq for type %s: got %#v, want %#v", typName, got, want)
35+
}
36+
})
37+
}

src/unique/doc.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright 2024 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+
/*
6+
The unique package provides facilities for canonicalizing ("interning")
7+
comparable values.
8+
*/
9+
package unique

0 commit comments

Comments
 (0)