Skip to content

Commit b8f304c

Browse files
committed
wip: reduce nr allocations
1 parent d23b92a commit b8f304c

File tree

6 files changed

+133
-59
lines changed

6 files changed

+133
-59
lines changed

Diff for: fieldpath/path.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func (fp Path) Copy() Path {
8080

8181
// MakePath constructs a Path. The parts may be PathElements, ints, strings.
8282
func MakePath(parts ...interface{}) (Path, error) {
83-
var fp Path
83+
fp := make(Path, 0, len(parts))
8484
for _, p := range parts {
8585
switch t := p.(type) {
8686
case PathElement:

Diff for: fieldpath/serialize-pe.go

+8-13
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@ package fieldpath
1919
import (
2020
"errors"
2121
"fmt"
22-
"io"
2322
"strconv"
24-
"strings"
2523

24+
"sigs.k8s.io/structured-merge-diff/v4/internal/builder"
2625
"sigs.k8s.io/structured-merge-diff/v4/value"
2726
)
2827

@@ -57,7 +56,7 @@ var (
5756
func DeserializePathElement(s string) (PathElement, error) {
5857
b := []byte(s)
5958
if len(b) < 2 {
60-
return PathElement{}, errors.New("key must be 2 characters long:")
59+
return PathElement{}, errors.New("key must be 2 characters long")
6160
}
6261
typeSep, b := b[:2], b[2:]
6362
if typeSep[1] != peSepBytes[0] {
@@ -99,41 +98,37 @@ func DeserializePathElement(s string) (PathElement, error) {
9998

10099
// SerializePathElement serializes a path element
101100
func SerializePathElement(pe PathElement) (string, error) {
102-
buf := strings.Builder{}
101+
buf := builder.JSONBuilder{}
103102
err := serializePathElementToWriter(&buf, pe)
104103
return buf.String(), err
105104
}
106105

107-
func serializePathElementToWriter(w io.Writer, pe PathElement) error {
106+
func serializePathElementToWriter(w *builder.JSONBuilder, pe PathElement) error {
108107
switch {
109108
case pe.FieldName != nil:
110109
if _, err := w.Write(peFieldSepBytes); err != nil {
111110
return err
112111
}
113-
fmt.Fprintf(w, "%s", *pe.FieldName)
112+
w.WriteString(*pe.FieldName)
114113
case pe.Key != nil:
115114
if _, err := w.Write(peKeySepBytes); err != nil {
116115
return err
117116
}
118-
jsonVal, err := value.FieldListToJSON(*pe.Key)
119-
if err != nil {
117+
if err := value.FieldListToJSON(*pe.Key, w); err != nil {
120118
return err
121119
}
122-
fmt.Fprintf(w, "%s", jsonVal)
123120
case pe.Value != nil:
124121
if _, err := w.Write(peValueSepBytes); err != nil {
125122
return err
126123
}
127-
jsonVal, err := value.ToJSON(*pe.Value)
128-
if err != nil {
124+
if err := value.ToJSON(*pe.Value, w); err != nil {
129125
return err
130126
}
131-
fmt.Fprintf(w, "%s", jsonVal)
132127
case pe.Index != nil:
133128
if _, err := w.Write(peIndexSepBytes); err != nil {
134129
return err
135130
}
136-
fmt.Fprintf(w, "%d", *pe.Index)
131+
w.WriteString(strconv.Itoa(*pe.Index))
137132
default:
138133
return errors.New("invalid PathElement")
139134
}

Diff for: fieldpath/serialize.go

+45-36
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,36 @@ limitations under the License.
1717
package fieldpath
1818

1919
import (
20-
"bytes"
21-
gojson "encoding/json"
2220
"fmt"
2321
"io"
22+
"sort"
2423

2524
json "sigs.k8s.io/json"
25+
"sigs.k8s.io/structured-merge-diff/v4/internal/builder"
2626
)
2727

2828
func (s *Set) ToJSON() ([]byte, error) {
29-
buf := bytes.Buffer{}
30-
err := s.ToJSONStream(&buf)
31-
if err != nil {
29+
buf := builder.JSONBuilder{}
30+
if err := s.emitContentsV1(false, &buf, &builder.ReusableBuilder{}); err != nil {
3231
return nil, err
3332
}
3433
return buf.Bytes(), nil
3534
}
3635

3736
func (s *Set) ToJSONStream(w io.Writer) error {
38-
err := s.emitContentsV1(false, w)
39-
if err != nil {
37+
buf := builder.JSONBuilder{}
38+
if err := s.emitContentsV1(false, &buf, &builder.ReusableBuilder{}); err != nil {
4039
return err
4140
}
42-
return nil
41+
_, err := buf.WriteTo(w)
42+
return err
4343
}
4444

4545
type orderedMapItemWriter struct {
46-
w io.Writer
46+
w *builder.JSONBuilder
4747
hasItems bool
48+
49+
builder *builder.ReusableBuilder
4850
}
4951

5052
// writeKey writes a key to the writer, including a leading comma if necessary.
@@ -74,16 +76,24 @@ func (om *orderedMapItemWriter) writeKey(key []byte) error {
7476
// After writing the key, the caller should write the encoded value, e.g. using
7577
// writeEmptyValue or by directly writing the value to the writer.
7678
func (om *orderedMapItemWriter) writePathKey(pe PathElement) error {
77-
pev, err := SerializePathElement(pe)
78-
if err != nil {
79+
if om.hasItems {
80+
if _, err := om.w.Write([]byte{','}); err != nil {
81+
return err
82+
}
83+
}
84+
85+
if err := serializePathElementToWriter(om.builder.Reset(), pe); err != nil {
7986
return err
8087
}
81-
key, err := gojson.Marshal(pev)
82-
if err != nil {
88+
if err := om.w.WriteJSON(om.builder.UnsafeString()); err != nil {
8389
return err
8490
}
8591

86-
return om.writeKey(key)
92+
if _, err := om.w.Write([]byte{':'}); err != nil {
93+
return err
94+
}
95+
om.hasItems = true
96+
return nil
8797
}
8898

8999
// writeEmptyValue writes an empty JSON object to the writer.
@@ -95,11 +105,11 @@ func (om orderedMapItemWriter) writeEmptyValue() error {
95105
return nil
96106
}
97107

98-
func (s *Set) emitContentsV1(includeSelf bool, w io.Writer) error {
99-
om := orderedMapItemWriter{w: w}
108+
func (s *Set) emitContentsV1(includeSelf bool, w *builder.JSONBuilder, r *builder.ReusableBuilder) error {
109+
om := orderedMapItemWriter{w: w, builder: r}
100110
mi, ci := 0, 0
101111

102-
if _, err := om.w.Write([]byte{'{'}); err != nil {
112+
if _, err := w.Write([]byte{'{'}); err != nil {
103113
return err
104114
}
105115

@@ -129,7 +139,7 @@ func (s *Set) emitContentsV1(includeSelf bool, w io.Writer) error {
129139
if err := om.writePathKey(cpe); err != nil {
130140
return err
131141
}
132-
if err := s.Children.members[ci].set.emitContentsV1(c == 0, om.w); err != nil {
142+
if err := s.Children.members[ci].set.emitContentsV1(c == 0, w, r); err != nil {
133143
return err
134144
}
135145

@@ -160,14 +170,14 @@ func (s *Set) emitContentsV1(includeSelf bool, w io.Writer) error {
160170
if err := om.writePathKey(cpe); err != nil {
161171
return err
162172
}
163-
if err := s.Children.members[ci].set.emitContentsV1(false, om.w); err != nil {
173+
if err := s.Children.members[ci].set.emitContentsV1(false, w, r); err != nil {
164174
return err
165175
}
166176

167177
ci++
168178
}
169179

170-
if _, err := om.w.Write([]byte{'}'}); err != nil {
180+
if _, err := w.Write([]byte{'}'}); err != nil {
171181
return err
172182
}
173183

@@ -237,34 +247,33 @@ func readIterV1(data []byte) (children *Set, isMember bool, err error) {
237247
children = &Set{}
238248
}
239249

250+
// Append the member to the members list, we will sort it later
240251
m := &children.Members.members
241-
// Since we expect that most of the time these will have been
242-
// serialized in the right order, we just verify that and append.
243-
appendOK := len(*m) == 0 || (*m)[len(*m)-1].Less(pe)
244-
if appendOK {
245-
*m = append(*m, pe)
246-
} else {
247-
children.Members.Insert(pe)
248-
}
252+
*m = append(*m, pe)
249253
}
250254

251255
if v.target != nil {
252256
if children == nil {
253257
children = &Set{}
254258
}
255259

256-
// Since we expect that most of the time these will have been
257-
// serialized in the right order, we just verify that and append.
260+
// Append the child to the children list, we will sort it later
258261
m := &children.Children.members
259-
appendOK := len(*m) == 0 || (*m)[len(*m)-1].pathElement.Less(pe)
260-
if appendOK {
261-
*m = append(*m, setNode{pe, v.target})
262-
} else {
263-
*children.Children.Descend(pe) = *v.target
264-
}
262+
*m = append(*m, setNode{pe, v.target})
265263
}
266264
}
267265

266+
// Sort the members and children
267+
if children != nil {
268+
sort.Slice(children.Members.members, func(i, j int) bool {
269+
return children.Members.members[i].Less(children.Members.members[j])
270+
})
271+
272+
sort.Slice(children.Children.members, func(i, j int) bool {
273+
return children.Children.members[i].pathElement.Less(children.Children.members[j].pathElement)
274+
})
275+
}
276+
268277
if children == nil {
269278
isMember = true
270279
}

Diff for: internal/builder/builder.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package builder
2+
3+
import (
4+
"bytes"
5+
gojson "encoding/json"
6+
"unsafe"
7+
)
8+
9+
type ReusableBuilder struct {
10+
JSONBuilder
11+
}
12+
13+
func (r *ReusableBuilder) Reset() *JSONBuilder {
14+
r.JSONBuilder.Reset()
15+
return &r.JSONBuilder
16+
}
17+
18+
func (r *ReusableBuilder) UnsafeString() string {
19+
b := r.Bytes()
20+
return *(*string)(unsafe.Pointer(&b))
21+
}
22+
23+
type JSONBuilder struct {
24+
initialised bool
25+
bytes.Buffer
26+
gojson.Encoder
27+
}
28+
29+
type noNewlineWriter struct {
30+
*bytes.Buffer
31+
}
32+
33+
func (w noNewlineWriter) Write(p []byte) (n int, err error) {
34+
if len(p) > 0 && p[len(p)-1] == '\n' {
35+
p = p[:len(p)-1]
36+
}
37+
return w.Buffer.Write(p)
38+
}
39+
40+
// noescape hides a pointer from escape analysis. It is the identity function
41+
// but escape analysis doesn't think the output depends on the input.
42+
// noescape is inlined and currently compiles down to zero instructions.
43+
// USE CAREFULLY!
44+
// This was copied from the runtime; see issues 23382 and 7921.
45+
//
46+
//go:nosplit
47+
//go:nocheckptr
48+
func noescape(p unsafe.Pointer) unsafe.Pointer {
49+
x := uintptr(p)
50+
return unsafe.Pointer(x ^ 0) //nolint:unsafeptr
51+
}
52+
53+
func (r *JSONBuilder) WriteJSON(v interface{}) error {
54+
if !r.initialised {
55+
r.Encoder = *gojson.NewEncoder(noNewlineWriter{&r.Buffer})
56+
r.initialised = true
57+
}
58+
59+
return r.Encoder.Encode((*interface{})(noescape(unsafe.Pointer(&v))))
60+
}

Diff for: value/fields.go

+16-6
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ limitations under the License.
1717
package value
1818

1919
import (
20-
gojson "encoding/json"
2120
"sort"
2221
"strings"
2322

2423
"sigs.k8s.io/json"
24+
"sigs.k8s.io/structured-merge-diff/v4/internal/builder"
2525
)
2626

2727
// Field is an individual key-value pair.
@@ -50,12 +50,22 @@ func FieldListFromJSON(input []byte) (FieldList, error) {
5050
}
5151

5252
// FieldListToJSON is a helper function for producing a JSON document.
53-
func FieldListToJSON(v FieldList) ([]byte, error) {
54-
m := make(map[string]interface{}, len(v))
55-
for _, f := range v {
56-
m[f.Name] = f.Value.Unstructured()
53+
func FieldListToJSON(v FieldList, w *builder.JSONBuilder) error {
54+
w.WriteByte('{')
55+
for i, f := range v {
56+
if err := w.WriteJSON(f.Name); err != nil {
57+
return err
58+
}
59+
w.WriteByte(':')
60+
if err := w.WriteJSON(f.Value.Unstructured()); err != nil {
61+
return err
62+
}
63+
if i < len(v)-1 {
64+
w.WriteByte(',')
65+
}
5766
}
58-
return gojson.Marshal(m)
67+
w.WriteByte('}')
68+
return nil
5969
}
6070

6171
// Sort sorts the field list by Name.

Diff for: value/value.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ limitations under the License.
1717
package value
1818

1919
import (
20-
gojson "encoding/json"
2120
"fmt"
2221
"strings"
2322

2423
"gopkg.in/yaml.v2"
2524
"sigs.k8s.io/json"
25+
"sigs.k8s.io/structured-merge-diff/v4/internal/builder"
2626
)
2727

2828
// A Value corresponds to an 'atom' in the schema. It should return true
@@ -85,8 +85,8 @@ func FromJSON(input []byte) (Value, error) {
8585
}
8686

8787
// ToJSON is a helper function for producing a JSON document.
88-
func ToJSON(v Value) ([]byte, error) {
89-
return gojson.Marshal(v.Unstructured())
88+
func ToJSON(v Value, w *builder.JSONBuilder) error {
89+
return w.WriteJSON(v.Unstructured())
9090
}
9191

9292
// ToYAML marshals a value as YAML.

0 commit comments

Comments
 (0)