Skip to content

Commit 97e6bec

Browse files
committed
Improvements in parser performance
1 parent e2485a6 commit 97e6bec

File tree

11 files changed

+510
-394
lines changed

11 files changed

+510
-394
lines changed

benchmark_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,17 @@ func BenchmarkVersionParser(b *testing.B) {
7272
}
7373
}
7474

75-
// Results for v0.11.0:
7675
// $ go test -benchmem -run=^$ -bench ^BenchmarkVersionParser$ go.bug.st/relaxed-semver
7776
// goos: linux
7877
// goarch: amd64
7978
// pkg: go.bug.st/relaxed-semver
8079
// cpu: AMD Ryzen 5 3600 6-Core Processor
80+
81+
// Results for v0.11.0:
8182
// BenchmarkVersionParser-12 188611 7715 ns/op 8557 B/op 51 allocs/op
83+
84+
// Results for v0.12.0: \o/
85+
// BenchmarkVersionParser-12 1298325 912.9 ns/op 0 B/op 0 allocs/op
8286
}
8387

8488
func BenchmarkVersionComparator(b *testing.B) {
@@ -99,11 +103,15 @@ func BenchmarkVersionComparator(b *testing.B) {
99103
}
100104
}
101105

102-
// Results for v0.11.0:
103106
// $ go test -benchmem -run=^$ -bench ^BenchmarkVersionComparator$ go.bug.st/relaxed-semver -v
104107
// goos: linux
105108
// goarch: amd64
106109
// pkg: go.bug.st/relaxed-semver
107110
// cpu: AMD Ryzen 5 3600 6-Core Processor
111+
112+
// Results for v0.11.0:
108113
// BenchmarkVersionComparator-12 74793 17347 ns/op 0 B/op 0 allocs/op
114+
115+
// Results for v0.12.0: :-(
116+
// BenchmarkVersionComparator-12 66262 18340 ns/op 0 B/op 0 allocs/op
109117
}

binary.go

Lines changed: 20 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,22 @@ func marshalByteArray(b []byte) []byte {
1919
return res
2020
}
2121

22-
func marshalInt(i int) []byte {
23-
res := make([]byte, 4)
24-
binary.BigEndian.PutUint32(res, uint32(i))
25-
return res
26-
}
27-
2822
// MarshalBinary implements binary custom encoding
2923
func (v *Version) MarshalBinary() ([]byte, error) {
24+
// TODO could be preallocated without bytes.Buffer
3025
res := new(bytes.Buffer)
31-
_, _ = res.Write(marshalByteArray(v.major))
32-
_, _ = res.Write(marshalByteArray(v.minor))
33-
_, _ = res.Write(marshalByteArray(v.patch))
34-
_, _ = res.Write(marshalInt(len(v.prerelases)))
35-
for _, pre := range v.prerelases {
36-
_, _ = res.Write(marshalByteArray(pre))
37-
}
38-
_, _ = res.Write(marshalInt(len(v.numericPrereleases)))
39-
for _, npre := range v.numericPrereleases {
40-
v := []byte{0}
41-
if npre {
42-
v[0] = 1
43-
}
44-
_, _ = res.Write(v)
45-
}
46-
_, _ = res.Write(marshalInt(len(v.builds)))
47-
for _, build := range v.builds {
48-
_, _ = res.Write(marshalByteArray(build))
49-
}
26+
intBuff := [4]byte{}
27+
_, _ = res.Write(marshalByteArray([]byte(v.raw)))
28+
binary.BigEndian.PutUint32(intBuff[:], uint32(v.major))
29+
_, _ = res.Write(intBuff[:])
30+
binary.BigEndian.PutUint32(intBuff[:], uint32(v.minor))
31+
_, _ = res.Write(intBuff[:])
32+
binary.BigEndian.PutUint32(intBuff[:], uint32(v.patch))
33+
_, _ = res.Write(intBuff[:])
34+
binary.BigEndian.PutUint32(intBuff[:], uint32(v.prerelease))
35+
_, _ = res.Write(intBuff[:])
36+
binary.BigEndian.PutUint32(intBuff[:], uint32(v.build))
37+
_, _ = res.Write(intBuff[:])
5038
return res.Bytes(), nil
5139
}
5240

