]> Cypherpunks repositories - gostls13.git/commitdiff
net/url: remove an allocation for short strings in escape
authorIan Davis <nospam@iandavis.com>
Mon, 10 Sep 2018 11:13:09 +0000 (12:13 +0100)
committerBrad Fitzpatrick <bradfitz@golang.org>
Tue, 11 Sep 2018 12:25:22 +0000 (12:25 +0000)
Use a 64 byte array to avoid an allocation on the assumption that
most url escaping is performed on short strings. Also adds a fast
path for escaping strings whose only replacements are spaces which
is common in query components.

Adds benchmarks for QueryEscape, PathEscape, QueryUnescape and
PathUnescape but no optimizations are include for the unescape functions
so I don't include those benchmark results here.

Reduces allocations by 10% in the existing String benchmark with a
modest performance increase.

name               old time/op    new time/op    delta
QueryEscape/#00-8    64.6ns ± 1%    43.8ns ± 0%  -32.14%  (p=0.000 n=9+9)
QueryEscape/#01-8     276ns ± 3%     249ns ± 0%   -9.62%  (p=0.000 n=10+7)
QueryEscape/#02-8     176ns ± 2%     155ns ± 3%  -12.21%  (p=0.000 n=10+10)
QueryEscape/#03-8     388ns ± 1%     362ns ± 0%   -6.55%  (p=0.000 n=10+8)
QueryEscape/#04-8    2.32µs ± 2%    2.27µs ± 2%   -2.26%  (p=0.001 n=10+10)
PathEscape/#00-8     78.0ns ± 3%    63.4ns ± 1%  -18.69%  (p=0.000 n=10+10)
PathEscape/#01-8      276ns ± 2%     260ns ± 0%   -6.01%  (p=0.000 n=10+10)
PathEscape/#02-8      175ns ± 0%     153ns ± 0%  -12.53%  (p=0.000 n=8+10)
PathEscape/#03-8      389ns ± 2%     361ns ± 0%   -7.21%  (p=0.000 n=10+9)
PathEscape/#04-8     2.30µs ± 2%    2.27µs ± 1%   -1.33%  (p=0.001 n=9+10)
String-8             3.56µs ± 4%    3.42µs ± 7%   -4.00%  (p=0.003 n=10+10)

name               old alloc/op   new alloc/op   delta
QueryEscape/#00-8     16.0B ± 0%      8.0B ± 0%  -50.00%  (p=0.000 n=10+10)
QueryEscape/#01-8      128B ± 0%       64B ± 0%  -50.00%  (p=0.000 n=10+10)
QueryEscape/#02-8     64.0B ± 0%     32.0B ± 0%  -50.00%  (p=0.000 n=10+10)
QueryEscape/#03-8      128B ± 0%       64B ± 0%  -50.00%  (p=0.000 n=10+10)
QueryEscape/#04-8      832B ± 0%      832B ± 0%     ~     (all equal)
PathEscape/#00-8      32.0B ± 0%     16.0B ± 0%  -50.00%  (p=0.000 n=10+10)
PathEscape/#01-8       128B ± 0%       64B ± 0%  -50.00%  (p=0.000 n=10+10)
PathEscape/#02-8      64.0B ± 0%     32.0B ± 0%  -50.00%  (p=0.000 n=10+10)
PathEscape/#03-8       128B ± 0%       64B ± 0%  -50.00%  (p=0.000 n=10+10)
PathEscape/#04-8       704B ± 0%      704B ± 0%     ~     (all equal)
String-8             1.84kB ± 0%    1.66kB ± 0%   -9.57%  (p=0.000 n=10+10)

name               old allocs/op  new allocs/op  delta
QueryEscape/#00-8      2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)
QueryEscape/#01-8      2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)
QueryEscape/#02-8      2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)
QueryEscape/#03-8      2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)
QueryEscape/#04-8      2.00 ± 0%      2.00 ± 0%     ~     (all equal)
PathEscape/#00-8       2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)
PathEscape/#01-8       2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)
PathEscape/#02-8       2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)
PathEscape/#03-8       2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)
PathEscape/#04-8       2.00 ± 0%      2.00 ± 0%     ~     (all equal)
String-8               69.0 ± 0%      61.0 ± 0%  -11.59%  (p=0.000 n=10+10)

Updates #17860

Change-Id: I45c5e9d40b242f874c61f6ccc73bf94c494bb868
Reviewed-on: https://go-review.googlesource.com/134296
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
src/net/url/url.go
src/net/url/url_test.go

index 4943ea6d67ceb54b0147c169551f4730efa4d6ef..b678b82352e050de2b946150d0fb06f6e8ae96e5 100644 (file)
@@ -304,7 +304,26 @@ func escape(s string, mode encoding) string {
                return s
        }
 
