]> Cypherpunks repositories - gostls13.git/commitdiff
net/http: mapping data structure
authorJonathan Amsterdam <jba@google.com>
Mon, 11 Sep 2023 14:59:48 +0000 (10:59 -0400)
committerJonathan Amsterdam <jba@google.com>
Tue, 12 Sep 2023 17:47:07 +0000 (17:47 +0000)
Our goal for the new ServeMux patterns is to match the routing
performance of the existing ServeMux patterns. To achieve that
we needed to optimize lookup for small maps.

This CL introduces a simple data structure called a mapping that
optimizes lookup by using a slice for small collections of key-value
pairs, switching to a map when the collection gets large.

Mappings are a core part of the routing algorithm, which uses a
decision tree to match path elements.   The children of a tree node are
held in a mapping.

Change-Id: I923b3ad1376ace2c3e3421aa9b802ad12d47c871
Reviewed-on: https://go-review.googlesource.com/c/go/+/526617
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Eli Bendersky <eliben@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
src/net/http/mapping.go [new file with mode: 0644]
src/net/http/mapping_test.go [new file with mode: 0644]

diff --git a/src/net/http/mapping.go b/src/net/http/mapping.go
new file mode 100644 (file)
index 0000000..87e6d5a
--- /dev/null
@@ -0,0 +1,78 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package http
+
+// A mapping is a collection of key-value pairs where the keys are unique.
+// A zero mapping is empty and ready to use.
+// A mapping tries to pick a representation that makes [mapping.find] most efficient.
+type mapping[K comparable, V any] struct {
+       s []entry[K, V] // for few pairs
+       m map[K]V       // for many pairs
+}
+
+type entry[K comparable, V any] struct {
+       key   K
+       value V
+}
+
+// maxSlice is the maximum number of pairs for which a slice is used.
+// It is a variable for benchmarking.
+var maxSlice int = 8
+
+// add adds a key-value pair to the mapping.
+func (h *mapping[K, V]) add(k K, v V) {
+       if h.m == nil && len(h.s) < maxSlice {
+               h.s = append(h.s, entry[K, V]{k, v})
+       } else {
+               if h.m == nil {
+                       h.m = map[K]V{}
+                       for _, e := range h.s {
+                               h.m[e.key] = e.value
+                       }
+                       h.s = nil
+               }
+               h.m[k] = v
+       }
+}
+
+// find returns the value corresponding to the given key.
+// The second return value is false if there is no value
+// with that key.
+func (h *mapping[K, V]) find(k K) (v V, found bool) {
+       if h == nil {
+               return v, false
+       }
+       if h.m != nil {
+               v, found = h.m[k]
+               return v, found
+       }
+       for _, e := range h.s {
+               if e.key == k {
+                       return e.value, true
+               }
+       }
+       return v, false
+}
+
+// eachPair calls f for each pair in the mapping.
+// If f returns false, pairs returns immediately.
+func (h *mapping[K, V]) eachPair(f func(k K, v V) bool) {
+       if h == nil {
+               return
+       }
+       if h.m != nil {
+               for k, v := range h.m {
+                       if !f(k, v) {
+                               return
+                       }
+               }
+       } else {
+               for _, e := range h.s {
+                       if !f(e.key, e.value) {
+                               return
+                       }
+               }
+       }
+}
diff --git a/src/net/http/mapping_test.go b/src/net/http/mapping_test.go
new file mode 100644 (file)
index 0000000..0aed9d9
--- /dev/null
@@ -0,0 +1,154 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package http
+
+import (
+       "cmp"
+       "fmt"
+       "slices"
+       "strconv"
+       "testing"
+)
+
+func TestMapping(t *testing.T) {
+       var m mapping[int, string]
+       for i := 0; i < maxSlice; i++ {
+               m.add(i, strconv.Itoa(i))
+       }
+       if m.m != nil {
+               t.Fatal("m.m != nil")
+       }
+       for i := 0; i < maxSlice; i++ {
+               g, _ := m.find(i)
+               w := strconv.Itoa(i)
+               if g != w {
+                       t.Fatalf("%d: got %s, want %s", i, g, w)
+               }
+       }
+       m.add(4, "4")
+       if m.s != nil {
+               t.Fatal("m.s != nil")
+       }
+       if m.m == nil {
+               t.Fatal("m.m == nil")
+       }
+       g, _ := m.find(4)
+       if w := "4"; g != w {
+               t.Fatalf("got %s, want %s", g, w)
+       }
+}
+
+func TestMappingEachPair(t *testing.T) {
+       var m mapping[int, string]
+       var want []entry[int, string]
+       for i := 0; i < maxSlice*2; i++ {
+               v := strconv.Itoa(i)
+               m.add(i, v)
+               want = append(want, entry[int, string]{i, v})
+
+       }
+
+       var got []entry[int, string]
+       m.eachPair(func(k int, v string) bool {
+               got = append(got, entry[int, string]{k, v})
+               return true
+       })
+       slices.SortFunc(got, func(e1, e2 entry[int, string]) int {
+               return cmp.Compare(e1.key, e2.key)
+       })
+       if !slices.Equal(got, want) {
+               t.Errorf("got %v, want %v", got, want)
+       }
+}
+
+func BenchmarkFindChild(b *testing.B) {
+       key := "articles"
+       children := []string{
+               "*",
+               "cmd.html",
+               "code.html",
+               "contrib.html",
+               "contribute.html",
+               "debugging_with_gdb.html",
+               "docs.html",
+               "effective_go.html",
+               "files.log",
+               "gccgo_contribute.html",
+               "gccgo_install.html",
+               "go-logo-black.png",
+               "go-logo-blue.png",
+               "go-logo-white.png",
+               "go1.1.html",
+               "go1.2.html",
+               "go1.html",
+               "go1compat.html",
+               "go_faq.html",
+               "go_mem.html",
+               "go_spec.html",
+               "help.html",
+               "ie.css",
+               "install-source.html",
+               "install.html",
+               "logo-153x55.png",
+               "Makefile",
+               "root.html",
+               "share.png",
+               "sieve.gif",
+               "tos.html",
+               "articles",
+       }
+       if len(children) != 32 {
+               panic("bad len")
+       }
+       for _, n := range []int{2, 4, 8, 16, 32} {
+               list := children[:n]
+               b.Run(fmt.Sprintf("n=%d", n), func(b *testing.B) {
+
+                       b.Run("rep=linear", func(b *testing.B) {
+                               var entries []entry[string, any]
+                               for _, c := range list {
+                                       entries = append(entries, entry[string, any]{c, nil})
+                               }
+                               b.ResetTimer()
+                               for i := 0; i < b.N; i++ {
+                                       findChildLinear(key, entries)
+                               }
+                       })
+                       b.Run("rep=map", func(b *testing.B) {
+                               m := map[string]any{}
+                               for _, c := range list {
+                                       m[c] = nil
+                               }
+                               var x any
+                               b.ResetTimer()
+                               for i := 0; i < b.N; i++ {
+                                       x = m[key]
+                               }
+                               _ = x
+                       })
+                       b.Run(fmt.Sprintf("rep=hybrid%d", maxSlice), func(b *testing.B) {
+                               var h mapping[string, any]
+                               for _, c := range list {
+                                       h.add(c, nil)
+                               }
+                               var x any
+                               b.ResetTimer()
+                               for i := 0; i < b.N; i++ {
+                                       x, _ = h.find(key)
+                               }
+                               _ = x
+                       })
+               })
+       }
+}
+
+func findChildLinear(key string, entries []entry[string, any]) any {
+       for _, e := range entries {
+               if key == e.key {
+                       return e.value
+               }
+       }
+       return nil
+}