@@ -63,31 +51,13 @@ func decodeInt(data []byte) (int, []byte) {
6351
func (v *Version) UnmarshalBinary(data []byte) error {
6452
var buff []byte
6553

66-
v.major, data = decodeArray(data)
67-
v.minor, data = decodeArray(data)
68-
v.patch, data = decodeArray(data)
69-
n, data := decodeInt(data)
70-
v.prerelases = nil
71-
for i := 0; i < n; i++ {
72-
buff, data = decodeArray(data)
73-
v.prerelases = append(v.prerelases, buff)
74-
}
75-
v.numericPrereleases = nil
76-
n, data = decodeInt(data)
77-
for i := 0; i < n; i++ {
78-
num := false
79-
if data[0] == 1 {
80-
num = true
81-
}
82-
v.numericPrereleases = append(v.numericPrereleases, num)
83-
data = data[1:]
84-
}
85-
v.builds = nil
86-
n, data = decodeInt(data)
87-
for i := 0; i < n; i++ {
88-
buff, data = decodeArray(data)
89-
v.builds = append(v.builds, buff)
90-
}
54+
buff, data = decodeArray(data)
55+
v.raw = string(buff)
56+
v.major, data = decodeInt(data)
57+
v.minor, data = decodeInt(data)
58+
v.patch, data = decodeInt(data)
59+
v.prerelease, data = decodeInt(data)
60+
v.build, _ = decodeInt(data)
9161
return nil
9262
}
9363

binary_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ func TestGOBEncoderVersion(t *testing.T) {
2020

2121
v, err := Parse(testVersion)
2222
require.NoError(t, err)
23-
dumpV := fmt.Sprintf("%s,%s,%s,%s,%v,%s", v.major, v.minor, v.patch, v.prerelases, v.numericPrereleases, v.builds)
24-
require.Equal(t, "1,2,3,[aaa 4 5 6],[false true true true],[bbb 7 8 9]", dumpV)
23+
dumpV := fmt.Sprintf("%v,%v,%v,%v,%v,%v", v.raw, v.major, v.minor, v.patch, v.prerelease, v.build)
24+
require.Equal(t, "1.2.3-aaa.4.5.6+bbb.7.8.9,1,3,5,15,25", dumpV)
2525
require.Equal(t, testVersion, v.String())
2626

2727
dataV := new(bytes.Buffer)
@@ -31,7 +31,7 @@ func TestGOBEncoderVersion(t *testing.T) {
3131
var u Version
3232
err = gob.NewDecoder(dataV).Decode(&u)
3333
require.NoError(t, err)
34-
dumpU := fmt.Sprintf("%s,%s,%s,%s,%v,%s", u.major, u.minor, u.patch, u.prerelases, u.numericPrereleases, u.builds)
34+
dumpU := fmt.Sprintf("%v,%v,%v,%v,%v,%v", v.raw, u.major, u.minor, u.patch, u.prerelease, u.build)
3535

3636
require.Equal(t, dumpV, dumpU)
3737
require.Equal(t, testVersion, u.String())

json.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ func (v *Version) UnmarshalJSON(data []byte) error {
2626
return err
2727
}
2828

29+
v.raw = parsed.raw
2930
v.major = parsed.major
3031
v.minor = parsed.minor
3132
v.patch = parsed.patch
32-
v.prerelases = parsed.prerelases
33-
v.numericPrereleases = parsed.numericPrereleases
34-
v.builds = parsed.builds
33+
v.prerelease = parsed.prerelease
34+
v.build = parsed.build
3535
return nil
3636
}
3737

json_test.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,9 @@ func TestJSONParseVersion(t *testing.T) {
2626
var u Version
2727
err = json.Unmarshal(data, &u)
2828
require.NoError(t, err)
29-
dump := fmt.Sprintf("%s,%s,%s,%s,%v,%s",
30-
u.major, u.minor, u.patch,
31-
u.prerelases, u.numericPrereleases,
32-
u.builds)
33-
require.Equal(t, "1,2,3,[aaa 4 5 6],[false true true true],[bbb 7 8 9]", dump)
29+
dump := fmt.Sprintf("%v,%v,%v,%v,%v,%v",
30+
u.raw, u.major, u.minor, u.patch, u.prerelease, u.build)
31+
require.Equal(t, "1.2.3-aaa.4.5.6+bbb.7.8.9,1,3,5,15,25", dump)
3432
require.Equal(t, testVersion, u.String())
3533

3634
err = json.Unmarshal([]byte(`"invalid"`), &u)

parser.go

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import (
1010
"fmt"
1111
)
1212

13-
var empty = []byte("")
14-
1513
// MustParse parse a version string and panic if the parsing fails
1614
func MustParse(inVersion string) *Version {
1715
res, err := Parse(inVersion)
@@ -24,9 +22,7 @@ func MustParse(inVersion string) *Version {
2422
// Parse parse a version string
2523
func Parse(inVersion string) (*Version, error) {
2624
result := &Version{
27-
major: empty[:],
28-
minor: empty[:],
29-
patch: empty[:],
25+
raw: inVersion,
3026
}
3127
if err := parseInto([]byte(inVersion), result); err != nil {
3228
return nil, err
@@ -62,8 +58,12 @@ func parseInto(in []byte, result *Version) error {
6258
return fmt.Errorf("no major version found")
6359
}
6460
if curr == '0' {
65-
result.major = in[0:1] // 0
61+
result.major = 1
6662
if !next() {
63+
result.minor = 1
64+
result.patch = 1
65+
result.prerelease = 1
66+
result.build = 1
6767
return nil
6868
}
6969
if numeric[curr] {
@@ -76,14 +76,22 @@ func parseInto(in []byte, result *Version) error {
7676
} else {
7777
for {
7878
if !next() {
79-
result.major = in[0:currIdx]
79+
result.major = currIdx
80+
result.minor = currIdx
81+
result.patch = currIdx
82+
result.prerelease = currIdx
83+
result.build = currIdx
8084
return nil
8185
}
8286
if numeric[curr] {
8387
continue
8488
}
8589
if versionSeparator[curr] {
86-
result.major = in[0:currIdx]
90+
result.major = currIdx
91+
result.minor = currIdx
92+
result.patch = currIdx
93+
result.prerelease = currIdx
94+
result.build = currIdx
8795
break
8896
}
8997
return fmt.Errorf("invalid major version separator '%c'", curr)
@@ -96,8 +104,11 @@ func parseInto(in []byte, result *Version) error {
96104
return fmt.Errorf("no minor version found")
97105
}
98106
if curr == '0' {
99-
result.minor = in[currIdx : currIdx+1] // x.0
107+
result.minor = currIdx + 1
100108
if !next() {
109+
result.patch = currIdx
110+
result.prerelease = currIdx
111+
result.build = currIdx
101112
return nil
102113
}
103114
if numeric[curr] {
@@ -108,22 +119,29 @@ func parseInto(in []byte, result *Version) error {
108119
}
109120
// Fallthrough and parse next element
110121
} else {
111-
minorIdx := currIdx
112122
for {
113123
if !next() {
114-
result.minor = in[minorIdx:currIdx]
124+
result.minor = currIdx
125+
result.patch = currIdx
126+
result.prerelease = currIdx
127+
result.build = currIdx
115128
return nil
116129
}
117130
if numeric[curr] {
118131
continue
119132
}
120133
if versionSeparator[curr] {
121-
result.minor = in[minorIdx:currIdx]
134+
result.minor = currIdx
135+
result.patch = currIdx
136+
result.prerelease = currIdx
137+
result.build = currIdx
122138
break
123139
}
124140
return fmt.Errorf("invalid minor version separator '%c'", curr)
125141
}
126142
}
143+
} else {
144+
result.minor = currIdx
127145
}
128146

129147
// Parse patch
@@ -132,8 +150,10 @@ func parseInto(in []byte, result *Version) error {
132150
return fmt.Errorf("no patch version found")
133151
}
134152
if curr == '0' {
135-
result.patch = in[currIdx : currIdx+1] // x.y.0
153+
result.patch = currIdx + 1
136154
if !next() {
155+
result.prerelease = currIdx
156+
result.build = currIdx
137157
return nil
138158
}
139159
if numeric[curr] {
@@ -144,22 +164,27 @@ func parseInto(in []byte, result *Version) error {
144164
}
145165
// Fallthrough and parse next element
146166
} else {
147-
patchIdx := currIdx
148167
for {
149168
if !next() {
150-
result.patch = in[patchIdx:currIdx]
169+
result.patch = currIdx
170+
result.prerelease = currIdx
171+
result.build = currIdx
151172
return nil
152173
}
153174
if numeric[curr] {
154175
continue
155176
}
156177
if curr == '-' || curr == '+' {
157-
result.patch = in[patchIdx:currIdx]
178+
result.patch = currIdx
179+
result.prerelease = currIdx
180+
result.build = currIdx
158181
break
159182
}
160183
return fmt.Errorf("invalid patch version separator '%c'", curr)
161184
}
162185
}
186+
} else {
187+
result.patch = currIdx
163188
}
164189

165190
// 9. A pre-release version MAY be denoted by appending a hyphen and a series
@@ -186,9 +211,9 @@ func parseInto(in []byte, result *Version) error {
186211
if zeroPrefix && !alphaIdentifier && currIdx-prereleaseIdx > 1 {
187212
return fmt.Errorf("numeric prerelease must not be prefixed with zero")
188213
}
189-
result.prerelases = append(result.prerelases, in[prereleaseIdx:currIdx])
190-
result.numericPrereleases = append(result.numericPrereleases, !alphaIdentifier)
214+
result.prerelease = currIdx
191215
if !hasNext {
216+
result.build = currIdx
192217
return nil
193218
}
194219
if curr == '+' {
@@ -214,6 +239,8 @@ func parseInto(in []byte, result *Version) error {
214239
}
215240
return fmt.Errorf("invalid prerelease separator: '%c'", curr)
216241
}
242+
} else {
243+
result.prerelease = currIdx
217244
}
218245

219246
// 10. Build metadata MAY be denoted by appending a plus sign and a series of
@@ -233,7 +260,7 @@ func parseInto(in []byte, result *Version) error {
233260
if buildIdx == currIdx {
234261
return fmt.Errorf("empty build tag not allowed")
235262
}
236-
result.builds = append(result.builds, in[buildIdx:currIdx])
263+
result.build = currIdx
237264
if !hasNext {
238265
return nil
239266
}
@@ -250,3 +277,35 @@ func parseInto(in []byte, result *Version) error {
250277
}
251278
return fmt.Errorf("invalid separator: '%c'", curr)
252279
}
280+
281+
func (v *Version) majorString() string {
282+
return v.raw[:v.major]
283+
}
284+
285+
func (v *Version) minorString() string {
286+
if v.minor > v.major {
287+
return v.raw[v.major+1 : v.minor]
288+
}
289+
return ""
290+
}
291+
292+
func (v *Version) patchString() string {
293+
if v.patch > v.minor {
294+
return v.raw[v.minor+1 : v.patch]
295+
}
296+
return ""
297+
}
298+
299+
func (v *Version) prereleaseString() string {
300+
if v.prerelease > v.patch {
301+
return v.raw[v.patch+1 : v.prerelease]
302+
}
303+
return ""
304+
}
305+
306+
func (v *Version) buildString() string {
307+
if v.build > v.prerelease {
308+
return v.raw[v.prerelease+1 : v.build]
309+
}
310+
return ""
311+
}

0 commit comments

Comments
 (0)