1
0
mirror of https://github.com/golang/go synced 2024-11-18 13:44:48 -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:
Jonathan Amsterdam 2023-09-11 10:59:48 -04:00
parent 3602465954
commit 9f5a2cf61c
2 changed files with 232 additions and 0 deletions

78
src/net/http/mapping.go Normal file
View 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
}
}
}
}

View 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
}