mirror of
https://github.com/golang/go
synced 2024-11-18 15:24:41 -07:00
net/http: mapping data structure
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>
This commit is contained in:
parent
3602465954
commit
9f5a2cf61c
78
src/net/http/mapping.go
Normal file
78
src/net/http/mapping.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
154
src/net/http/mapping_test.go
Normal file
154
src/net/http/mapping_test.go
Normal file
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user