Skip to content

Commit

Permalink
Fix Clear not zeroing items when buffer full (#26)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
gammazero authored Oct 19, 2022
1 parent e9ddfbe commit ac07eb9
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 37 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
61 changes: 34 additions & 27 deletions deque.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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] {
Expand Down Expand Up @@ -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()

Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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.
Expand All @@ -193,21 +196,21 @@ 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
q.count = 0
}

// 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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
12 changes: 12 additions & 0 deletions deque_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 5 additions & 6 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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

0 comments on commit ac07eb9

Please sign in to comment.