Skip to content

Commit 01194a0

Browse files
committed
perf(mangling): optimized the processing of initialisms
This PR significantly improves the performance of name mangling utilities (e.g. ToGoName, etc). The need for this occured while benchmarking go-swagger's CI suite: surprisingly, the topmost allocator was swag.ToGoName. It is the result of a dozen successive optimization passes driven by profiling. These functions now execute ~10x faster and need 100 times less memory allocations. See BENCHMARK.md Optimization techniques used to reduce allocations: * pointer -> value (everything was pointers: now everything is values) * interface -> struct with a Kind field * closure -> func * string to []byte: use unsafe conversion * string concatenation -> use bytes.Buffer (recyclable, unlike strings.Builder) * static values converted over and over again: pre-bake the conversions * var x []T -> make([]T, 0, heuristic size) * temporarily allocated values -> use pool to recycle previously allocated data items Optimization techniques used to reduce CPU: * read unicode rune -> short-circuit for single byte runes * map lookup -> func with switch statement * for i, v := range -> for i := range (minor impact) Signed-off-by: Frédéric BIDON <fredbi@yahoo.com>
1 parent b3e7a53 commit 01194a0

11 files changed

+828
-278
lines changed

BENCHMARK.md

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Benchmarks
2+
3+
## Name mangling utilities
4+
5+
```bash
6+
go test -bench XXX -run XXX -benchtime 30s
7+
```
8+
9+
### Benchmarks at b3e7a5386f996177e4808f11acb2aa93a0f660df
10+
11+
```
12+
goos: linux
13+
goarch: amd64
14+
pkg: github.com/go-openapi/swag
15+
cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
16+
BenchmarkToXXXName/ToGoName-4 862623 44101 ns/op 10450 B/op 732 allocs/op
17+
BenchmarkToXXXName/ToVarName-4 853656 40728 ns/op 10468 B/op 734 allocs/op
18+
BenchmarkToXXXName/ToFileName-4 1268312 27813 ns/op 9785 B/op 617 allocs/op
19+
BenchmarkToXXXName/ToCommandName-4 1276322 27903 ns/op 9785 B/op 617 allocs/op
20+
BenchmarkToXXXName/ToHumanNameLower-4 895334 40354 ns/op 10472 B/op 731 allocs/op
21+
BenchmarkToXXXName/ToHumanNameTitle-4 882441 40678 ns/op 10566 B/op 749 allocs/op
22+
```
23+
24+
### Benchmarks after PR #79
25+
26+
~ x10 performance improvement and ~ /100 memory allocations.
27+
28+
```
29+
goos: linux
30+
goarch: amd64
31+
pkg: github.com/go-openapi/swag
32+
cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
33+
BenchmarkToXXXName/ToGoName-4 9595830 3991 ns/op 42 B/op 5 allocs/op
34+
BenchmarkToXXXName/ToVarName-4 9194276 3984 ns/op 62 B/op 7 allocs/op
35+
BenchmarkToXXXName/ToFileName-4 17002711 2123 ns/op 147 B/op 7 allocs/op
36+
BenchmarkToXXXName/ToCommandName-4 16772926 2111 ns/op 147 B/op 7 allocs/op
37+
BenchmarkToXXXName/ToHumanNameLower-4 9788331 3749 ns/op 92 B/op 6 allocs/op
38+
BenchmarkToXXXName/ToHumanNameTitle-4 9188260 3941 ns/op 104 B/op 6 allocs/op
39+
```
40+
41+
```
42+
goos: linux
43+
goarch: amd64
44+
pkg: github.com/go-openapi/swag
45+
cpu: AMD Ryzen 7 5800X 8-Core Processor
46+
BenchmarkToXXXName/ToGoName-16 18527378 1972 ns/op 42 B/op 5 allocs/op
47+
BenchmarkToXXXName/ToVarName-16 15552692 2093 ns/op 62 B/op 7 allocs/op
48+
BenchmarkToXXXName/ToFileName-16 32161176 1117 ns/op 147 B/op 7 allocs/op
49+
BenchmarkToXXXName/ToCommandName-16 32256634 1137 ns/op 147 B/op 7 allocs/op
50+
BenchmarkToXXXName/ToHumanNameLower-16 18599661 1946 ns/op 92 B/op 6 allocs/op
51+
BenchmarkToXXXName/ToHumanNameTitle-16 17581353 2054 ns/op 105 B/op 6 allocs/op
52+
```

go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ module github.com/go-openapi/swag
22

