Skip to content

Commit

Permalink
New takes one optional arg to set base capacity. (#30)
Browse files Browse the repository at this point in the history
* New takes one optional arg to set base capacity.
* Added Grow function to ensure enough space for n additional items.
* Replace SetMinCapacity with SetBaseCapacity
  • Loading branch information
gammazero authored Nov 14, 2024
1 parent 81826a9 commit 632e04b
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 60 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ Deque generalizes a queue and a stack, to efficiently add and remove items at ei

## 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 base capacity (see [`deque.New`])(https://pkg.go.dev/github.com/gammazero/deque#New) can be set so that there is no resizing at or below that specified amount. The Deque can also be grown to a size sufficient to store n items, to prevent resizing when adding a number of itmes.

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.
The ring-buffer implementation improves memory and time performance with fewer GC pauses, compared to implementations based on slices or linked lists. By wrapping around the buffer, previously used space is reused, making allocation unnecessary until all buffer capacity is used. The ring buffer implementation performs best when resizes are infrequest, as is the case when items moving in and out of the Deque are balanced or when the base capacity is large enough to rarely require a resize.

For maximum speed, this deque implementation leaves concurrency safety up to the application to provide, however the application chooses, if needed at all.

Expand Down
107 changes: 63 additions & 44 deletions deque.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,49 +16,42 @@ type Deque[T any] struct {
minCap int
}

// New creates a new Deque, optionally setting the current and minimum capacity
// when non-zero values are given for these. The Deque instance returns
// operates on items of the type specified by the type argument. For example,
// to create a Deque that contains strings,
// New creates a new Deque, optionally setting the base capacity when a
// non-zero value is given. The returned Deque instance operates on items of
// the type specified by the type argument. For example, to create a Deque that
// contains strings do one of the following:
//
// var stringDeque deque.Deque[string]
// stringDeque := deque.New[string]()
// stringDeque := new(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:
// To create a Deque that will never resize to have space for less than 64
// items, specify a base capacity when calling New:
//
// d := deque.New[int](2048, 32)
// d := deque.New[int](64)
//
// 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:
// To ensure the Deque can store 1000 items without needing to resize while
// items are added:
//
// d := deque.New[int](0, 64)
// d.Grow(1000)
//
// Any size values supplied here are rounded up to the nearest power of 2.
func New[T any](size ...int) *Deque[T] {
var capacity, minimum int
if len(size) >= 1 {
capacity = size[0]
if len(size) >= 2 {
minimum = size[1]
// Any values supplied here are rounded up to the nearest power of 2, since the
// Deque grows by powers of 2.
func New[T any](initVals ...int) *Deque[T] {
var baseCap int
if len(initVals) >= 1 {
if len(initVals) >= 2 {
panic("Deque.New: too many arguments")
}
baseCap = initVals[0]
}

minCap := minCapacity
for minCap < minimum {
for minCap < baseCap {
minCap <<= 1
}

var buf []T
if capacity != 0 {
bufSize := minCap
for bufSize < capacity {
bufSize <<= 1
}
buf = make([]T, bufSize)
}

return &Deque[T]{
buf: buf,
minCap: minCap,
}
}
Expand Down Expand Up @@ -203,6 +196,37 @@ func (q *Deque[T]) Clear() {
q.count = 0
}

// Grow grows the deque's capacity, if necessary, to guarantee space for
// another n items. After Grow(n), at least n items can be written to the
// buffer without another allocation. If n is negative, Grow will panic.
func (q *Deque[T]) Grow(n int) {
if n < 0 {
panic("deque.Grow: negative count")
}
c := q.Cap()
l := q.Len()
// If already big enough.
if n <= c-l {
return
}

if c == 0 {
c = minCapacity
}

newLen := l + n
for c < newLen {
c <<= 1
}
if l == 0 {
q.buf = make([]T, c)
q.head = 0
q.tail = 0
} else {
q.resize(c)
}
}

// 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
Expand Down Expand Up @@ -349,19 +373,14 @@ func (q *Deque[T]) Remove(at int) T {
return q.PopBack()
}

// SetMinCapacity sets a minimum capacity of 2^minCapacityExp. If the value of
// the minimum capacity is less than or equal to the minimum allowed, then
// capacity is set to the minimum allowed. This may be called at anytime to set
// a new minimum capacity.
//
// Setting a larger minimum capacity may be used to prevent resizing when the
// number of stored items changes frequently across a wide range.
func (q *Deque[T]) SetMinCapacity(minCapacityExp uint) {
if 1<<minCapacityExp > minCapacity {
q.minCap = 1 << minCapacityExp
} else {
q.minCap = minCapacity
// SetBaseCapacity sets a base capacity so that at least the specified number
// of items can always be stored without resizing.
func (q *Deque[T]) SetBaseCapacity(baseCap int) {
minCap := minCapacity
for minCap < baseCap {
minCap <<= 1
}
q.minCap = minCap
}

// Swap exchanges the two values at idxA and idxB. It panics if either index is
Expand Down Expand Up @@ -406,21 +425,21 @@ func (q *Deque[T]) growIfFull() {
q.buf = make([]T, q.minCap)
return
}
q.resize()
q.resize(q.count << 1)
}

// shrinkIfExcess resize down if the buffer 1/4 full.
func (q *Deque[T]) shrinkIfExcess() {
if len(q.buf) > q.minCap && (q.count<<2) == len(q.buf) {
q.resize()
q.resize(q.count << 1)
}
}

// resize resizes the deque to fit exactly twice its current contents. This is
// used to grow the queue when it is full, and also to shrink it when it is
// only a quarter full.
func (q *Deque[T]) resize() {
newBuf := make([]T, q.count<<1)
func (q *Deque[T]) resize(newSize int) {
newBuf := make([]T, newSize)
if q.tail > q.head {
copy(newBuf, q.buf[q.head:q.tail])
} else {
Expand Down
64 changes: 51 additions & 13 deletions deque_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,43 @@ func TestBack(t *testing.T) {
}
}

func TestGrow(t *testing.T) {
var q Deque[int]
assertPanics(t, "should panic when calling New with too many args", func() {
q.Grow(-1)
})

q.Grow(35)
if q.Cap() != 64 {
t.Fatal(t, "did not grow to expected capacity")
}

q.Grow(55)
if q.Cap() != 64 {
t.Fatal(t, "expected no capacity change")
}

q.Grow(77)
if q.Cap() != 128 {
t.Fatal(t, "did not grow to expected capacity")
}

for i := 0; i < 127; i++ {
q.PushBack(i)
}
if q.Cap() != 128 {
t.Fatal(t, "expected no capacity change")
}

q.Grow(2)
if q.Cap() != 256 {
t.Fatal(t, "did not grow to expected capacity")
}
}

func TestNew(t *testing.T) {
minCap := 64
q := New[string](0, minCap)
q := New[string](minCap)
if q.Cap() != 0 {
t.Fatal("should not have allowcated mem yet")
}
Expand All @@ -264,9 +298,9 @@ func TestNew(t *testing.T) {
if q.Cap() != minCap {
t.Fatalf("worng capactiy expected %d, got %d", minCap, q.Cap())
}

curCap := 128
q = New[string](curCap, minCap)
q = New[string](minCap)
q.Grow(curCap)
if q.Cap() != curCap {
t.Fatalf("Cap() should return %d, got %d", curCap, q.Cap())
}
Expand All @@ -277,6 +311,10 @@ func TestNew(t *testing.T) {
if q.Cap() != curCap {
t.Fatalf("Cap() should return %d, got %d", curCap, q.Cap())
}

assertPanics(t, "should panic when calling New with too many args", func() {
New[int](64, 128)
})
}

func checkRotate(t *testing.T, size int) {
Expand Down Expand Up @@ -500,7 +538,8 @@ func TestInsert(t *testing.T) {
}
}

qs := New[string](16)
qs := New[string]()
qs.Grow(16)

for i := 0; i < qs.Cap(); i++ {
qs.PushBack(fmt.Sprint(i))
Expand Down Expand Up @@ -760,25 +799,24 @@ func TestRemoveOutOfRangePanics(t *testing.T) {
})
}

func TestSetMinCapacity(t *testing.T) {
func TestSetMBaseapacity(t *testing.T) {
var q Deque[string]
exp := uint(8)
q.SetMinCapacity(exp)
q.SetBaseCapacity(200)
q.PushBack("A")
if q.minCap != 1<<exp {
if q.minCap != 256 {
t.Fatal("wrong minimum capacity")
}
if len(q.buf) != 1<<exp {
if q.Cap() != 256 {
t.Fatal("wrong buffer size")
}
q.PopBack()
if q.minCap != 1<<exp {
if q.minCap != 256 {
t.Fatal("wrong minimum capacity")
}
if len(q.buf) != 1<<exp {
if q.Cap() != 256 {
t.Fatal("wrong buffer size")
}
q.SetMinCapacity(0)
q.SetBaseCapacity(0)
if q.minCap != minCapacity {
t.Fatal("wrong minimum capacity")
}
Expand Down Expand Up @@ -876,7 +914,7 @@ func BenchmarkYoyo(b *testing.B) {

func BenchmarkYoyoFixed(b *testing.B) {
var q Deque[int]
q.SetMinCapacity(16)
q.SetBaseCapacity(64000)
for i := 0; i < b.N; i++ {
for j := 0; j < 65536; j++ {
q.PushBack(j)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/gammazero/deque

go 1.18
go 1.22

0 comments on commit 632e04b

Please sign in to comment.