diff --git a/benchmark_test.go b/benchmark_test.go index dd3c96e..abb07e9 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -33,6 +33,7 @@ import ( "github.com/christianrpetrin/queue-tests/queueimpl5" "github.com/christianrpetrin/queue-tests/queueimpl6" "github.com/christianrpetrin/queue-tests/queueimpl7" + "github.com/christianrpetrin/queue-tests/queueimpl7generic" gammazero "github.com/gammazero/deque" juju "github.com/juju/utils/deque" phf "github.com/phf/go-queue/queue" @@ -51,7 +52,7 @@ var ( {count: 10}, {count: 100, remove: true}, {count: 1000}, // 1k - {count: 10000, remove: true}, //10k + {count: 10000, remove: true}, // 10k {count: 100000}, // 100k } @@ -105,7 +106,7 @@ func BenchmarkGammazero(b *testing.B) { for _, test := range tests { b.Run(strconv.Itoa(test.count), func(b *testing.B) { for n := 0; n < b.N; n++ { - var q gammazero.Deque + var q gammazero.Deque[int] for i := 0; i < test.count; i++ { q.PushBack(i) @@ -310,6 +311,27 @@ func BenchmarkImpl7(b *testing.B) { } } +func BenchmarkImpl7Generic(b *testing.B) { + for _, test := range tests { + b.Run(strconv.Itoa(test.count), func(b *testing.B) { + for n := 0; n < b.N; n++ { + q := queueimpl7generic.New[int]() + + for i := 0; i < test.count; i++ { + q.Push(i) + + if test.remove && i > 0 && i%3 == 0 { + tmp, tmp2 = q.Pop() + } + } + for q.Len() > 0 { + tmp, tmp2 = q.Pop() + } + } + }) + } +} + // Below are supplemental tests. func BenchmarkCookiejar(b *testing.B) { diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2d84583 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/christianrpetrin/queue-tests + +go 1.18 + +require ( + github.com/gammazero/deque v0.2.0 + github.com/juju/utils v0.0.0-20200604140309-9d78121a29e0 + github.com/phf/go-queue v0.0.0-20170504031614-9abe38d0371d + gopkg.in/karalabe/cookiejar.v2 v2.0.0-20150724131613-8dcd6a7f4951 +) + +require ( + github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e860cef --- /dev/null +++ b/go.sum @@ -0,0 +1,28 @@ +github.com/gammazero/deque v0.2.0 h1:SkieyNB4bg2/uZZLxvya0Pq6diUlwx7m2TeT7GAIWaA= +github.com/gammazero/deque v0.2.0/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= +github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c h1:3UvYABOQRhJAApj9MdCN+Ydv841ETSoy6xLzdmmr/9A= +github.com/juju/collections v0.0.0-20200605021417-0d0ec82b7271 h1:4R626WTwa7pRYQFiIRLVPepMhm05eZMEx+wIurRnMLc= +github.com/juju/errors v0.0.0-20220203013757-bd733f3c86b9 h1:EJHbsNpQyupmMeWTq7inn+5L/WZ7JfzCVPJ+DP9McCQ= +github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4 h1:NO5tuyw++EGLnz56Q8KMyDZRwJwWO8jQnj285J3FOmY= +github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208 h1:/WiCm+Vpj87e4QWuWwPD/bNE9kDrWCLvPBHOQNcG2+A= +github.com/juju/retry v0.0.0-20180821225755-9058e192b216 h1:/eQL7EJQKFHByJe3DeE8Z36yqManj9UY5zppDoQi4FU= +github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494 h1:XEDzpuZb8Ma7vLja3+5hzUqVTvAqm5Y+ygvnDs5iTMM= +github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494/go.mod h1:rUquetT0ALL48LHZhyRGvjjBH8xZaZ8dFClulKK5wK4= +github.com/juju/utils v0.0.0-20200604140309-9d78121a29e0 h1:4XlJ/Wj/bH3zGa2GU+Us72FgtmL1n3dwjP7LW7+TF/o= +github.com/juju/utils v0.0.0-20200604140309-9d78121a29e0/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= +github.com/juju/utils/v3 v3.0.0-20220130232349-cd7ecef0e94a h1:5ZWDCeCF0RaITrZGemzmDFIhjR/MVSvBUqgSyaeTMbE= +github.com/juju/version/v2 v2.0.0-20211007103408-2e8da085dc23 h1:wtEPbidt1VyHlb8RSztU6ySQj29FLsOQiI9XiJhXDM4= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/phf/go-queue v0.0.0-20170504031614-9abe38d0371d h1:U+PMnTlV2tu7RuMK5etusZG3Cf+rpow5hqQByeCzJ2g= +github.com/phf/go-queue v0.0.0-20170504031614-9abe38d0371d/go.mod h1:lXfE4PvvTW5xOjO6Mba8zDPyw8M93B6AQ7frTGnMlA8= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/karalabe/cookiejar.v2 v2.0.0-20150724131613-8dcd6a7f4951 h1:DMTcQRFbEH62YPRWwOI647s2e5mHda3oBPMHfrLs2bw= +gopkg.in/karalabe/cookiejar.v2 v2.0.0-20150724131613-8dcd6a7f4951/go.mod h1:owOxCRGGeAx1uugABik6K9oeNu1cgxP/R9ItzLDxNWA= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/queueimpl7generic/benchmark_test.go b/queueimpl7generic/benchmark_test.go new file mode 100644 index 0000000..099cc81 --- /dev/null +++ b/queueimpl7generic/benchmark_test.go @@ -0,0 +1,113 @@ +// Copyright (c) 2018 Christian R. Petrin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software out restriction, including out limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", OUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package queueimpl7generic + +import ( + "strconv" + "testing" +) + +const ( + // count holds the number of items to add to the queue. + count = 1000 +) + +var ( + // Used to store temp values, avoiding any compiler optimizations. + tmp interface{} + tmp2 bool +) + +func BenchmarkMaxFirstSliceSize(b *testing.B) { + tests := []struct { + name string + size int + }{ + {size: 1}, + {size: 2}, + {size: 4}, + {size: 8}, + {size: 16}, + {size: 32}, + {size: 64}, + {size: 128}, + } + + for _, test := range tests { + b.Run(strconv.Itoa(test.size), func(b *testing.B) { + for n := 0; n < b.N; n++ { + maxFirstSliceSize = test.size + q := New[int]() + + for i := 0; i < count; i++ { + q.Push(n) + } + for tmp, tmp2 = q.Pop(); tmp2; tmp, tmp2 = q.Pop() { + } + } + }) + } +} + +func BenchmarkMaxSubsequentSliceSize(b *testing.B) { + tests := []struct { + name string + size int + }{ + {size: 1}, + {size: 10}, + {size: 20}, + {size: 30}, + {size: 40}, + {size: 50}, + {size: 60}, + {size: 70}, + {size: 80}, + {size: 90}, + {size: 100}, + {size: 200}, + {size: 2}, + {size: 4}, + {size: 8}, + {size: 16}, + {size: 32}, + {size: 64}, + {size: 128}, + {size: 256}, + {size: 512}, + {size: 1024}, + } + + for _, test := range tests { + b.Run(strconv.Itoa(test.size), func(b *testing.B) { + for n := 0; n < b.N; n++ { + maxInternalSliceSize = test.size + q := New[int]() + + for i := 0; i < count; i++ { + q.Push(n) + } + for tmp, tmp2 = q.Pop(); tmp2; tmp, tmp2 = q.Pop() { + } + } + }) + } +} diff --git a/queueimpl7generic/queueimpl7generic.go b/queueimpl7generic/queueimpl7generic.go new file mode 100644 index 0000000..f771b7a --- /dev/null +++ b/queueimpl7generic/queueimpl7generic.go @@ -0,0 +1,153 @@ +// Copyright (c) 2018 Christian R. Petrin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Package queueimpl7generic implements an unbounded, dynamically growing FIFO queue. +// Internally, queue store the values in fixed sized slices that are linked using +// a singly linked list. +// This implementation tests the queue performance when performing lazy creation of +// the internal slice as well as starting with a 1 sized slice, allowing it to grow +// up to 16 by using the builtin append function. Subsequent slices are created with +// 128 fixed size. +package queueimpl7generic + +// Keeping below as var so it is possible to run the slice size bench tests with no coding changes. +var ( + // firstSliceSize holds the size of the first slice. + firstSliceSize = 1 + + // maxFirstSliceSize holds the maximum size of the first slice. + maxFirstSliceSize = 16 + + // maxInternalSliceSize holds the maximum size of each internal slice. + maxInternalSliceSize = 128 +) + +// Queueimpl7Generic represents an unbounded, dynamically growing FIFO queue. +// The zero value for queue is an empty queue ready to use. +type Queueimpl7Generic[T any] struct { + // Head points to the first node of the linked list. + head *Node[T] + + // Tail points to the last node of the linked list. + // In an empty queue, head and tail points to the same node. + tail *Node[T] + + // Hp is the index pointing to the current first element in the queue + // (i.e. first element added in the current queue values). + hp int + + // Len holds the current queue values length. + len int + + // lastSliceSize holds the size of the last created internal slice. + lastSliceSize int +} + +// Node represents a queue node. +// Each node holds a slice of user managed values. +type Node[T any] struct { + // v holds the list of user added values in this node. + v []T + + // n points to the next node in the linked list. + n *Node[T] +} + +// New returns an initialized queue. +func New[T any]() *Queueimpl7Generic[T] { + return new(Queueimpl7Generic[T]).Init() +} + +// Init initializes or clears queue q. +func (q *Queueimpl7Generic[T]) Init() *Queueimpl7Generic[T] { + q.head = nil + q.tail = nil + q.hp = 0 + q.len = 0 + return q +} + +// Len returns the number of elements of queue q. +// The complexity is O(1). +func (q *Queueimpl7Generic[T]) Len() int { return q.len } + +// Front returns the first element of queue q or nil if the queue is empty. +// The second, bool result indicates whether a valid value was returned; +// if the queue is empty, false will be returned. +// The complexity is O(1). +func (q *Queueimpl7Generic[T]) Front() (T, bool) { + if q.head == nil { + return empty[T](), false + } + return q.head.v[q.hp], true +} + +// Push adds a value to the queue. +// The complexity is O(1). +func (q *Queueimpl7Generic[T]) Push(v T) { + if q.head == nil { + h := newNode[T](firstSliceSize) + q.head = h + q.tail = h + q.lastSliceSize = maxFirstSliceSize + } else if len(q.tail.v) >= q.lastSliceSize { + n := newNode[T](maxInternalSliceSize) + q.tail.n = n + q.tail = n + q.lastSliceSize = maxInternalSliceSize + } + + q.tail.v = append(q.tail.v, v) + q.len++ +} + +// Pop retrieves and removes the current element from the queue. +// The second, bool result indicates whether a valid value was returned; +// if the queue is empty, false will be returned. +// The complexity is O(1). +func (q *Queueimpl7Generic[T]) Pop() (T, bool) { + if q.head == nil { + return empty[T](), false + } + + v := q.head.v[q.hp] + q.head.v[q.hp] = empty[T]() // Avoid memory leaks + q.len-- + q.hp++ + if q.hp >= len(q.head.v) { + n := q.head.n + q.head.n = nil // Avoid memory leaks + q.head = n + q.hp = 0 + } + return v, true +} + +// newNode returns an initialized node. +func newNode[T any](capacity int) *Node[T] { + return &Node[T]{ + v: make([]T, 0, capacity), + } +} + +func empty[T any]() T { + var tmp T + return tmp +} diff --git a/queueimpl7generic/queueimpl7generic_test.go b/queueimpl7generic/queueimpl7generic_test.go new file mode 100644 index 0000000..6e8b2ca --- /dev/null +++ b/queueimpl7generic/queueimpl7generic_test.go @@ -0,0 +1,224 @@ +// Copyright (c) 2018 Christian R. Petrin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package queueimpl7generic + +import ( + "testing" +) + +func TestQueueImpl7NewQueueShouldReturnInitiazedInstanceOfQueue(t *testing.T) { + q := New[int]() + + if q == nil { + t.Error("Expected: new instance of queue; Got: nil") + } +} + +func TestQueueImpl7WithZeroValueShouldReturnReadyToUseQueue(t *testing.T) { + var q Queueimpl7Generic[int] + q.Push(1) + q.Push(2) + + v, ok := q.Pop() + if !ok || v != 1 { + t.Errorf("Expected: 1; Got: %d", v) + } + v, ok = q.Pop() + if !ok || v != 2 { + t.Errorf("Expected: 2; Got: %d", v) + } + _, ok = q.Pop() + if ok { + t.Error("Expected: empty slice (ok=false); Got: ok=true") + } +} + +func TestQueueImpl7WithZeroValueAndEmptyShouldReturnAsEmpty(t *testing.T) { + var q Queueimpl7Generic[int] + if _, ok := q.Front(); ok { + t.Error("Expected: false as the queue is empty; Got: true") + } + if _, ok := q.Pop(); ok { + t.Error("Expected: false as the queue is empty; Got: true") + } + if l := q.Len(); l != 0 { + t.Errorf("Expected: 0 as the queue is empty; Got: %d", l) + } +} + +func TestQueueImpl7InitShouldReturnAEmptyQueue(t *testing.T) { + var q Queueimpl7Generic[int] + q.Push(1) + + q.Init() + + if _, ok := q.Front(); ok { + t.Error("Expected: false as the queue is empty; Got: true") + } + if _, ok := q.Pop(); ok { + t.Error("Expected: false as the queue is empty; Got: true") + } + if l := q.Len(); l != 0 { + t.Errorf("Expected: 0 as the queue is empty; Got: %d", l) + } +} + +func TestQueueImpl7WithNilValuesShouldReturnAllValuesInOrder(t *testing.T) { + q := New[int]() + q.Push(1) + q.Push(0) + q.Push(2) + q.Push(0) + + v, ok := q.Pop() + if !ok || v != 1 { + t.Errorf("Expected: 1; Got: %d", v) + } + v, ok = q.Pop() + if !ok || v != 0 { + t.Errorf("Expected: 0; Got: %d", v) + } + v, ok = q.Pop() + if !ok || v != 2 { + t.Errorf("Expected: 2; Got: %d", v) + } + v, ok = q.Pop() + if !ok || v != 0 { + t.Errorf("Expected: 0; Got: %d", v) + } + _, ok = q.Pop() + if ok { + t.Error("Expected: empty slice (ok=false); Got: ok=true") + } +} + +func TestQueueImpl7PushPopFrontShouldRetrieveAllElementsInOrder(t *testing.T) { + tests := map[string]struct { + putCount []int + getCount []int + remainingCount int + }{ + "Test 1 item": { + putCount: []int{1}, + getCount: []int{1}, + remainingCount: 0, + }, + "Test 100 items": { + putCount: []int{100}, + getCount: []int{100}, + remainingCount: 0, + }, + "Test 1000 items": { + putCount: []int{1000}, + getCount: []int{1000}, + remainingCount: 0, + }, + "Test sequence 1": { + putCount: []int{1, 2, 100, 101}, + getCount: []int{1, 2, 100, 101}, + remainingCount: 0, + }, + "Test sequence 2": { + putCount: []int{10, 1}, + getCount: []int{1, 10}, + remainingCount: 0, + }, + "Test sequence 3": { + putCount: []int{101, 101}, + getCount: []int{100, 101}, + remainingCount: 1, + }, + "Test sequence 4": { + putCount: []int{1000, 1000, 1001}, + getCount: []int{10, 10, 1}, + remainingCount: 2980, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + q := New[int]() + lastPut := 0 + lastGet := 0 + var ok bool + var v int + for count := 0; count < len(test.getCount); count++ { + for i := 1; i <= test.putCount[count]; i++ { + lastPut++ + q.Push(lastPut) + if v, ok = q.Front(); !ok || v != lastGet+1 { + t.Errorf("Expected: %d; Got: %d", lastGet, v) + } + } + + for i := 1; i <= test.getCount[count]; i++ { + lastGet++ + v, ok = q.Front() + if !ok || v != lastGet { + t.Errorf("Expected: %d; Got: %d", lastGet, v) + } + v, ok = q.Pop() + if !ok || v != lastGet { + t.Errorf("Expected: %d; Got: %d", lastGet, v) + } + } + } + + if q.Len() != test.remainingCount { + t.Errorf("Expected: %d; Got: %d", test.remainingCount, q.Len()) + } + + if test.remainingCount > 0 { + if v, ok = q.Front(); !ok || v == 0 { + t.Error("Expected: non-empty queue; Got: empty") + } + } else { + if v, ok = q.Front(); ok || v != 0 { + t.Error("Expected: empty queue; Got: non-empty") + } + } + + for i := 1; i <= test.remainingCount; i++ { + lastGet++ + + if v, ok = q.Front(); !ok || v != lastGet { + t.Errorf("Expected: %d; Got: %d", lastGet, v) + } + v, ok = q.Pop() + if !ok || v != lastGet { + t.Errorf("Expected: %d; Got: %d", lastGet, v) + } + } + if v, ok = q.Front(); ok || v != 0 { + t.Errorf("Expected: 0 as the queue should be empty; Got: %d", v) + } + if v, ok = q.Pop(); ok || v != 0 { + t.Errorf("Expected: 0 as the queue should be empty; Got: %d", v) + } + if v, ok = q.Front(); ok || v != 0 { + t.Error("Expected: empty queue; Got: non-empty") + } + if q.Len() != 0 { + t.Errorf("Expected: %d; Got: %d", 0, q.Len()) + } + }) + } +}