-       t := make([]byte, len(s)+2*hexCount)
+       var buf [64]byte
+       var t []byte
+
+       required := len(s) + 2*hexCount
+       if required <= len(buf) {
+               t = buf[:required]
+       } else {
+               t = make([]byte, required)
+       }
+
+       if hexCount == 0 {
+               copy(t, s)
+               for i := 0; i < len(s); i++ {
+                       if s[i] == ' ' {
+                               t[i] = '+'
+                       }
+               }
+               return string(t)
+       }
+
        j := 0
        for i := 0; i < len(s); i++ {
                switch c := s[i]; {
index 19d4d636d62217da1751f06ea5e249ea0bbdc72f..231340a9eb89d3152c22ff132df7e6a410a8e712 100644 (file)
@@ -1754,3 +1754,106 @@ func TestInvalidUserPassword(t *testing.T) {
                t.Errorf("error = %q; want substring %q", got, wantsub)
        }
 }
+
+var escapeBenchmarks = []struct {
+       unescaped string
+       query     string
+       path      string
+}{
+       {
+               unescaped: "one two",
+               query:     "one+two",
+               path:      "one%20two",
+       },
+       {
+               unescaped: "Фотки собак",
+               query:     "%D0%A4%D0%BE%D1%82%D0%BA%D0%B8+%D1%81%D0%BE%D0%B1%D0%B0%D0%BA",
+               path:      "%D0%A4%D0%BE%D1%82%D0%BA%D0%B8%20%D1%81%D0%BE%D0%B1%D0%B0%D0%BA",
+       },
+
+       {
+               unescaped: "shortrun(break)shortrun",
+               query:     "shortrun%28break%29shortrun",
+               path:      "shortrun%28break%29shortrun",
+       },
+
+       {
+               unescaped: "longerrunofcharacters(break)anotherlongerrunofcharacters",
+               query:     "longerrunofcharacters%28break%29anotherlongerrunofcharacters",
+               path:      "longerrunofcharacters%28break%29anotherlongerrunofcharacters",
+       },
+
+       {
+               unescaped: strings.Repeat("padded/with+various%characters?that=need$some@escaping+paddedsowebreak/256bytes", 4),
+               query:     strings.Repeat("padded%2Fwith%2Bvarious%25characters%3Fthat%3Dneed%24some%40escaping%2Bpaddedsowebreak%2F256bytes", 4),
+               path:      strings.Repeat("padded%2Fwith+various%25characters%3Fthat=need$some@escaping+paddedsowebreak%2F256bytes", 4),
+       },
+}
+
+func BenchmarkQueryEscape(b *testing.B) {
+       for _, tc := range escapeBenchmarks {
+               b.Run("", func(b *testing.B) {
+                       b.ReportAllocs()
+                       var g string
+                       for i := 0; i < b.N; i++ {
+                               g = QueryEscape(tc.unescaped)
+                       }
+                       b.StopTimer()
+                       if g != tc.query {
+                               b.Errorf("QueryEscape(%q) == %q, want %q", tc.unescaped, g, tc.query)
+                       }
+
+               })
+       }
+}
+
+func BenchmarkPathEscape(b *testing.B) {
+       for _, tc := range escapeBenchmarks {
+               b.Run("", func(b *testing.B) {
+                       b.ReportAllocs()
+                       var g string
+                       for i := 0; i < b.N; i++ {
+                               g = PathEscape(tc.unescaped)
+                       }
+                       b.StopTimer()
+                       if g != tc.path {
+                               b.Errorf("PathEscape(%q) == %q, want %q", tc.unescaped, g, tc.path)
+                       }
+
+               })
+       }
+}
+
+func BenchmarkQueryUnescape(b *testing.B) {
+       for _, tc := range escapeBenchmarks {
+               b.Run("", func(b *testing.B) {
+                       b.ReportAllocs()
+                       var g string
+                       for i := 0; i < b.N; i++ {
+                               g, _ = QueryUnescape(tc.query)
+                       }
+                       b.StopTimer()
+                       if g != tc.unescaped {
+                               b.Errorf("QueryUnescape(%q) == %q, want %q", tc.query, g, tc.unescaped)
+                       }
+
+               })
+       }
+}
+
+func BenchmarkPathUnescape(b *testing.B) {
+       for _, tc := range escapeBenchmarks {
+               b.Run("", func(b *testing.B) {
+                       b.ReportAllocs()
+                       var g string
+                       for i := 0; i < b.N; i++ {
+                               g, _ = PathUnescape(tc.path)
+                       }
+                       b.StopTimer()
+                       if g != tc.unescaped {
+                               b.Errorf("PathUnescape(%q) == %q, want %q", tc.path, g, tc.unescaped)
+                       }
+
+               })
+       }
+}