// Copyright 2024 The Cockroach Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or // implied. See the License for the specific language governing // permissions and limitations under the License. package fifo import "sync" // Queue implements an allocation efficient FIFO queue. It is not safe for // concurrent access. // // Note that the queue provides pointer access to the internal storage (via // PeekFront and PushBack) so it must be used with care. These pointers must not // be used once the respective element is popped out of the queue. // // -- Implementation -- // // The queue is implemented as a linked list of nodes, where each node is a // small ring buffer. The nodes are allocated using a sync.Pool (a single pool // is created for any given type and is used for all queues of that type). type Queue[T any] struct { len int head, tail *queueNode[T] pool *QueueBackingPool[T] } // MakeQueue constructs a new Queue. // // The pool should be a singleton object initialized with MakeQueueBackingPool. // A single pool can and should be used by all queues of that type. func MakeQueue[T any](pool *QueueBackingPool[T]) Queue[T] { return Queue[T]{ pool: pool, } } // Len returns the current length of the queue. func (q *Queue[T]) Len() int { return q.len } // PushBack adds t to the end of the queue. // // The returned pointer can be used to modify the element while it is in the // queue; it is valid until the element is removed from the queue. func (q *Queue[T]) PushBack(t T) *T { if q.head == nil { q.head = q.pool.get() q.tail = q.head } else if q.tail.IsFull() { newTail := q.pool.get() q.tail.next = newTail q.tail = newTail } q.len++ return q.tail.PushBack(t) } // PeekFront returns the current head of the queue, or nil if the queue is // empty. // // The result is only valid until the next call to PopFront. func (q *Queue[T]) PeekFront() *T { if q.len == 0 { return nil } return q.head.PeekFront() } // PopFront removes the current head of the queue. // // It is illegal to call PopFront on an empty queue. func (q *Queue[T]) PopFront() { q.head.PopFront() // If this is the only node, we don't want to release it; otherwise we would // allocate/free a node every time we transition between the queue being empty // and non-empty. if q.head.len == 0 && q.head.next != nil { oldHead := q.head q.head = oldHead.next q.pool.put(oldHead) } q.len-- } // QueueBackingPool is a sync.Pool that used to allocate internal nodes // for Queue[T]. type QueueBackingPool[T any] struct { pool sync.Pool } // MakeQueueBackingPool makes a queue backing pool. It is intented to be used to // initialize a singleton (global) variable. A single pool can and should be // used by all queues of that type. func MakeQueueBackingPool[T any]() QueueBackingPool[T] { return QueueBackingPool[T]{ pool: sync.Pool{ New: func() interface{} { return &queueNode[T]{} }, }, } } func (qp *QueueBackingPool[T]) get() *queueNode[T] { return qp.pool.Get().(*queueNode[T]) } func (qp *QueueBackingPool[T]) put(n *queueNode[T]) { *n = queueNode[T]{} qp.pool.Put(n) } // We batch the allocation of this many queue objects. The value was chosen // without experimentation - it provides a reasonable amount of amortization // without a very large increase in memory overhead if T is large. const queueNodeSize = 8 type queueNode[T any] struct { buf [queueNodeSize]T head, len int32 next *queueNode[T] } func (qn *queueNode[T]) IsFull() bool { return qn.len == queueNodeSize } func (qn *queueNode[T]) PushBack(t T) *T { if invariants && qn.len >= queueNodeSize { panic("cannot push back into a full node") } i := (qn.head + qn.len) % queueNodeSize qn.buf[i] = t qn.len++ return &qn.buf[i] } func (qn *queueNode[T]) PeekFront() *T { return &qn.buf[qn.head] } func (qn *queueNode[T]) PopFront() T { if invariants && qn.len == 0 { panic("cannot pop from empty queue") } t := qn.buf[qn.head] var zero T qn.buf[qn.head] = zero qn.head = (qn.head + 1) % queueNodeSize qn.len-- return t }