diff --git a/README.md b/README.md index 635bdcf..dddfce1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # stack [![Build Status](https://travis-ci.com/ef-ds/stack.svg?branch=master)](https://travis-ci.com/ef-ds/stack) [![codecov](https://codecov.io/gh/ef-ds/stack/branch/master/graph/badge.svg)](https://codecov.io/gh/ef-ds/stack) [![Go Report Card](https://goreportcard.com/badge/github.com/ef-ds/stack)](https://goreportcard.com/report/github.com/ef-ds/stack) [![GoDoc](https://godoc.org/github.com/ef-ds/stack?status.svg)](https://godoc.org/github.com/ef-ds/stack) -Package stack implements a very fast and efficient general purpose Last-In-First-Out (LIFO) stack data structure that is specifically optimized to perform when used by Microservices and serverless services running in production environments. Internally, stack stores the elements in a dynamic growing semi-circular doubly linked list of arrays. +Package stack implements a very fast and efficient general purpose Last-In-First-Out (LIFO) stack data structure that is specifically optimized to perform when used by Microservices and serverless services running in production environments. Internally, stack stores the elements in a dynamic growing semi-circular inverted singly linked list of arrays. ## Install @@ -64,7 +64,7 @@ See the [benchmark tests](https://github.com/ef-ds/stack-bench-tests/blob/master ## Performance -Stack has constant time (O(1)) on all its operations (Push/Pop/Back/Len). It's not amortized constant because stack never copies more than 256 (maxInternalSliceSize/sliceGrowthFactor) items and when it expands or grow, it never does so by more than 1024 (maxInternalSliceSize) items in a single operation. +Stack has constant time (O(1)) on all its operations (Push/Pop/Back/Len). It's not amortized constant because stack never copies more than 256 (maxInternalSliceSize/2) items and when it expands or grow, it never does so by more than 512 (maxInternalSliceSize) items in a single operation. Stack offers either the best or very competitive performance across all test sets, suites and ranges. @@ -74,9 +74,9 @@ See [performance](https://github.com/ef-ds/stack-bench-tests/blob/master/PERFORM ## Design -The Efficient Data Structures (ef-ds) stack employs a new, modern stack design: a semi-circular shaped, linked slices design. +The Efficient Data Structures (ef-ds) stack employs a new, modern stack design: a dynamic growing semi-circular inverted singly linked list of slices. -That means the [LIFO stack](https://en.wikipedia.org/wiki/Stack_(abstract_data_type)) is a [doubly-linked list](https://en.wikipedia.org/wiki/Doubly_linked_list) where each node value is a fixed size [slice](https://tour.golang.org/moretypes/7). It is semi-circular in shape because the first node in the linked list points to itself, but the last one points to nil. +That means the [LIFO stack](https://en.wikipedia.org/wiki/Stack_(abstract_data_type)) is a [singly-linked list](https://en.wikipedia.org/wiki/Singly_linked_list) where each node value is a fixed size [slice](https://tour.golang.org/moretypes/7). It is inverted singly linked list because each node points only to the previous one (instead of next) and it is semi-circular in shape because the first node in the linked list points to itself, but the last one points to nil. ![ns/op](testdata/stack.jpg?raw=true "stack Design") @@ -151,7 +151,7 @@ One sofware engineer can't change the world him/herself, but a whole bunch of us ## Competition -We're extremely interested in improving stack. Please let us know your suggestions for possible improvements and if you know of other high performance stacks not tested here, let us know and we're very glad to benchmark them. +We're extremely interested in improving stack and we're on an endless quest for better efficiency and more performance. Please let us know your suggestions for possible improvements and if you know of other high performance stacks not tested here, let us know and we're very glad to benchmark them. ## Releases diff --git a/stack.go b/stack.go index 041faad..fbbeb0d 100644 --- a/stack.go +++ b/stack.go @@ -26,29 +26,16 @@ package stack const ( // firstSliceSize holds the size of the first slice. - firstSliceSize = 4 - - // sliceGrowthFactor determines by how much and how fast the first internal - // slice should grow. A growth factor of 4, firstSliceSize = 4 and - // maxInternalSliceSize = 256, the first slice will start with size 4, - // then 16 (4*4), then 64 (16*4), then 256 (64*4), then 1024 (256*4). - // The growth factor should be tweaked together with firstSliceSize and - // maxInternalSliceSize and for maximum efficiency. - // sliceGrowthFactor only applies to the very first slice creates. All other - // subsequent slices are created with fixed size of maxInternalSliceSize. - sliceGrowthFactor = 4 + firstSliceSize = 8 // maxInternalSliceSize holds the maximum size of each internal slice. - maxInternalSliceSize = 1024 + maxInternalSliceSize = 512 ) // Stack implements an unbounded, dynamically growing Last-In-First-Out (LIFO) // stack data structure. // The zero value for stack is an empty stack ready to use. type Stack struct { - // Head points to the first node of the linked list. - head *node - // Tail points to the last node of the linked list. // In an empty stack, head and tail points to the same node. tail *node @@ -63,9 +50,6 @@ type node struct { // v holds the list of user added values in this node. v []interface{} - // n points to the next node in the linked list. - n *node - // p points to the previous node in the linked list. p *node } @@ -99,31 +83,14 @@ func (s *Stack) Back() (interface{}, bool) { // Push adds value v to the the back of the stack. // The complexity is O(1). func (s *Stack) Push(v interface{}) { - switch { - case s.head == nil: - // No nodes present yet. - h := &node{v: make([]interface{}, 0, firstSliceSize)} - h.p = h - s.head = h - s.tail = h - case len(s.tail.v) < cap(s.tail.v): - // There's room in the tail slice. - case cap(s.tail.v) < maxInternalSliceSize: - // We're on the first slice and it hasn't grown large enough yet. - l := len(s.tail.v) - nv := make([]interface{}, l, l*sliceGrowthFactor) - copy(nv, s.tail.v) - s.tail.v = nv - case s.tail.n != nil: - // There's at least one unused slice between head and tail nodes. - n := s.tail.n - s.tail = n - default: - // No available nodes, so make one. - n := &node{v: make([]interface{}, 0, maxInternalSliceSize)} - n.p = s.tail - s.tail.n = n - s.tail = n + if s.tail == nil { + s.tail = &node{v: make([]interface{}, 0, firstSliceSize)} + s.tail.p = s.tail + } else if len(s.tail.v) >= maxInternalSliceSize { + s.tail = &node{ + v: make([]interface{}, 0, maxInternalSliceSize), + p: s.tail, + } } s.len++ s.tail.v = append(s.tail.v, v) @@ -137,6 +104,7 @@ func (s *Stack) Pop() (interface{}, bool) { if s.len == 0 { return nil, false } + s.len-- tp := len(s.tail.v) - 1 vp := &s.tail.v[tp] @@ -144,9 +112,7 @@ func (s *Stack) Pop() (interface{}, bool) { *vp = nil // Avoid memory leaks s.tail.v = s.tail.v[:tp] if tp <= 0 { - // Move to the previous slice as all elements - // in the current one were removed. - s.tail = s.tail.p + s.tail = s.tail.p // Move to the previous slice. } return v, true } diff --git a/testdata/stack.jpg b/testdata/stack.jpg index 4aa1d6c..97bc3f8 100644 Binary files a/testdata/stack.jpg and b/testdata/stack.jpg differ diff --git a/unit_test.go b/unit_test.go index 19f44b3..4a17d5e 100644 --- a/unit_test.go +++ b/unit_test.go @@ -21,7 +21,6 @@ package stack import ( - "fmt" "testing" ) @@ -54,40 +53,28 @@ func TestPushPopShouldHaveAllInternalLinksInARing(t *testing.T) { pushValue, extraAddedItems := 0, 0 // Push maxInternalSliceSize items to fill the first array - expectedHeadSliceSize := firstSliceSize for i := 1; i <= maxInternalSliceSize; i++ { pushValue++ s.Push(pushValue) - - if pushValue >= expectedHeadSliceSize { - expectedHeadSliceSize *= sliceGrowthFactor - } } // Push 1 extra item to force the creation of a new array pushValue++ s.Push(pushValue) extraAddedItems++ - checkLinks(t, s, pushValue, maxInternalSliceSize, maxInternalSliceSize, s.tail, s.head, nil, s.head) + checkLinks(t, s, pushValue, maxInternalSliceSize) // Push another maxInternalSliceSize-1 to fill the second array for i := 1; i <= maxInternalSliceSize-1; i++ { pushValue++ s.Push(pushValue) - checkLinks(t, s, pushValue, maxInternalSliceSize, maxInternalSliceSize, s.tail, s.head, nil, s.head) + checkLinks(t, s, pushValue, maxInternalSliceSize) } // Push 1 extra item to force the creation of a new array (3 total) pushValue++ s.Push(pushValue) - checkLinks(t, s, pushValue, maxInternalSliceSize, maxInternalSliceSize, s.tail.p, s.head, nil, s.head.n) - /// Check middle links - if s.head.n.n != s.tail { - t.Error("Expected: s.head.n.n == s.tail; Got: s.head.n.n != s.tail") - } - if s.head.n.p != s.head { - t.Error("Expected: s.head.n.p == s.head; Got: s.head.n.p != s.head") - } + checkLinks(t, s, pushValue, maxInternalSliceSize) // Check final len after all pushes if s.Len() != maxInternalSliceSize+maxInternalSliceSize+extraAddedItems { @@ -101,14 +88,7 @@ func TestPushPopShouldHaveAllInternalLinksInARing(t *testing.T) { t.Errorf("Expected: %d; Got: %d", popValue, v) } popValue-- - checkLinks(t, s, popValue, maxInternalSliceSize, maxInternalSliceSize, s.tail, s.head, s.tail.n, s.head) - //Check last slice links (not tail anymore; tail is the middle one) - if s.tail.n.n != nil { - t.Error("Expected: s.tail.n.n == nil; Got: s.tail.n.n != nil") - } - if s.tail.n.p != s.tail { - t.Error("Expected: s.tail.n.p == s.tail; Got: s.tail.n.p != s.tail") - } + checkLinks(t, s, popValue, maxInternalSliceSize) // Pop maxInternalSliceSize-1 items to empty the tail (middle) slice for i := 1; i <= maxInternalSliceSize-1; i++ { @@ -116,14 +96,7 @@ func TestPushPopShouldHaveAllInternalLinksInARing(t *testing.T) { t.Errorf("Expected: %d; Got: %d", popValue, v) } popValue-- - checkLinks(t, s, popValue, maxInternalSliceSize, maxInternalSliceSize, s.tail, s.head, s.tail.n, s.head) - /// Check last slice links - if s.tail.n.n != nil { - t.Error("Expected: s.tail.n.n == nil; Got: s.tail.n.n != nil") - } - if s.tail.n.p != s.tail { - t.Error("Expected: s.tail.n.p == s.tail; Got: s.tail.n.p != s.tail") - } + checkLinks(t, s, popValue, maxInternalSliceSize) } // Pop one extra item to force moving the tail to the head (first) slice. This also means the old tail @@ -132,21 +105,7 @@ func TestPushPopShouldHaveAllInternalLinksInARing(t *testing.T) { t.Errorf("Expected: %d; Got: %d", popValue, v) } popValue-- - checkLinks(t, s, popValue, maxInternalSliceSize, maxInternalSliceSize, s.tail.n, s.head, s.tail.n, s.head) - /// Check middle links - if s.head.n.n != s.tail.n.n { - t.Error("Expected: s.head.n.n == s.tail.n.n; Got: s.head.n.n != s.tail.n.n") - } - if s.head.n.p != s.tail { - t.Error("Expected: s.head.n.p == s.tail; Got: s.head.n.p != s.tail") - } - //Check last slice links (not tail anymore; tail is the first one) - if s.head.n.n.n != nil { - t.Error("Expected: s.head.n.n.n == nil; Got: s.head.n.n.n != nil") - } - if s.head.n.n.p != s.tail.n { - t.Error("Expected: s.head.n.n.p == s.tail.n; Got: s.head.n.n.p != s.tail.n") - } + checkLinks(t, s, popValue, maxInternalSliceSize) // Pop maxFirstSliceSize-1 items to empty the head (first) slice for i := 1; i <= maxInternalSliceSize; i++ { @@ -154,21 +113,7 @@ func TestPushPopShouldHaveAllInternalLinksInARing(t *testing.T) { t.Errorf("Expected: %d; Got: %d", popValue, v) } popValue-- - checkLinks(t, s, popValue, maxInternalSliceSize, maxInternalSliceSize, s.tail.n, s.head, s.tail.n, s.head) - /// Check middle links - if s.head.n.n != s.tail.n.n { - t.Error("Expected: s.head.n.n == s.tail.n.n; Got: s.head.n.n != s.tail.n.n") - } - if s.head.n.p != s.tail { - t.Error("Expected: s.head.n.p == s.tail; Got: s.head.n.p != s.tail") - } - //Check last slice links (not tail anymore; tail is the first one) - if s.head.n.n.n != nil { - t.Error("Expected: s.head.n.n.n == nil; Got: s.head.n.n.n != nil") - } - if s.head.n.n.p != s.tail.n { - t.Error("Expected: s.head.n.n.p == s.tail.n; Got: s.head.n.n.p != s.tail.n") - } + checkLinks(t, s, popValue, maxInternalSliceSize) } // The stack shoud be empty @@ -178,40 +123,19 @@ func TestPushPopShouldHaveAllInternalLinksInARing(t *testing.T) { if _, ok := s.Back(); ok { t.Error("Expected: false; Got: true") } - if cap(s.head.v) != maxInternalSliceSize { - t.Errorf("Expected: %d; Got: %d", maxInternalSliceSize, cap(s.head.v)) - } if cap(s.tail.v) != maxInternalSliceSize { t.Errorf("Expected: %d; Got: %d", maxInternalSliceSize, cap(s.tail.v)) } - if s.head.n == s.tail.p { - t.Error("Expected: s.head.n != s.tail.p; Got: s.head.n == s.tail.p") - } } // Helper methods----------------------------------------------------------------------------------- // Checks the internal slices and its links. -func checkLinks(t *testing.T, s *Stack, length, headSliceSize, tailSliceSize int, headNext, headPrevious, tailNext, tailPrevious *node) { +func checkLinks(t *testing.T, s *Stack, length, tailSliceSize int) { t.Helper() if s.Len() != length { t.Errorf("Unexpected length; Expected: %d; Got: %d", length, s.Len()) } - if cap(s.head.v) != headSliceSize { - t.Errorf("Unexpected head size; Expected: %d; Got: %d", headSliceSize, len(s.head.v)) - } - if s.head.n != headNext { - t.Error("Unexpected head node; Expected: s.head.n == headNext; Got: s.head.n != headNext") - } - if s.head.p != headPrevious { - t.Error("Unexpected head; Expected: s.head.p == headPrevious; Got: s.head.p != headPrevious") - } - if s.tail.n != tailNext { - t.Error("Unexpected tailNext; Expected: s.tail.n == tailNext; Got: s.tail.n != tailNext") - } - if s.tail.p != tailPrevious { - t.Error("Unexpected tailPrevious; Expected: s.tail.p == tailPrevious; Got: s.tail.p != tailPrevious") - } if cap(s.tail.v) != tailSliceSize { t.Errorf("Unexpected tail size; Expected: %d; Got: %d", tailSliceSize, len(s.tail.v)) } @@ -231,7 +155,7 @@ func assertInvariants(t *testing.T, s *Stack, val func(i int) interface{}) { if s == nil { fail("non-nil stack", s, "non-nil") } - if s.head == nil { + if s.tail == nil { // Zero value. if s.tail != nil { fail("nil tail when zero", s.tail, nil) @@ -241,85 +165,7 @@ func assertInvariants(t *testing.T, s *Stack, val func(i int) interface{}) { } return } - - spareLinkCount := 0 - inStack := true - elemCount := 0 - smallNodeCount := 0 - index := 0 - walkLinks(t, s, func(n *node) { - if len(n.v) < maxInternalSliceSize { - smallNodeCount++ - if len(n.v) > maxInternalSliceSize { - fail("first node within bounds", len(n.v), maxInternalSliceSize) - } - } - if len(n.v) > maxInternalSliceSize { - fail("slice too big", len(n.v), maxInternalSliceSize) - } - for i, v := range n.v { - failElem := func(what string, got, want interface{}) { - fail(fmt.Sprintf("at elem %d, node %p, %s", i, n, what), got, want) - t.FailNow() - } - if !inStack { - if v != nil { - failElem("all values outside queue nil", v, nil) - } - continue - } - if v != nil { - if val != nil { - want := val(index) - if want != v { - failElem(fmt.Sprintf("element %d has expected value", index), v, want) - } - } - elemCount++ - index++ - } - } - if !inStack { - spareLinkCount++ - } - if n == s.tail { - inStack = false - } - }) - if inStack { - // We never encountered the tail pointer. - t.Errorf("tail does not point to element in list") - } - if elemCount != s.len { - fail("element count == s.len", elemCount, s.len) - } - if smallNodeCount > 1 { - fail("only one first node", smallNodeCount, 1) - } if t.Failed() { t.FailNow() } } - -// walkLinks calls f for each node in the linked list. -// It also checks link invariants: -func walkLinks(t *testing.T, s *Stack, f func(n *node)) { - t.Helper() - fail := func(what string, got, want interface{}) { - t.Errorf("link invariant %s fail; got %v want %v", what, got, want) - } - n := s.head - for { - if n.n != nil && n.n.p != n { - fail("node.n.p == node", n.n.p, n) - } - if n.p.n != nil && n.p.n != n { - fail("node.p.n == node", n.p.n, n) - } - f(n) - n = n.n - if n == nil { - break - } - } -}