gammazero / deque

Fast ring-buffer deque (double-ended queue)

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Benefits of this library?

jshnaidman opened this issue · comments

commented

What's the advantage, if any, of using this in place of standard slice?

Taken from Marwan Burelle's example on SO:

queue := make([]int, 0)
// Push to the queue
queue = append(queue, 1)
// Top (just get next element, don't remove it)
x = queue[0]
// Discard top element
queue = queue[1:]
// Is empty ?
if len(queue) == 0 {
    fmt.Println("Queue is empty !")
}

Does deque offer any benchmarks comparing it to standard slice implementations?

The advantage is to keep adding and removing without needing to allocate more space or move items in the slice.

If you keep appending to a slice and removing items from the front, the slice will need to grow its underlying memory. In your example, queue = append(queue, 1) will allocate more memory and copy the underlying memory from the old to the new. Also queue = queue[1:] will move the start of the slice but will not allow the old queue[0] to be reused in the slice.

Also, suppose you want to insert items to the front of the queue. This means you will need to make more room in the queue or create a new larger queue, and then move all the items one place towards the back, then add the new item at position 0.

The deque avoids all that allocating and copying by using a circular buffer that keeps moving the head and tail inside the same slice as data is written and read. https://github.com/gammazero/deque/wiki#deque-diagram

Hi, gammazero. I run a benchmark to compare the slice append and deque PushFront/PushBack with int and string type, i hope deque operation should fast than slice operation, but it doesn't, here is the code:

func BenchmarkSliceAppendInt(b *testing.B) {
	var s []int
	for i := 0; i < b.N; i++ {
		s = append(s, i)
	}
}

func BenchmarkPushFrontInt(b *testing.B) {
	var q Deque[int]
	for i := 0; i < b.N; i++ {
		q.PushFront(i)
	}
}

func BenchmarkPushBackInt(b *testing.B) {
	var q Deque[int]
	for i := 0; i < b.N; i++ {
		q.PushBack(i)
	}
}

func BenchmarkSliceAppendString(b *testing.B) {
	var s []string
	for i := 0; i < b.N; i++ {
		s = append(s, "hello")
	}
}

func BenchmarkPushFrontString(b *testing.B) {
	var q Deque[string]
	for i := 0; i < b.N; i++ {
		q.PushFront("hello")
	}
}

func BenchmarkPushBackString(b *testing.B) {
	var q Deque[string]
	for i := 0; i < b.N; i++ {
		q.PushBack("hello")
	}
}

And here is the result:

goos: darwin
goarch: arm64
pkg: github.com/gammazero/deque
BenchmarkSliceAppendInt
BenchmarkSliceAppendInt-8      	100000000	        10.50 ns/op
BenchmarkPushFrontInt
BenchmarkPushFrontInt-8        	143916168	        11.31 ns/op
BenchmarkPushBackInt
BenchmarkPushBackInt-8         	100000000	        11.27 ns/op
BenchmarkSliceAppendString
BenchmarkSliceAppendString-8   	42217932	        30.71 ns/op
BenchmarkPushFrontString
BenchmarkPushFrontString-8     	81312292	        31.59 ns/op
BenchmarkPushBackString
BenchmarkPushBackString-8      	67633578	        35.78 ns/op

I ran it on my mackbook, with golang 1.21.0

And here is another benchmark to compare the golang standard container/list with it, here is the code:

package deque

import (
	"container/list"
	"testing"
)

func BenchmarkListPushFrontInt(b *testing.B) {
	l := list.New()
	for i := 0; i < b.N; i++ {
		l.PushFront(i)
	}
}

func BenchmarkPushFrontInt(b *testing.B) {
	var q Deque[int]
	for i := 0; i < b.N; i++ {
		q.PushFront(i)
	}
}

func BenchmarkListPushBackInt(b *testing.B) {
	l := list.New()
	for i := 0; i < b.N; i++ {
		l.PushBack(i)
	}
}

func BenchmarkPushBackInt(b *testing.B) {
	var q Deque[int]
	for i := 0; i < b.N; i++ {
		q.PushBack(i)
	}
}

func BenchmarkListPushFrontString(b *testing.B) {
	l := list.New()
	for i := 0; i < b.N; i++ {
		l.PushFront("hello")
	}
}

func BenchmarkPushFrontString(b *testing.B) {
	var q Deque[string]
	for i := 0; i < b.N; i++ {
		q.PushFront("hello")
	}
}

func BenchmarkListPushBackString(b *testing.B) {
	l := list.New()
	for i := 0; i < b.N; i++ {
		l.PushBack("hello")
	}
}

func BenchmarkPushBackString(b *testing.B) {
	var q Deque[string]
	for i := 0; i < b.N; i++ {
		q.PushBack("hello")
	}
}

func BenchmarkListInsert(b *testing.B) {
	l := list.New()
	for i := 0; i < b.N; i++ {
		l.PushBack(i)
	}
	e4 := l.PushBack(4)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		l.InsertBefore(l.Len()/2, e4)
	}
}

func BenchmarkInsert(b *testing.B) {
	q := new(Deque[int])
	for i := 0; i < b.N; i++ {
		q.PushBack(i)
	}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		q.Insert(q.Len()/2, -i)
	}
}

func BenchmarkListYoyo(b *testing.B) {
	l := list.New()
	for i := 0; i < b.N; i++ {
		for j := 0; j < 65536; j++ {
			l.PushBack(j)
		}
	}
}

func BenchmarkYoyo(b *testing.B) {
	var q Deque[int]
	for i := 0; i < b.N; i++ {
		for j := 0; j < 65536; j++ {
			q.PushBack(j)
		}
	}
}

And here is the result:

goos: darwin
goarch: arm64
pkg: github.com/gammazero/deque
BenchmarkListPushFrontInt
BenchmarkListPushFrontInt-8      	24749220	        71.06 ns/op
BenchmarkPushFrontInt
BenchmarkPushFrontInt-8          	170587078	         6.116 ns/op
BenchmarkListPushBackInt
BenchmarkListPushBackInt-8       	18711698	        61.20 ns/op
BenchmarkPushBackInt
BenchmarkPushBackInt-8           	153760459	        10.11 ns/op
BenchmarkListPushFrontString
BenchmarkListPushFrontString-8   	22249174	        58.63 ns/op
BenchmarkPushFrontString
BenchmarkPushFrontString-8       	90458510	        32.04 ns/op
BenchmarkListPushBackString
BenchmarkListPushBackString-8    	21515786	        49.91 ns/op
BenchmarkPushBackString
BenchmarkPushBackString-8        	91012513	        33.46 ns/op
BenchmarkListInsert
BenchmarkListInsert-8            	19552266	        53.17 ns/op
BenchmarkInsert
BenchmarkInsert-8                	  111536	    122618 ns/op
BenchmarkListYoyo
BenchmarkListYoyo-8              	     354	   3928389 ns/op
BenchmarkYoyo
BenchmarkYoyo-8                  	    5184	    829642 ns/op

It really fast than container/list except the Insert benchmark, but there is a note in dequeu Insert that this is not the issue with it.

// Important: Deque is optimized for O(1) operations at the ends of the queue,
// not for operations in the the middle. Complexity of this function is
// constant plus linear in the lesser of the distances between the index and
// either of the ends of the queue.

So i think dqueue should compare with the container/list, they are both double chain and support the similar functions.