Skip to content

Commit 4f4fbff

Browse files
committed
Reduce number of allocations during deserialize
1 parent 59b4860 commit 4f4fbff

File tree

6 files changed

+349
-56
lines changed

6 files changed

+349
-56
lines changed

Diff for: fieldpath/serialize-pe.go

+13-14
Original file line numberDiff line numberDiff line change
@@ -29,40 +29,39 @@ var ErrUnknownPathElementType = errors.New("unknown path element type")
2929

3030
const (
3131
// Field indicates that the content of this path element is a field's name
32-
peField = "f"
32+
peField byte = 'f'
3333

3434
// Value indicates that the content of this path element is a field's value
35-
peValue = "v"
35+
peValue byte = 'v'
3636

3737
// Index indicates that the content of this path element is an index in an array
38-
peIndex = "i"
38+
peIndex byte = 'i'
3939

4040
// Key indicates that the content of this path element is a key value map
41-
peKey = "k"
41+
peKey byte = 'k'
4242

4343
// Separator separates the type of a path element from the contents
44-
peSeparator = ":"
44+
peSeparator byte = ':'
4545
)
4646

4747
var (
48-
peFieldSepBytes = []byte(peField + peSeparator)
49-
peValueSepBytes = []byte(peValue + peSeparator)
50-
peIndexSepBytes = []byte(peIndex + peSeparator)
51-
peKeySepBytes = []byte(peKey + peSeparator)
52-
peSepBytes = []byte(peSeparator)
48+
peFieldSepBytes = []byte{peField, peSeparator}
49+
peValueSepBytes = []byte{peValue, peSeparator}
50+
peIndexSepBytes = []byte{peIndex, peSeparator}
51+
peKeySepBytes = []byte{peKey, peSeparator}
5352
)
5453

