Skip to content

Commit a27abbd

Browse files
optimization cleanPath
1 parent c221133 commit a27abbd

File tree

2 files changed

+163
-41
lines changed

2 files changed

+163
-41
lines changed

path.go

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,29 @@ package gin
1919
//
2020
// If the result of this process is an empty string, "/" is returned.
2121
func cleanPath(p string) string {
22-
const stackBufSize = 128
2322
// Turn empty string into "/"
2423
if p == "" {
2524
return "/"
2625
}
2726

27+
n := len(p)
28+
29+
// If the path length is 1, handle special cases separately:
30+
// - If it is "/" or ".", return "/".
31+
// - Otherwise, prepend "/" to the path and return it.
32+
if n == 1 {
33+
if p[0] == '/' || p[0] == '.' {
34+
return "/"
35+
}
36+
return "/" + p
37+
}
38+
39+
const stackBufSize = 128
40+
2841
// Reasonably sized buffer on stack to avoid allocations in the common case.
2942
// If a larger buffer is required, it gets allocated dynamically.
3043
buf := make([]byte, 0, stackBufSize)
3144

32-
n := len(p)
33-
3445
// Invariants:
3546
// reading from path; r is index of next byte to process.
3647
// writing to buf; w is index of next byte to write.
@@ -39,57 +50,52 @@ func cleanPath(p string) string {
3950
r := 1
4051
w := 1
4152

42-
if p[0] != '/' {
43-
r = 0
44-
45-
if n+1 > stackBufSize {
46-
buf = make([]byte, n+1)
47-
} else {
48-
buf = buf[:n+1]
49-
}
50-
buf[0] = '/'
51-
}
52-
53-
trailing := n > 1 && p[n-1] == '/'
53+
var trailing bool
5454

5555
// A bit more clunky without a 'lazybuf' like the path package, but the loop
5656
// gets completely inlined (bufApp calls).
5757
// loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
5858
// calls (except make, if needed).
5959

6060
for r < n {
61-
switch {
62-
case p[r] == '/':
61+
switch p[r] {
62+
case '/':
6363
// empty path element, trailing slash is added after the end
6464
r++
6565

66-
case p[r] == '.' && r+1 == n:
67-
trailing = true
68-
r++
69-
70-
case p[r] == '.' && p[r+1] == '/':
71-
// . element
72-
r += 2
73-
74-
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
75-
// .. element: remove to last /
76-
r += 3
77-
78-
if w > 1 {
79-
// can backtrack
80-
w--
81-
82-
if len(buf) == 0 {
83-
for w > 1 && p[w] != '/' {
84-
w--
85-
}
86-
} else {
87-
for w > 1 && buf[w] != '/' {
66+
case '.':
67+
if r+1 == n {
68+
trailing = true
69+
r++
70+
// Reduce one comparison between r and n
71+
goto endOfLoop
72+
}
73+
switch p[r+1] {
74+
case '/':
75+
// . element
76+
r += 2
77+
78+
case '.':
79+
if r+2 == n || p[r+2] == '/' {
80+
// .. element: remove to last /
81+
r += 3
82+
83+
if w > 1 {
84+
// can backtrack
8885
w--
86+
87+
if len(buf) == 0 {
88+
for w > 1 && p[w] != '/' {
89+
w--
90+
}
91+
} else {
92+
for w > 1 && buf[w] != '/' {
93+
w--
94+
}
95+
}
8996
}
9097
}
9198
}
92-
9399
default:
94100
// Real path element.
95101
// Add slash if needed
@@ -107,8 +113,9 @@ func cleanPath(p string) string {
107113
}
108114
}
109115

116+
endOfLoop:
110117
// Re-append trailing slash
111-
if trailing && w > 1 {
118+
if (trailing || p[n-1] == '/') && w > 1 {
112119
bufApp(&buf, p, w, '/')
113120
w++
114121
}

path_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ var cleanTests = []cleanPathTest{
2727

2828
// missing root
2929
{"", "/"},
30+
{"a", "/a"},
3031
{"a/", "/a/"},
3132
{"abc", "/abc"},
3233
{"abc/def", "/abc/def"},
@@ -143,3 +144,117 @@ func BenchmarkPathCleanLong(b *testing.B) {
143144
}
144145
}
145146
}
147+
148+
func cleanPathOld(p string) string {
149+
const stackBufSize = 128
150+
// Turn empty string into "/"
151+
if p == "" {
152+
return "/"
153+
}
154+
155+
// Reasonably sized buffer on stack to avoid allocations in the common case.
156+
// If a larger buffer is required, it gets allocated dynamically.
157+
buf := make([]byte, 0, stackBufSize)
158+
159+
n := len(p)
160+
161+
// Invariants:
162+
// reading from path; r is index of next byte to process.
163+
// writing to buf; w is index of next byte to write.
164+
165+
// path must start with '/'
166+
r := 1
167+
w := 1
168+
169+
if p[0] != '/' {
170+
r = 0
171+
172+
if n+1 > stackBufSize {
173+
buf = make([]byte, n+1)
174+
} else {
175+
buf = buf[:n+1]
176+
}
177+
buf[0] = '/'
178+
}
179+
180+
trailing := n > 1 && p[n-1] == '/'
181+
182+
// A bit more clunky without a 'lazybuf' like the path package, but the loop
183+
// gets completely inlined (bufApp calls).
184+
// loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
185+
// calls (except make, if needed).
186+
187+
for r < n {
188+
switch {
189+
case p[r] == '/':
190+
// empty path element, trailing slash is added after the end
191+
r++
192+
193+
case p[r] == '.' && r+1 == n:
194+
trailing = true
195+
r++
196+
197+
case p[r] == '.' && p[r+1] == '/':
198+
// . element
199+
r += 2
200+
201+
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
202+
// .. element: remove to last /
203+
r += 3
204+
205+
if w > 1 {
206+
// can backtrack
207+
w--
208+
209+
if len(buf) == 0 {
210+
for w > 1 && p[w] != '/' {
211+
w--
212+
}
213+
} else {
214+
for w > 1 && buf[w] != '/' {
215+
w--
216+
}
217+
}
218+
}
219+
220+
default:
221+
// Real path element.
222+
// Add slash if needed
223+
if w > 1 {
224+
bufApp(&buf, p, w, '/')
225+
w++
226+
}
227+
228+
// Copy element
229+
for r < n && p[r] != '/' {
230+
bufApp(&buf, p, w, p[r])
231+
w++
232+
r++
233+
}
234+
}
235+
}
236+
237+
// Re-append trailing slash
238+
if trailing && w > 1 {
239+
bufApp(&buf, p, w, '/')
240+
w++
241+
}
242+
243+
// If the original string was not modified (or only shortened at the end),
244+
// return the respective substring of the original string.
245+
// Otherwise return a new string from the buffer.
246+
if len(buf) == 0 {
247+
return p[:w]
248+
}
249+
return string(buf[:w])
250+
}
251+
252+
func BenchmarkPathCleanOld(b *testing.B) {
253+
b.ReportAllocs()
254+
255+
for i := 0; i < b.N; i++ {
256+
for _, test := range cleanTests {
257+
cleanPathOld(test.path)
258+
}
259+
}
260+
}

0 commit comments

Comments
 (0)