From ac07eb98a26f5e071e960bc1690387fd8523b2fa Mon Sep 17 00:00:00 2001 From: Andrew Gillis Date: Wed, 19 Oct 2022 08:32:21 -0700 Subject: [PATCH] Fix Clear not zeroing items when buffer full (#26) * Fix Clear not zeroing items when buffer full When the deque buffer is full Clear was not zeroing the items in it. This fixes that problem. Additional changes: - New out-of-range panic messages - Minor document spelling and formatting fixes --- README.md | 8 +++---- deque.go | 61 ++++++++++++++++++++++++++++----------------------- deque_test.go | 12 ++++++++++ doc.go | 11 +++++----- 4 files changed, 55 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 6023318..eb06369 100644 --- a/README.md +++ b/README.md @@ -18,19 +18,19 @@ $ go get github.com/gammazero/deque ## Deque data structure -Deque generalizes a queue and a stack, to efficiently add and remove items at either end with O(1) performance. [Queue](https://en.wikipedia.org/wiki/Queue_(abstract_data_type)) (FIFO) operations are supported using `PushBack()` and `PopFront()`. [Stack](https://en.wikipedia.org/wiki/Stack_(abstract_data_type)) (LIFO) operations are supported using `PushBack()` and `PopBack()`. +Deque generalizes a queue and a stack, to efficiently add and remove items at either end with O(1) performance. [Queue](https://en.wikipedia.org/wiki/Queue_(abstract_data_type)) (FIFO) operations are supported using `PushBack` and `PopFront`. [Stack](https://en.wikipedia.org/wiki/Stack_(abstract_data_type)) (LIFO) operations are supported using `PushBack` and `PopBack`. ## Ring-buffer Performance -This deque implementation is optimized for CPU and GC performance. The circular buffer automatically re-sizes by powers of two, growing when additional capacity is needed and shrinking when only a quarter of the capacity is used, and uses bitwise arithmetic for all calculations. Since growth is by powers of two, adding elements will only cause O(log n) allocations. A minimum capacity can be set so that there is no resizing at or below that specified amount. +This deque implementation is optimized for CPU and GC performance. The circular buffer automatically re-sizes by powers of two, growing when additional capacity is needed and shrinking when only a quarter of the capacity is used, and uses bitwise arithmetic for all calculations. Since growth is by powers of two, adding elements will only cause O(log n) allocations. A minimum capacity can be set so that there is no resizing at or below that specified amount. -The ring-buffer implementation improves memory and time performance with fewer GC pauses, compared to implementations based on slices and linked lists. By wrapping around the buffer, previously used space is reused, making allocation unnecessary until all buffer capacity is used. If the deque is only filled and then completely emptied before being filled again, then the ring structure offers little benefit for memory reuse over reusing a slice. +The ring-buffer implementation improves memory and time performance with fewer GC pauses, compared to implementations based on slices and linked lists. By wrapping around the buffer, previously used space is reused, making allocation unnecessary until all buffer capacity is used. If the deque is only filled and then completely emptied before being filled again, then the ring structure offers little benefit for memory reuse over a slice. For maximum speed, this deque implementation leaves concurrency safety up to the application to provide, however the application chooses, if needed at all. ## Reading Empty Deque -Since it is OK for the deque to contain a `nil` value, it is necessary to either panic or return a second boolean value to indicate the deque is empty, when reading or removing an element. This deque panics when reading from an empty deque. This is a run-time check to help catch programming errors, which may be missed if a second return value is ignored. Simply check `Deque.Len()` before reading from the deque. +Since it is OK for the deque to contain a `nil` value, it is necessary to either panic or return a second boolean value to indicate the deque is empty, when reading or removing an element. This deque panics when reading from an empty deque. This is a run-time check to help catch programming errors, which may be missed if a second return value is ignored. Simply check `Deque.Len()` before reading from the deque. ## Generics diff --git a/deque.go b/deque.go index a120ccf..a1743c1 100644 --- a/deque.go +++ b/deque.go @@ -1,11 +1,13 @@ package deque +import "fmt" + // minCapacity is the smallest capacity that deque may have. Must be power of 2 // for bitwise modulus: x % n == x & (n - 1). const minCapacity = 16 // Deque represents a single instance of the deque data structure. A Deque -// instance contains items of the type sepcified by the type argument. +// instance contains items of the type specified by the type argument. type Deque[T any] struct { buf []T head int @@ -19,15 +21,17 @@ type Deque[T any] struct { // operates on items of the type specified by the type argument. For example, // to create a Deque that contains strings, // -// stringDeque := deque.New[string]() +// stringDeque := deque.New[string]() // // To create a Deque with capacity to store 2048 ints without resizing, and // that will not resize below space for 32 items when removing items: -// d := deque.New[int](2048, 32) +// +// d := deque.New[int](2048, 32) // // To create a Deque that has not yet allocated memory, but after it does will // never resize to have space for less than 64 items: -// d := deque.New[int](0, 64) +// +// d := deque.New[int](0, 64) // // Any size values supplied here are rounded up to the nearest power of 2. func New[T any](size ...int) *Deque[T] { @@ -77,8 +81,8 @@ func (q *Deque[T]) Len() int { } // PushBack appends an element to the back of the queue. Implements FIFO when -// elements are removed with PopFront(), and LIFO when elements are removed -// with PopBack(). +// elements are removed with PopFront, and LIFO when elements are removed with +// PopBack. func (q *Deque[T]) PushBack(elem T) { q.growIfFull() @@ -99,7 +103,7 @@ func (q *Deque[T]) PushFront(elem T) { } // PopFront removes and returns the element from the front of the queue. -// Implements FIFO when used with PushBack(). If the queue is empty, the call +// Implements FIFO when used with PushBack. If the queue is empty, the call // panics. func (q *Deque[T]) PopFront() T { if q.count <= 0 { @@ -117,7 +121,7 @@ func (q *Deque[T]) PopFront() T { } // PopBack removes and returns the element from the back of the queue. -// Implements LIFO when used with PushBack(). If the queue is empty, the call +// Implements LIFO when used with PushBack. If the queue is empty, the call // panics. func (q *Deque[T]) PopBack() T { if q.count <= 0 { @@ -138,8 +142,7 @@ func (q *Deque[T]) PopBack() T { } // Front returns the element at the front of the queue. This is the element -// that would be returned by PopFront(). This call panics if the queue is -// empty. +// that would be returned by PopFront. This call panics if the queue is empty. func (q *Deque[T]) Front() T { if q.count <= 0 { panic("deque: Front() called when empty") @@ -148,7 +151,7 @@ func (q *Deque[T]) Front() T { } // Back returns the element at the back of the queue. This is the element that -// would be returned by PopBack(). This call panics if the queue is empty. +// would be returned by PopBack. This call panics if the queue is empty. func (q *Deque[T]) Back() T { if q.count <= 0 { panic("deque: Back() called when empty") @@ -170,21 +173,21 @@ func (q *Deque[T]) Back() T { // in the buffer must be readable without altering the buffer contents. func (q *Deque[T]) At(i int) T { if i < 0 || i >= q.count { - panic("deque: At() called with index out of range") + panic(outOfRangeText(i, q.Len())) } // bitwise modulus return q.buf[(q.head+i)&(len(q.buf)-1)] } -// Set puts the element at index i in the queue. Set shares the same purpose -// than At() but perform the opposite operation. The index i is the same index -// defined by At(). If the index is invalid, the call panics. -func (q *Deque[T]) Set(i int, elem T) { +// Set assigns the item to index i in the queue. Set indexes the deque the same +// as At but perform the opposite operation. If the index is invalid, the call +// panics. +func (q *Deque[T]) Set(i int, item T) { if i < 0 || i >= q.count { - panic("deque: Set() called with index out of range") + panic(outOfRangeText(i, q.Len())) } // bitwise modulus - q.buf[(q.head+i)&(len(q.buf)-1)] = elem + q.buf[(q.head+i)&(len(q.buf)-1)] = item } // Clear removes all elements from the queue, but retains the current capacity. @@ -193,11 +196,11 @@ func (q *Deque[T]) Set(i int, elem T) { // only added. Only when items are removed is the queue subject to getting // resized smaller. func (q *Deque[T]) Clear() { - // bitwise modulus - modBits := len(q.buf) - 1 var zero T - for h := q.head; h != q.tail; h = (h + 1) & modBits { - q.buf[h] = zero + modBits := len(q.buf) - 1 + h := q.head + for i := 0; i < q.Len(); i++ { + q.buf[(h+i)&modBits] = zero } q.head = 0 q.tail = 0 @@ -205,9 +208,9 @@ func (q *Deque[T]) Clear() { } // Rotate rotates the deque n steps front-to-back. If n is negative, rotates -// back-to-front. Having Deque provide Rotate() avoids resizing that could -// happen if implementing rotation using only Pop and Push methods. If q.Len() -// is one or less, or q is nil, then Rotate does nothing. +// back-to-front. Having Deque provide Rotate avoids resizing that could happen +// if implementing rotation using only Pop and Push methods. If q.Len() is one +// or less, or q is nil, then Rotate does nothing. func (q *Deque[T]) Rotate(n int) { if q.Len() <= 1 { return @@ -294,7 +297,7 @@ func (q *Deque[T]) RIndex(f func(T) bool) int { // either of the ends of the queue. func (q *Deque[T]) Insert(at int, item T) { if at < 0 || at > q.count { - panic("deque: Insert() called with index out of range") + panic(outOfRangeText(at, q.Len())) } if at*2 < q.count { q.PushFront(item) @@ -327,7 +330,7 @@ func (q *Deque[T]) Insert(at int, item T) { // either of the ends of the queue. func (q *Deque[T]) Remove(at int) T { if at < 0 || at >= q.Len() { - panic("deque: Remove() called with index out of range") + panic(outOfRangeText(at, q.Len())) } rm := (q.head + at) & (len(q.buf) - 1) @@ -411,3 +414,7 @@ func (q *Deque[T]) resize() { q.tail = q.count q.buf = newBuf } + +func outOfRangeText(i, len int) string { + return fmt.Sprintf("deque: index out of range %d with length %d", i, len) +} diff --git a/deque_test.go b/deque_test.go index 21ad8fb..771292f 100644 --- a/deque_test.go +++ b/deque_test.go @@ -401,6 +401,18 @@ func TestClear(t *testing.T) { break } } + + for i := 0; i < 128; i++ { + q.PushBack(i) + } + q.Clear() + // Check that there are no remaining references after Clear() + for i := 0; i < len(q.buf); i++ { + if q.buf[i] != 0 { + t.Error("queue has non-nil deleted elements after Clear()") + break + } + } } func TestIndex(t *testing.T) { diff --git a/doc.go b/doc.go index 3e6832b..6cfead9 100644 --- a/doc.go +++ b/doc.go @@ -4,10 +4,10 @@ implementation. Deque generalizes a queue and a stack, to efficiently add and remove items at either end with O(1) performance. Queue (FIFO) operations are supported using -PushBack() and PopFront(). Stack (LIFO) operations are supported using -PushBack() and PopBack(). +PushBack and PopFront. Stack (LIFO) operations are supported using PushBack and +PopBack. -Ring-buffer Performance +# Ring-buffer Performance The ring-buffer automatically resizes by powers of two, growing when additional capacity is needed and shrinking when only a quarter of the capacity is used, @@ -20,7 +20,7 @@ and linked lists. For maximum speed, this deque implementation leaves concurrency safety up to the application to provide, however the application chooses, if needed at all. -Reading Empty Deque +# Reading Empty Deque Since it is OK for the deque to contain the zero-value of an item, it is necessary to either panic or return a second boolean value to indicate the @@ -29,11 +29,10 @@ reading from an empty deque. This is a run-time check to help catch programming errors, which may be missed if a second return value is ignored. Simply check Deque.Len() before reading from the deque. -Generics +# Generics Deque uses generics to create a Deque that contains items of the type specified. To create a Deque that holds a specific type, provide a type argument to New or with the variable declaration. - */ package deque