From 016af753cf0f5a32ae2af9147a5d2e75418e499e Mon Sep 17 00:00:00 2001 From: wanghaolong613 Date: Thu, 16 Oct 2025 15:13:24 +0800 Subject: [PATCH] optimization cleanPath --- path.go | 78 ++++++++++++++++++++-------------- path_test.go | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 30 deletions(-) diff --git a/path.go b/path.go index 82438c1378..6ed82f53ae 100644 --- a/path.go +++ b/path.go @@ -19,18 +19,29 @@ package gin // // If the result of this process is an empty string, "/" is returned. func cleanPath(p string) string { - const stackBufSize = 128 // Turn empty string into "/" if p == "" { return "/" } + n := len(p) + + // If the path length is 1, handle special cases separately: + // - If it is "/" or ".", return "/". + // - Otherwise, prepend "/" to the path and return it. + if n == 1 { + if p[0] == '/' || p[0] == '.' { + return "/" + } + return "/" + p + } + + const stackBufSize = 128 + // Reasonably sized buffer on stack to avoid allocations in the common case. // If a larger buffer is required, it gets allocated dynamically. buf := make([]byte, 0, stackBufSize) - n := len(p) - // Invariants: // reading from path; r is index of next byte to process. // writing to buf; w is index of next byte to write. @@ -50,7 +61,7 @@ func cleanPath(p string) string { buf[0] = '/' } - trailing := n > 1 && p[n-1] == '/' + var trailing bool // A bit more clunky without a 'lazybuf' like the path package, but the loop // gets completely inlined (bufApp calls). @@ -58,38 +69,44 @@ func cleanPath(p string) string { // calls (except make, if needed). for r < n { - switch { - case p[r] == '/': + switch p[r] { + case '/': // empty path element, trailing slash is added after the end r++ - case p[r] == '.' && r+1 == n: - trailing = true - r++ - - case p[r] == '.' && p[r+1] == '/': - // . element - r += 2 - - case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): - // .. element: remove to last / - r += 3 - - if w > 1 { - // can backtrack - w-- - - if len(buf) == 0 { - for w > 1 && p[w] != '/' { - w-- - } - } else { - for w > 1 && buf[w] != '/' { + case '.': + if r+1 == n { + trailing = true + r++ + // Reduce one comparison between r and n + goto endOfLoop + } + switch p[r+1] { + case '/': + // . element + r += 2 + + case '.': + if r+2 == n || p[r+2] == '/' { + // .. element: remove to last / + r += 3 + + if w > 1 { + // can backtrack w-- + + if len(buf) == 0 { + for w > 1 && p[w] != '/' { + w-- + } + } else { + for w > 1 && buf[w] != '/' { + w-- + } + } } } } - default: // Real path element. // Add slash if needed @@ -107,8 +124,9 @@ func cleanPath(p string) string { } } +endOfLoop: // Re-append trailing slash - if trailing && w > 1 { + if (trailing || p[n-1] == '/') && w > 1 { bufApp(&buf, p, w, '/') w++ } diff --git a/path_test.go b/path_test.go index 2269b78ee1..c8547ddfef 100644 --- a/path_test.go +++ b/path_test.go @@ -27,6 +27,7 @@ var cleanTests = []cleanPathTest{ // missing root {"", "/"}, + {"a", "/a"}, {"a/", "/a/"}, {"abc", "/abc"}, {"abc/def", "/abc/def"}, @@ -143,3 +144,117 @@ func BenchmarkPathCleanLong(b *testing.B) { } } } + +func cleanPathOld(p string) string { + const stackBufSize = 128 + // Turn empty string into "/" + if p == "" { + return "/" + } + + // Reasonably sized buffer on stack to avoid allocations in the common case. + // If a larger buffer is required, it gets allocated dynamically. + buf := make([]byte, 0, stackBufSize) + + n := len(p) + + // Invariants: + // reading from path; r is index of next byte to process. + // writing to buf; w is index of next byte to write. + + // path must start with '/' + r := 1 + w := 1 + + if p[0] != '/' { + r = 0 + + if n+1 > stackBufSize { + buf = make([]byte, n+1) + } else { + buf = buf[:n+1] + } + buf[0] = '/' + } + + trailing := n > 1 && p[n-1] == '/' + + // A bit more clunky without a 'lazybuf' like the path package, but the loop + // gets completely inlined (bufApp calls). + // loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function + // calls (except make, if needed). + + for r < n { + switch { + case p[r] == '/': + // empty path element, trailing slash is added after the end + r++ + + case p[r] == '.' && r+1 == n: + trailing = true + r++ + + case p[r] == '.' && p[r+1] == '/': + // . element + r += 2 + + case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): + // .. element: remove to last / + r += 3 + + if w > 1 { + // can backtrack + w-- + + if len(buf) == 0 { + for w > 1 && p[w] != '/' { + w-- + } + } else { + for w > 1 && buf[w] != '/' { + w-- + } + } + } + + default: + // Real path element. + // Add slash if needed + if w > 1 { + bufApp(&buf, p, w, '/') + w++ + } + + // Copy element + for r < n && p[r] != '/' { + bufApp(&buf, p, w, p[r]) + w++ + r++ + } + } + } + + // Re-append trailing slash + if trailing && w > 1 { + bufApp(&buf, p, w, '/') + w++ + } + + // If the original string was not modified (or only shortened at the end), + // return the respective substring of the original string. + // Otherwise return a new string from the buffer. + if len(buf) == 0 { + return p[:w] + } + return string(buf[:w]) +} + +func BenchmarkPathCleanOld(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + for _, test := range cleanTests { + cleanPathOld(test.path) + } + } +}