use a sync.Pool to reduce allocation of linked list elements

Especially the sentPacketHistory linked list shows up in allocation
profiles, since a new list element is allocated for every single packet
we send.
Using a pool for the receiving path, as well as for the frame sorter, is
less critical, since we're tracking ranges there instead of individual
packets / frames, but it doesn't hurt either.
The other occurrences where we use a linked list (connection ID tracking
and the token store) are used so rarely (a few times over the lifetime
of the connection) that using a pool doesn't make any sense there.
This commit is contained in:
Marten Seemann
2022-12-30 18:02:19 +13:00
parent dd30a02627
commit d9665c632e
5 changed files with 56 additions and 7 deletions

View File

@@ -1,4 +1,6 @@
# Usage
This is the Go standard library implementation of a linked list
(https://golang.org/src/container/list/list.go), modified to use Go generics.
(https://golang.org/src/container/list/list.go), with the following modifications:
* it uses Go generics
* it allows passing in a `sync.Pool` (via the `NewWithPool` constructor) to reduce allocations of `Element` structs

View File

@@ -11,6 +11,12 @@
// }
package list
import "sync"
func NewPool[T any]() *sync.Pool {
return &sync.Pool{New: func() any { return &Element[T]{} }}
}
// Element is an element of a linked list.
type Element[T any] struct {
// Next and previous pointers in the doubly-linked list of elements.
@@ -52,6 +58,8 @@ func (e *Element[T]) List() *List[T] {
type List[T any] struct {
root Element[T] // sentinel list element, only &root, root.prev, and root.next are used
len int // current list length excluding (this) sentinel element
pool *sync.Pool
}
// Init initializes or clears list l.
@@ -65,6 +73,12 @@ func (l *List[T]) Init() *List[T] {
// New returns an initialized list.
func New[T any]() *List[T] { return new(List[T]).Init() }
// NewWithPool returns an initialized list, using a sync.Pool for list elements.
func NewWithPool[T any](pool *sync.Pool) *List[T] {
l := &List[T]{pool: pool}
return l.Init()
}
// Len returns the number of elements of list l.
// The complexity is O(1).
func (l *List[T]) Len() int { return l.len }
@@ -105,7 +119,14 @@ func (l *List[T]) insert(e, at *Element[T]) *Element[T] {
// insertValue is a convenience wrapper for insert(&Element{Value: v}, at).
func (l *List[T]) insertValue(v T, at *Element[T]) *Element[T] {
return l.insert(&Element[T]{Value: v}, at)
var e *Element[T]
if l.pool != nil {
e = l.pool.Get().(*Element[T])
} else {
e = &Element[T]{}
}
e.Value = v
return l.insert(e, at)
}
// remove removes e from its list, decrements l.len
@@ -115,6 +136,9 @@ func (l *List[T]) remove(e *Element[T]) {
e.next = nil // avoid memory leaks
e.prev = nil // avoid memory leaks
e.list = nil
if l.pool != nil {
l.pool.Put(e)
}
l.len--
}
@@ -136,12 +160,13 @@ func (l *List[T]) move(e, at *Element[T]) {
// It returns the element value e.Value.
// The element must not be nil.
func (l *List[T]) Remove(e *Element[T]) T {
v := e.Value
if e.list == l {
// if e.list == l, l must have been initialized when e was inserted
// in l or l == nil (e is a zero Element) and l.remove will crash
l.remove(e)
}
return e.Value
return v
}
// PushFront inserts a new element e with value v at the front of list l and returns e.