Skip to content

Latest commit

 

History

History
198 lines (154 loc) · 5.06 KB

golang_deque.md

File metadata and controls

198 lines (154 loc) · 5.06 KB

golang 双端队列

虽然使用 golang 标准库 container/list 很容易实现双端队列,但经过 benchmark 压测发现数据差点意思. 在 github 中找到一个叫 edwingeng/deque 的项目,该队列的实现很不错,另外经过线上使用后,高频读写下 cpu 有下降,另外在几百万条数据堆积场景下, gc duration 也有下降. 下面分析下 deque 的设计实现.

代码地址:

https://github.com/edwingeng/deque

deque 的设计实现

golang deque

优缺点

优点

  • 双端队列设计
  • 使用二维数组实现队列,比 list 更快,数据结构 gc 更友好
  • sync.pool 优化 chunk 对象
  • 更好的扩缩容机制
  • 代码生成器,避免 interface{} 装箱拆箱操作.
  • v2 版直接支持泛型

缺点

没锁,需要自己加一把锁.

主体设计

使用了二维数组实现了队列结构,第二层切片是一个 chunk,存放真实数据,而第一层切片关联索引第二层的 chunk.

type chunk struct {
	s    int
	e    int
	data [chunkSize]Elem
}

type deque struct {
	chunks []*chunk

	chunkPitch []*chunk
	sFree      int
	eFree      int
}

func (dq *deque) PushFront(v Elem) {
	var c *chunk
	n := len(dq.chunks)
	if n == 0 {
		dq.expandStart()
		c = dq.chunks[0]
	} else {
		c = dq.chunks[0]  // 获取 chunks 头节点.
		if c.s == 0 {    // 如果该 chunk 满了, 则获取一个 chunk.
			dq.expandStart() // 把 start, end 设置 243
			c = dq.chunks[0]
		}
	}
	c.s--
	c.data[c.s] = v // 也把数据放到 chunk 的尾部.
}

func (dq *deque) PushBack(v Elem) {
	var c *chunk
	n := len(dq.chunks)
	if n == 0 {
		dq.expandEnd()
		c = dq.chunks[n]
	} else {
		c = dq.chunks[n-1]  // 获取尾部节点,如果该节点满了,则创建一个新的 chunk, start 和 end 为 0.
		if c.e == chunkSize { 
			dq.expandEnd()
			c = dq.chunks[n]
		}
	}

	// 把数据放到 chunk 对应的 index 里.
	c.data[c.e] = v

	// 自增,求下次 index 的位置.
	c.e++
}

func (dq *deque) PopBack() Elem {
	n := len(dq.chunks)
	if n == 0 {
		return elemDefValue
	}

	// 获取尾部的 chunk
	c := dq.chunks[n-1]

	// 相等说明为空
	if c.e == c.s {
		return elemDefValue
	}

	// 获取 chunk 尾部的元素, 并向前滚动索引
	c.e--
	r := c.data[c.e]

	// 置空
	c.data[c.e] = elemDefValue

	// 如果没有数据了, 则进行缩容
	if c.e == 0 {
		dq.shrinkEnd()
	}
	return r
}

func (dq *deque) Len() int {
	n := len(dq.chunks)
	switch n {
	case 0:
		return 0
	case 1:
		// 只有单 chunk
		return dq.chunks[0].e - dq.chunks[0].s
	default:
		// 头部 chunk + 尾部 chunk + 中间 chunk
		return chunkSize - dq.chunks[0].s + dq.chunks[n-1].e + (n-2)*chunkSize
	}
}

chunk 对象缓存

使用 sync.pool 缓存 chunk 数据结构, 用来减轻 gc 开销.

var (
	sharedChunkPool = newChunkPool(func() interface{} {
		return &chunk{}
	})
)

// 缩容
func (dq *deque) shrinkEnd() {
    ...
	sharedChunkPool.Put(c)
    ...
}

// 扩容
func (dq *deque) expandEnd() {
    ...
	c := sharedChunkPool.Get().(*chunk)
    ...
}

取巧的扩缩容设计

chunk 最大为 254 个元素,当超过阈值时会实例化一个新的 chunk. 每次新增的 chunk 也都会放到 chunks 切片里. deque 没有使用 list.List 链表来关联 chunk,则使用切片.

deque 对 chunks 切片做了一些优化,由于双端队列还有个向前插入 pushFront() 的场景,如果单纯用 slice,那么会不断实例化一个新切片,把新的chunk 放到 chunks[0],以前的切片 append 后面.

不断的实例化新切片和搬迁旧切片,该操作还是有开销的. 所以抽象了一个比 chunks 更大的切片做引用占位符.

http://xiaorui.cc/wp-content/uploads/2022/11/Jietu20221107-110902.jpg

func (dq *deque) expandEnd() {
	if f := dq.eFree; f == 0 {
		dq.realloc()
	}

	// 从 sync.pool 获取新的 chunk 数组
	c := sharedChunkPool.Get().(*chunk)

	// 重置 start, end 偏移量
	c.s, c.e = 0, 0
	dq.eFree--
	newEnd := len(dq.chunkPitch) - dq.eFree
	dq.chunkPitch[newEnd-1] = c
    
    // 把 chunkPitch 的数据引用到 chunks.
	dq.chunks = dq.chunkPitch[dq.sFree:newEnd]
}

benchmark

PushBack/Deque<harden>       100000000       12.0 ns/op       8 B/op      0 allocs/op
PushBack/Deque                20000000       55.5 ns/op      24 B/op      1 allocs/op
PushBack/list.List             5000000      158.7 ns/op      56 B/op      1 allocs/op

PushFront/Deque<harden>      195840157        9.2 ns/op       8 B/op      0 allocs/op
PushFront/Deque               30000000       49.2 ns/op      24 B/op      1 allocs/op
PushFront/list.List            5000000      159.2 ns/op      56 B/op      1 allocs/op

Random/Deque<harden>          65623633       15.1 ns/op       0 B/op      0 allocs/op
Random/Deque                  50000000       24.7 ns/op       4 B/op      0 allocs/op
Random/list.List              30000000       46.9 ns/op      28 B/op      1 allocs/op