├── .gitignore ├── LICENSE ├── README.md ├── window.go └── window_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | /output/ 2 | !.gitignore 3 | .* 4 | /bin/ 5 | _obj/ 6 | *.6 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012. Jake Brukhman 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 7 | 1. Redistributions of source code must retain the above 8 | copyright notice, this list of conditions and the 9 | following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials 13 | provided with the distribution. 14 | 3. The name of the author may not be used to endorse or 15 | promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 19 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 24 | GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 26 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 27 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Moving Window Data Structure 2 | 3 | Copyright (c) 2012. Jake Brukhman. (jbrukh@gmail.com). 4 | All rights reserved. See the LICENSE file for BSD-style 5 | license. 6 | 7 | ------------ 8 | 9 | ### Installation 10 | 11 | You need at least the following weekly version of Go: 12 | 13 | go version weekly.2012-02-07 +52ba9506bd99 14 | 15 | You can then use the 'go' command to obtain the package: 16 | 17 | $ go get github.com/jbrukh/window 18 | $ go install -v github.com/jbrukh/window 19 | 20 | To run the benchmarks: 21 | 22 | $ go test -test.bench=".*" 23 | 24 | ### Documentation 25 | 26 | A moving window is a FIFO data structure with a fixed maximum size and the property that if the size has been reached and an element is pushed into the window, then the head of the window is pushed out to accomodate it. 27 | 28 | Typically, two operations are of importance whilst working with moving windows in a high-performance context. First, pushes are expected to happen often as lots of data comes in. Secondly, the window is typically converted to a simple array for interfacing with secondary processes, such as graphing. 29 | 30 | The MovingWindow object implemented in this package optimizes both of these operations by trading off with space complexity. Given two parameters -- size `S` and space parameter `M` -- the window will allocate a single backing array of length `SM` and complexity of background copy operations will be proportional to `1/M`. 31 | 32 | As demonstrated in the benchmarks below, a MovingWindow can operate approximately 20x faster than a list-backed implementation and 3-4x faster than a ring-backed implementation. However, it is 5000x more efficient at generating array output because it can take slices of the backing array. It is also worth noting that most of the performance gain is reached quickly with relatively small `M`: for instance, setting `M := S/100` is a good starting point. 33 | 34 | ### Benchmarks 35 | 36 | Here are some benchmarks: 37 | 38 | BenchmarkListS1000 10 188497800 ns/op 39 | BenchmarkRingS1000 50 35760520 ns/op 40 | BenchmarkS1000M1 1 2817453000 ns/op 41 | BenchmarkS1000M10 200 9012795 ns/op 42 | BenchmarkS1000M100 200 8807370 ns/op 43 | BenchmarkS1000M500 200 8471190 ns/op 44 | BenchmarkSlicifyList 50000 41656 ns/op 45 | BenchmarkSlicifyRing 50000 48598 ns/op 46 | BenchmarkSlicifyWindow 200000000 9.30 ns/op 47 | 48 | The first set of benchmarks measures the time it takes for 1,000,000 points to go through a size 1000 list-based moving window, ring-based moving window, and a MovingWindow under different values of its space parameter M. The later test measures how long it takes to convert the moving windows into a slice, a common operation. 49 | -------------------------------------------------------------------------------- /window.go: -------------------------------------------------------------------------------- 1 | package window 2 | 3 | // An array-based moving window; a moving window 4 | // is a queue with a maximum size and the property 5 | // that when the size is reached, pushing a new 6 | // element into the queue causes the head to be 7 | // popped. 8 | // 9 | // You can optimize the amount of slice copying 10 | // that the MovingWindow will be doing by trading 11 | // off with space complexity. Namely, the underlying 12 | // array is allocated with a size that is the 13 | // multiple of the intended capacity of the queue 14 | // so that copying is less frequent. 15 | type MovingWindow struct { 16 | arr []float64 17 | size int 18 | head int 19 | tail int 20 | } 21 | 22 | // PushBack will push a new piece of data into 23 | // the moving window 24 | func (m *MovingWindow) PushBack(v float64) { 25 | // if the array is full, rewind 26 | if m.tail == len(m.arr) { 27 | m.rewind() 28 | } 29 | // push the value 30 | m.arr[m.tail] = v 31 | // check if the window is full, 32 | // and move head pointer appropriately 33 | if m.tail-m.head >= m.size { 34 | m.head++ 35 | } 36 | m.tail++ 37 | } 38 | 39 | // rewind will copy the last size-1 elements 40 | // from the end of the underlying array to 41 | // the front, starting at index 0. 42 | func (m *MovingWindow) rewind() { 43 | l := len(m.arr) 44 | for i := 0; i < m.size-1; i++ { 45 | m.arr[i] = m.arr[l-m.size+i+1] 46 | } 47 | m.head, m.tail = 0, m.size-1 48 | } 49 | 50 | // Slice will present the MovingWindow in 51 | // the form of a slice. This operation never 52 | // requires array copying of any kind. 53 | // 54 | // Note that this value is guaranteed to be 55 | // good only until the next call to push. If 56 | // you wish to save the reference, you should 57 | // make a copy. 58 | func (m *MovingWindow) Slice() []float64 { 59 | return m.arr[m.head:m.tail] 60 | } 61 | 62 | // Size returns the size of the moving window, 63 | // which is set at initialization 64 | func (m *MovingWindow) Size() int { 65 | return m.size 66 | } 67 | 68 | // New creates a new moving window, 69 | // with the size and multiple specified. 70 | // 71 | // This data structures trades off space 72 | // and copying complexity; more precisely, 73 | // the number of moving windows that can 74 | // be displayed without having to do any 75 | // array copying is proportional to approx 1/M, 76 | // where M is the multiple. 77 | func New(size, multiple int) *MovingWindow { 78 | if size < 1 || multiple < 1{ 79 | panic("Must have positive size and multiple") 80 | } 81 | capacity := size*multiple 82 | return &MovingWindow{ 83 | arr: make([]float64, capacity, capacity), 84 | size: size, 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /window_test.go: -------------------------------------------------------------------------------- 1 | package window 2 | 3 | import "testing" 4 | import "container/list" 5 | import "container/ring" 6 | 7 | func BenchmarkListS1000(b *testing.B) { 8 | l(1000,b) 9 | } 10 | 11 | func BenchmarkRingS1000(b *testing.B) { 12 | r(1000,b) 13 | } 14 | 15 | func BenchmarkS1000M1(b *testing.B) { 16 | m(1000,1,b) 17 | } 18 | 19 | func BenchmarkS1000M10(b *testing.B) { 20 | m(1000,10,b) 21 | } 22 | 23 | func BenchmarkS1000M100(b *testing.B) { 24 | m(1000,100,b) 25 | } 26 | 27 | func BenchmarkS1000M500(b *testing.B) { 28 | m(1000,500,b) 29 | } 30 | 31 | func BenchmarkSlicifyList(b *testing.B) { 32 | b.StopTimer() 33 | l := getList(1000) 34 | b.StartTimer() 35 | for i := 0; i < b.N; i++ { 36 | slicifyList(l) 37 | } 38 | } 39 | 40 | func BenchmarkSlicifyRing(b *testing.B) { 41 | b.StopTimer() 42 | l := getRing(1000) 43 | b.StartTimer() 44 | for i := 0; i < b.N; i++ { 45 | slicifyRing(l) 46 | } 47 | } 48 | 49 | func BenchmarkSlicifyWindow(b *testing.B) { 50 | b.StopTimer() 51 | w := getWindow(1000) 52 | b.StartTimer() 53 | for i := 0; i < b.N; i++ { 54 | slicifyWindow(w) 55 | } 56 | } 57 | 58 | // m will, given the size and multiple, run 59 | // 1000 times the size worth of data through 60 | // the moving window 61 | func m(size, multiple int, b *testing.B) { 62 | b.StopTimer() 63 | w := New(size, multiple) 64 | TIMES := 1000*size 65 | b.StartTimer() 66 | for i := 0; i < b.N; i++ { 67 | for j := 0; j < TIMES; j++ { 68 | w.PushBack(float64(i)) 69 | } 70 | } 71 | } 72 | 73 | func l(size int, b *testing.B) { 74 | b.StopTimer() 75 | lst := list.New() 76 | TIMES := 1000*size 77 | b.StartTimer() 78 | for i := 0; i < b.N; i++ { 79 | for j := 0; j < TIMES; j++ { 80 | lst.PushBack(float64(j)) 81 | if (lst.Len() > size) { 82 | lst.Remove(lst.Front()) 83 | } 84 | } 85 | } 86 | } 87 | 88 | // contributed by: Dustin 89 | func r(size int, b *testing.B) { 90 | b.StopTimer() 91 | rng := ring.New(size) 92 | TIMES := 1000 * size 93 | b.StartTimer() 94 | for i := 0; i < b.N; i++ { 95 | for j := 0; j < TIMES; j++ { 96 | rng.Value = float64(j) 97 | rng = rng.Prev() 98 | } 99 | } 100 | } 101 | 102 | func getList(size int) (l *list.List){ 103 | l = list.New() 104 | for i := 0; i < size; i++ { 105 | l.PushBack(float64(i)) 106 | } 107 | return 108 | } 109 | 110 | func getRing(size int) (r *ring.Ring) { 111 | r = ring.New(size) 112 | for i := 0; i < size; i++ { 113 | r.Value = float64(i) 114 | r = r.Prev() 115 | } 116 | return 117 | } 118 | 119 | func getWindow(size int) (w *MovingWindow) { 120 | w = New(size, 1) 121 | for i := 0; i < size; i++ { 122 | w.PushBack(float64(i)) 123 | } 124 | return 125 | } 126 | 127 | func slicifyList(lst *list.List) { 128 | s := make([]float64, 0, lst.Len()) 129 | for e := lst.Front(); e != nil; e = e.Next() { 130 | s = append(s, e.Value.(float64)) 131 | } 132 | } 133 | 134 | func slicifyRing(r *ring.Ring) { 135 | l := r.Len() 136 | s := make([]float64, 0, l) 137 | for i := 0; i < l; i++ { 138 | s = append(s, r.Value.(float64)) 139 | r = r.Prev() 140 | } 141 | } 142 | 143 | // not necessary, but to be completely fair 144 | func slicifyWindow(w *MovingWindow) { 145 | s := w.Slice() 146 | s[0] = 0 147 | } 148 | --------------------------------------------------------------------------------