33
require (
44
github.com/mailru/easyjson v0.7.7
5+
github.com/pkg/profile v1.7.0
56
github.com/stretchr/testify v1.8.0
67
gopkg.in/yaml.v3 v3.0.1
78
)
89

910
require (
1011
github.com/davecgh/go-spew v1.1.1 // indirect
12+
github.com/felixge/fgprof v0.9.3 // indirect
13+
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
1114
github.com/josharian/intern v1.0.0 // indirect
1215
github.com/kr/text v0.2.0 // indirect
1316
github.com/pmezard/go-difflib v1.0.0 // indirect

go.sum

+11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
2+
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
3+
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
14
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
25
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
36
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
47
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8+
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
9+
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
10+
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
11+
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
12+
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
513
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
614
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
715
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
@@ -12,13 +20,16 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
1220
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
1321
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
1422
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
23+
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
24+
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
1525
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1626
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1727
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
1828
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
1929
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
2030
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
2131
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
32+
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
2233
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
2334
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
2435
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

initialism_index.go

+137
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,130 @@ package swag
1616

1717
import (
1818
"sort"
19+
"strings"
1920
"sync"
2021
)
2122

23+
var (
24+
// commonInitialisms are common acronyms that are kept as whole uppercased words.
25+
commonInitialisms *indexOfInitialisms
26+
27+
// initialisms is a slice of sorted initialisms
28+
initialisms []string
29+
30+
// a copy of initialisms pre-baked as []rune
31+
initialismsRunes [][]rune
32+
initialismsUpperCased [][]rune
33+
34+
isInitialism func(string) bool
35+
36+
maxAllocMatches int
37+
)
38+
39+
func init() {
40+
// Taken from https://github.com/golang/lint/blob/3390df4df2787994aea98de825b964ac7944b817/lint.go#L732-L769
41+
configuredInitialisms := map[string]bool{
42+
"ACL": true,
43+
"API": true,
44+
"ASCII": true,
45+
"CPU": true,
46+
"CSS": true,
47+
"DNS": true,
48+
"EOF": true,
49+
"GUID": true,
50+
"HTML": true,
51+
"HTTPS": true,
52+
"HTTP": true,
53+
"ID": true,
54+
"IP": true,
55+
"IPv4": true,
56+
"IPv6": true,
57+
"JSON": true,
58+
"LHS": true,
59+
"OAI": true,
60+
"QPS": true,
61+
"RAM": true,
62+
"RHS": true,
63+
"RPC": true,
64+
"SLA": true,
65+
"SMTP": true,
66+
"SQL": true,
67+
"SSH": true,
68+
"TCP": true,
69+
"TLS": true,
70+
"TTL": true,
71+
"UDP": true,
72+
"UI": true,
73+
"UID": true,
74+
"UUID": true,
75+
"URI": true,
76+
"URL": true,
77+
"UTF8": true,
78+
"VM": true,
79+
"XML": true,
80+
"XMPP": true,
81+
"XSRF": true,
82+
"XSS": true,
83+
}
84+
85+
// a thread-safe index of initialisms
86+
commonInitialisms = newIndexOfInitialisms().load(configuredInitialisms)
87+
initialisms = commonInitialisms.sorted()
88+
initialismsRunes = asRunes(initialisms)
89+
initialismsUpperCased = asUpperCased(initialisms)
90+
maxAllocMatches = maxAllocHeuristic(initialismsRunes)
91+
92+
// a test function
93+
isInitialism = commonInitialisms.isInitialism
94+
}
95+
96+
func asRunes(in []string) [][]rune {
97+
out := make([][]rune, len(in))
98+
for i, initialism := range in {
99+
out[i] = []rune(initialism)
100+
}
101+
102+
return out
103+
}
104+
105+
func asUpperCased(in []string) [][]rune {
106+
out := make([][]rune, len(in))
107+
108+
for i, initialism := range in {
109+
out[i] = []rune(upper(trim(initialism)))
110+
}
111+
112+
return out
113+
}
114+
115+
func maxAllocHeuristic(in [][]rune) int {
116+
heuristic := make(map[rune]int)
117+
for _, initialism := range in {
118+
heuristic[initialism[0]]++
119+
}
120+
121+
var maxAlloc int
122+
for _, val := range heuristic {
123+
if val > maxAlloc {
124+
maxAlloc = val
125+
}
126+
}
127+
128+
return maxAlloc
129+
}
130+
131+
// AddInitialisms add additional initialisms
132+
func AddInitialisms(words ...string) {
133+
for _, word := range words {
134+
// commonInitialisms[upper(word)] = true
135+
commonInitialisms.add(upper(word))
136+
}
137+
// sort again
138+
initialisms = commonInitialisms.sorted()
139+
initialismsRunes = asRunes(initialisms)
140+
initialismsUpperCased = asUpperCased(initialisms)
141+
}
142+
22143
// indexOfInitialisms is a thread-safe implementation of the sorted index of initialisms.
23144
// Since go1.9, this may be implemented with sync.Map.
24145
type indexOfInitialisms struct {
@@ -63,3 +184,19 @@ func (m *indexOfInitialisms) sorted() (result []string) {
63184
sort.Sort(sort.Reverse(byInitialism(result)))
64185
return
65186
}
187+
188+
type byInitialism []string
189+
190+
func (s byInitialism) Len() int {
191+
return len(s)
192+
}
193+
func (s byInitialism) Swap(i, j int) {
194+
s[i], s[j] = s[j], s[i]
195+
}
196+
func (s byInitialism) Less(i, j int) bool {
197+
if len(s[i]) != len(s[j]) {
198+
return len(s[i]) < len(s[j])
199+
}
200+
201+
return strings.Compare(s[i], s[j]) > 0
202+
}

name_lexem.go

+38-32
Original file line numberDiff line numberDiff line change
@@ -14,74 +14,80 @@
1414

1515
package swag
1616

17-
import "unicode"
17+
import (
18+
"unicode"
19+
"unicode/utf8"
20+
)
1821

1922
type (
20-
nameLexem interface {
21-
GetUnsafeGoName() string
22-
GetOriginal() string
23-
IsInitialism() bool
24-
}
23+
lexemKind uint8
2524

26-
initialismNameLexem struct {
25+
nameLexem struct {
2726
original string
2827
matchedInitialism string
28+
kind lexemKind
2929
}
30+
)
3031

31-
casualNameLexem struct {
32-
original string
33-
}
32+
const (
33+
lexemKindCasualName lexemKind = iota
34+
lexemKindInitialismName
3435
)
3536

36-
func newInitialismNameLexem(original, matchedInitialism string) *initialismNameLexem {
37-
return &initialismNameLexem{
37+
func newInitialismNameLexem(original, matchedInitialism string) nameLexem {
38+
return nameLexem{
39+
kind: lexemKindInitialismName,
3840
original: original,
3941
matchedInitialism: matchedInitialism,
4042
}
4143
}
4244

43-
func newCasualNameLexem(original string) *casualNameLexem {
44-
return &casualNameLexem{
45+
func newCasualNameLexem(original string) nameLexem {
46+
return nameLexem{
47+
kind: lexemKindCasualName,
4548
original: original,
4649
}
4750
}
4851

49-
func (l *initialismNameLexem) GetUnsafeGoName() string {
50-
return l.matchedInitialism
51-
}
52+
func (l nameLexem) GetUnsafeGoName() string {
53+
if l.kind == lexemKindInitialismName {
54+
return l.matchedInitialism
55+
}
56+
57+
var (
58+
first rune
59+
rest string
60+
)
5261

53-
func (l *casualNameLexem) GetUnsafeGoName() string {
54-
var first rune
55-
var rest string
5662
for i, orig := range l.original {
5763
if i == 0 {
5864
first = orig
5965
continue
6066
}
67+
6168
if i > 0 {
6269
rest = l.original[i:]
6370
break
6471
}
6572
}
73+
6674
if len(l.original) > 1 {
67-
return string(unicode.ToUpper(first)) + lower(rest)
75+
b := poolOfBuffers.BorrowBuffer(utf8.UTFMax + len(rest))
76+
defer func() {
77+
poolOfBuffers.RedeemBuffer(b)
78+
}()
79+
b.WriteRune(unicode.ToUpper(first))
80+
b.WriteString(lower(rest))
81+
return b.String()
6882
}
6983

7084
return l.original
7185
}
7286

73-
func (l *initialismNameLexem) GetOriginal() string {
87+
func (l nameLexem) GetOriginal() string {
7488
return l.original
7589
}
7690

77-
func (l *casualNameLexem) GetOriginal() string {
78-
return l.original
79-
}
80-
81-
func (l *initialismNameLexem) IsInitialism() bool {
82-
return true
83-
}
84-
85-
func (l *casualNameLexem) IsInitialism() bool {
86-
return false
91+
func (l nameLexem) IsInitialism() bool {
92+
return l.kind == lexemKindInitialismName
8793
}

0 commit comments

Comments
 (0)