Skip to content

Commit b4bf3c6

Browse files
authored
Merge pull request #17 from bugst/performance
Increase parsing and comparison performance
2 parents 391e859 + fee3392 commit b4bf3c6

11 files changed

+783
-420
lines changed

benchmark_test.go

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//
2+
// Copyright 2018-2023 Cristian Maglie. All rights reserved.
3+
// Use of this source code is governed by a BSD-style
4+
// license that can be found in the LICENSE file.
5+
//
6+
7+
package semver
8+
9+
import (
10+
"testing"
11+
)
12+
13+
var list = []string{
14+
"0.0.1-rc.0", // 0
15+
"0.0.1-rc.0+build", // 1
16+
"0.0.1-rc.1", // 2
17+
"0.0.1", // 3
18+
"0.0.1+build", // 4
19+
"0.0.2-rc.1", // 5 - BREAKING CHANGE
20+
"0.0.2-rc.1+build", // 6
21+
"0.0.2", // 7
22+
"0.0.2+build", // 8
23+
"0.0.3-rc.1", // 9 - BREAKING CHANGE
24+
"0.0.3-rc.2", // 10
25+
"0.0.3", // 11
26+
"0.1.0", // 12 - BREAKING CHANGE
27+
"0.3.3-rc.0", // 13 - BREAKING CHANGE
28+
"0.3.3-rc.1", // 14
29+
"0.3.3", // 15
30+
"0.3.3+build", // 16
31+
"0.3.4-rc.1", // 17
32+
"0.3.4", // 18
33+
"0.4.0", // 19 - BREAKING CHANGE
34+
"1.0.0-rc", // 20 - BREAKING CHANGE
35+
"1.0.0", // 21
36+
"1.0.0+build", // 22
37+
"1.2.1-rc", // 23
38+
"1.2.1", // 24
39+
"1.2.1+build", // 25
40+
"1.2.3-rc.2", // 26
41+
"1.2.3-rc.2+build", // 27
42+
"1.2.3", // 28
43+
"1.2.3+build", // 29
44+
"1.2.4", // 30
45+
"1.3.0-rc.0+build", // 31
46+
"1.3.0", // 32
47+
"1.3.0+build", // 33
48+
"1.3.1-rc.0", // 34
49+
"1.3.1-rc.1", // 35
50+
"1.3.1", // 36
51+
"1.3.5", // 37
52+
"2.0.0-rc", // 38 - BREAKING CHANGE
53+
"2.0.0-rc+build", // 39
54+
"2.0.0", // 40
55+
"2.0.0+build", // 41
56+
"2.1.0-rc", // 42
57+
"2.1.0-rc+build", // 43
58+
"2.1.0", // 44
59+
"2.1.0+build", // 45
60+
"2.1.3-rc", // 46
61+
"2.1.3", // 47
62+
"2.3.0", // 48
63+
"2.3.1", // 49
64+
"3.0.0", // 50 - BREAKING CHANGE
65+
}
66+
67+
func BenchmarkVersionParser(b *testing.B) {
68+
res := &Version{}
69+
for i := 0; i < b.N; i++ {
70+
for _, v := range list {
71+
res.raw = v
72+
res.bytes = []byte(v)
73+
parse(res)
74+
}
75+
}
76+
77+
// $ go test -benchmem -run=^$ -bench ^BenchmarkVersionParser$ go.bug.st/relaxed-semver
78+
// goos: linux
79+
// goarch: amd64
80+
// pkg: go.bug.st/relaxed-semver
81+
// cpu: AMD Ryzen 5 3600 6-Core Processor
82+
83+
// Results for v0.11.0:
84+
// BenchmarkVersionParser-12 188611 7715 ns/op 8557 B/op 51 allocs/op
85+
86+
// Results for v0.12.0: \o/
87+
// BenchmarkVersionParser-12 479626 3719 ns/op 616 B/op 51 allocs/op
88+
}
89+
90+
func BenchmarkVersionComparator(b *testing.B) {
91+
b.StopTimer()
92+
vList := []*Version{}
93+
for _, in := range list {
94+
vList = append(vList, MustParse(in))
95+
}
96+
l := len(vList)
97+
b.StartTimer()
98+
99+
for i := 0; i < b.N; i++ {
100+
// cross compare all versions
101+
for x := 0; x < l; x++ {
102+
for y := 0; y < l; y++ {
103+
vList[x].CompareTo(vList[y])
104+
}
105+
}
106+
}
107+
108+
// $ go test -benchmem -run=^$ -bench ^BenchmarkVersionComparator$ go.bug.st/relaxed-semver -v
109+
// goos: linux
110+
// goarch: amd64
111+
// pkg: go.bug.st/relaxed-semver
112+
// cpu: AMD Ryzen 5 3600 6-Core Processor
113+
114+
// Results for v0.11.0:
115+
// BenchmarkVersionComparator-12 74793 17347 ns/op 0 B/op 0 allocs/op
116+
117+
// Results for v0.12.0: :-D
118+
// BenchmarkVersionComparator-12 101772 11720 ns/op 0 B/op 0 allocs/op
119+
}

binary.go

+21-50
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,14 @@ 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.bytes = []byte(v.raw)
57+
v.major, data = decodeInt(data)
58+
v.minor, data = decodeInt(data)
59+
v.patch, data = decodeInt(data)
60+
v.prerelease, data = decodeInt(data)
61+
v.build, _ = decodeInt(data)
9162
return nil
9263
}
9364

binary_test.go

+15-3
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,10 +31,22 @@ 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())
38+
39+
{
40+
dataV := new(bytes.Buffer)
41+
dataU := new(bytes.Buffer)
42+
require.NoError(t, gob.NewEncoder(dataV).Encode(MustParse("1.6.2")))
43+
require.NoError(t, gob.NewEncoder(dataU).Encode(MustParse("1.6.3")))
44+
45+
var v, u *Version
46+
require.NoError(t, gob.NewDecoder(dataV).Decode(&v))
47+
require.NoError(t, gob.NewDecoder(dataU).Decode(&u))
48+
require.True(t, u.GreaterThan(v))
49+
}
3850
}
3951

4052
func TestGOBEncoderRelaxedVersion(t *testing.T) {

json.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@ func (v *Version) UnmarshalJSON(data []byte) error {
2626
return err
2727
}
2828

29+
v.raw = parsed.raw
30+
v.bytes = []byte(v.raw)
2931
v.major = parsed.major
3032
v.minor = parsed.minor
3133
v.patch = parsed.patch
32-
v.prerelases = parsed.prerelases
33-
v.numericPrereleases = parsed.numericPrereleases
34-
v.builds = parsed.builds
34+
v.prerelease = parsed.prerelease
35+
v.build = parsed.build
3536
return nil
3637
}
3738

json_test.go

+7-5
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,20 @@ 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)
3735
require.Error(t, err)
3836

3937
err = json.Unmarshal([]byte(`123`), &u)
4038
require.Error(t, err)
39+
40+
require.NoError(t, json.Unmarshal([]byte(`"1.6.2"`), &v))
41+
require.NoError(t, json.Unmarshal([]byte(`"1.6.3"`), &u))
42+
require.True(t, u.GreaterThan(v))
4143
}
4244

4345
func TestJSONParseRelaxedVersion(t *testing.T) {

0 commit comments

Comments
 (0)