5554
// DeserializePathElement parses a serialized path element
5655
func DeserializePathElement(s string) (PathElement, error) {
57-
b := []byte(s)
56+
b := builder.StringToReadOnlyByteSlice(s)
5857
if len(b) < 2 {
5958
return PathElement{}, errors.New("key must be 2 characters long")
6059
}
61-
typeSep, b := b[:2], b[2:]
62-
if typeSep[1] != peSepBytes[0] {
60+
typeSep0, typeSep1, b := b[0], b[1], b[2:]
61+
if typeSep1 != peSeparator {
6362
return PathElement{}, fmt.Errorf("missing colon: %v", s)
6463
}
65-
switch typeSep[0] {
64+
switch typeSep0 {
6665
case peFieldSepBytes[0]:
6766
// Slice s rather than convert b, to save on
6867
// allocations.

Diff for: fieldpath/serialize.go

+30-31
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
"io"
2222
"sort"
2323

24-
json "sigs.k8s.io/json"
2524
"sigs.k8s.io/structured-merge-diff/v4/internal/builder"
2625
)
2726

@@ -202,31 +201,31 @@ func (s *Set) FromJSON(r io.Reader) error {
202201
return nil
203202
}
204203

205-
type setReader struct {
206-
target *Set
207-
isMember bool
208-
}
209-
210-
func (sr *setReader) UnmarshalJSON(data []byte) error {
211-
children, isMember, err := readIterV1(data)
212-
if err != nil {
213-
return err
214-
}
215-
sr.target = children
216-
sr.isMember = isMember
217-
return nil
218-
}
219-
220204
// returns true if this subtree is also (or only) a member of parent; s is nil
221205
// if there are no further children.
222206
func readIterV1(data []byte) (children *Set, isMember bool, err error) {
223-
m := map[string]setReader{}
207+
parser := builder.NewFastObjParser(data)
224208

225-
if err := json.UnmarshalCaseSensitivePreserveInts(data, &m); err != nil {
226-
return nil, false, err
227-
}
209+
for {
210+
rawKey, err := parser.Parse()
211+
if err == io.EOF {
212+
break
213+
} else if err != nil {
214+
return nil, false, fmt.Errorf("parsing JSON: %v", err)
215+
}
216+
217+
rawValue, err := parser.Parse()
218+
if err == io.EOF {
219+
return nil, false, fmt.Errorf("unexpected EOF")
220+
} else if err != nil {
221+
return nil, false, fmt.Errorf("parsing JSON: %v", err)
222+
}
223+
224+
k, err := builder.UnmarshalString(rawKey)
225+
if err != nil {
226+
return nil, false, fmt.Errorf("decoding key: %v", err)
227+
}
228228

229-
for k, v := range m {
230229
if k == "." {
231230
isMember = true
232231
continue
@@ -242,7 +241,12 @@ func readIterV1(data []byte) (children *Set, isMember bool, err error) {
242241
return nil, false, fmt.Errorf("parsing key as path element: %v", err)
243242
}
244243

245-
if v.isMember {
244+
grandChildren, isChildMember, err := readIterV1(rawValue)
245+
if err != nil {
246+
return nil, false, fmt.Errorf("parsing value as set: %v", err)
247+
}
248+
249+
if isChildMember {
246250
if children == nil {
247251
children = &Set{}
248252
}
@@ -252,26 +256,21 @@ func readIterV1(data []byte) (children *Set, isMember bool, err error) {
252256
*m = append(*m, pe)
253257
}
254258

255-
if v.target != nil {
259+
if grandChildren != nil {
256260
if children == nil {
257261
children = &Set{}
258262
}
259263

260264
// Append the child to the children list, we will sort it later
261265
m := &children.Children.members
262-
*m = append(*m, setNode{pe, v.target})
266+
*m = append(*m, setNode{pe, grandChildren})
263267
}
264268
}
265269

266270
// Sort the members and children
267271
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-
})
272+
sort.Sort(children.Members.members)
273+
sort.Sort(children.Children.members)
275274
}
276275

277276
if children == nil {

Diff for: internal/builder/fastobjparse.go

+224
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package builder
2+
3+
import (
4+
gojson "encoding/json"
5+
"fmt"
6+
"io"
7+
"reflect"
8+
"runtime"
9+
"unsafe"
10+
11+
"sigs.k8s.io/json"
12+
)
13+
14+
type parserState int
15+
16+
const (
17+
stateLookingForObj parserState = iota
18+
stateLookingForItem
19+
stateLookingForKeyValueSep
20+
stateLookingForItemSep
21+
stateLookingForValue
22+
stateEnd
23+
)
24+
25+
type FastObjParser struct {
26+
input []byte
27+
pos int
28+
29+
state parserState
30+
}
31+
32+
func NewFastObjParser(input []byte) FastObjParser {
33+
return FastObjParser{
34+
input: input,
35+
state: stateLookingForObj,
36+
}
37+
}
38+
39+
var whitespace = [256]bool{
40+
' ': true,
41+
'\t': true,
42+
'\n': true,
43+
'\r': true,
44+
}
45+
46+
func isWhitespace(c byte) bool {
47+
return whitespace[c]
48+
}
49+
50+
func (p *FastObjParser) getValue(startPos int) ([]byte, error) {
51+
foundRootValue := false
52+
isQuoted := false
53+
isEscaped := false
54+
level := 0
55+
i := startPos
56+
Loop:
57+
for ; i < len(p.input); i++ {
58+
if isQuoted {
59+
// Skip escaped character
60+
if isEscaped {
61+
isEscaped = false
62+
continue
63+
}
64+
65+
switch p.input[i] {
66+
case '\\':
67+
isEscaped = true
68+
case '"':
69+
isQuoted = false
70+
}
71+
72+
continue
73+
}
74+
75+
// Skip whitespace
76+
if isWhitespace(p.input[i]) {
77+
continue
78+
}
79+
80+
// If we are at the top level and find the next object, we are done
81+
if level == 0 && foundRootValue {
82+
switch p.input[i] {
83+
case ',', '}', ']', ':', '{', '[':
84+
break Loop
85+
}
86+
}
87+
88+
switch p.input[i] {
89+
// Keep track of the nesting level
90+
case '{':
91+
level++
92+
case '}':
93+
level--
94+
case '[':
95+
level++
96+
case ']':
97+
level--
98+
99+
// Start of a string
100+
case '"':
101+
isQuoted = true
102+
}
103+
104+
foundRootValue = true
105+
}
106+
107+
if level != 0 {
108+
return nil, fmt.Errorf("expected '}' or ']' but reached end of input")
109+
}
110+
111+
if isQuoted {
112+
return nil, fmt.Errorf("expected '\"' but reached end of input")
113+
}
114+
115+
if !foundRootValue {
116+
return nil, fmt.Errorf("expected value but reached end of input")
117+
}
118+
119+
return p.input[startPos:i], nil
120+
}
121+
122+
func (p *FastObjParser) Parse() ([]byte, error) {
123+
for {
124+
if p.pos >= len(p.input) {
125+
return nil, io.EOF
126+
}
127+
128+
// Skip whitespace
129+
if isWhitespace(p.input[p.pos]) {
130+
p.pos++
131+
continue
132+
}
133+
134+
switch p.state {
135+
case stateLookingForObj:
136+
if p.input[p.pos] != '{' {
137+
return nil, fmt.Errorf("expected '{' at position %d", p.pos)
138+
}
139+
140+
p.state = stateLookingForItem
141+
142+
case stateLookingForItem:
143+
if p.input[p.pos] == '}' {
144+
p.state = stateEnd
145+
return nil, io.EOF
146+
}
147+
148+
strSlice, err := p.getValue(p.pos)
149+
if err != nil {
150+
return nil, err
151+
}
152+
153+
p.pos += len(strSlice)
154+
p.state = stateLookingForKeyValueSep
155+
return strSlice, nil
156+
157+
case stateLookingForKeyValueSep:
158+
if p.input[p.pos] != ':' {
159+
return nil, fmt.Errorf("expected ':' at position %d", p.pos)
160+
}
161+
162+
p.state = stateLookingForValue
163+
164+
case stateLookingForValue:
165+
valueSlice, err := p.getValue(p.pos)
166+
if err != nil {
167+
return nil, err
168+
}
169+
170+
p.pos += len(valueSlice)
171+
p.state = stateLookingForItemSep
172+
return valueSlice, nil
173+
174+
case stateLookingForItemSep:
175+
if p.input[p.pos] == ',' {
176+
p.state = stateLookingForItem
177+
} else if p.input[p.pos] == '}' {
178+
p.state = stateEnd
179+
} else {
180+
return nil, fmt.Errorf("expected ',' or '}' at position %d", p.pos)
181+
}
182+
183+
case stateEnd:
184+
return nil, io.EOF
185+
}
186+
187+
p.pos++
188+
}
189+
}
190+
191+
func UnmarshalString(input []byte) (string, error) {
192+
var v string
193+
// No need to enable case sensitivity or int preservation here, as we are only unmarshalling strings.
194+
if err := gojson.Unmarshal(input, (*string)(noescape(unsafe.Pointer(&v)))); err != nil {
195+
return "", err
196+
}
197+
198+
runtime.KeepAlive(v)
199+
200+
return v, nil
201+
}
202+
203+
func UnmarshalInterface(input []byte) (interface{}, error) {
204+
var v interface{}
205+
if err := json.UnmarshalCaseSensitivePreserveInts(input, (*interface{})(noescape(unsafe.Pointer(&v)))); err != nil {
206+
return "", err
207+
}
208+
209+
runtime.KeepAlive(v)
210+
211+
return v, nil
212+
}
213+
214+
// Create a read-only byte array from a string
215+
func StringToReadOnlyByteSlice(s string) []byte {
216+
// Get StringHeader from string
217+
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
218+
219+
// Construct SliceHeader with capacity equal to the length
220+
sliceHeader := reflect.SliceHeader{Data: stringHeader.Data, Len: stringHeader.Len, Cap: stringHeader.Len}
221+
222+
// Convert SliceHeader to a byte slice
223+
return *(*[]byte)(unsafe.Pointer(&sliceHeader))
224+
}

0 commit comments

Comments
 (0)