數(shù)組棧、鏈表?xiàng)Ul的性能更優(yōu)【Go語言測(cè)試】

? 剛?cè)腴TGo語言,發(fā)現(xiàn)Go本身并沒有像Java那樣提供比如Stack,或是LinkedList的實(shí)現(xiàn),于是基于切片的特點(diǎn),封裝了棧、隊(duì)列、雙向隊(duì)列。棧也可以基于鏈表來實(shí)現(xiàn),那到底誰的性能會(huì)更優(yōu)呢,于是便有了這篇性能對(duì)比。

?本文將對(duì)比基于Go語言切片實(shí)現(xiàn)的棧基于鏈表實(shí)現(xiàn)的棧的性能,文中會(huì)涉及簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)、Go語言interface、slice、基準(zhǔn)測(cè)試等知識(shí)點(diǎn)。

?文中有不對(duì)的地方還望大家指出

1、棧、隊(duì)列、雙向隊(duì)列

?棧(Stack)是一種常見的數(shù)據(jù)結(jié)構(gòu),具有先進(jìn)后出(FILO)的特點(diǎn),只可以在棧頂進(jìn)行插入、刪除、查詢操作。定義棧的接口如下:

img
type Stack interface {
    //往棧中插入一個(gè)元素
    Push(int)
    
    //取出棧頂元素,如果棧為空,err!=nil
    Pop() (int, error)

    //獲取棧頂元素,如果棧為空,err!=nil
    Peek() (int, error)

    //獲取當(dāng)前棧的大小
    GetSze() int
}

?隊(duì)列(Queue)具有先進(jìn)先出(FIFO)的特點(diǎn),只可以從隊(duì)尾插入元素,從隊(duì)頭取出元素。定義棧的接口如下:

img
type Queue interface {
   //對(duì)尾入隊(duì)一個(gè)元素
   Enqueue(int)

   //對(duì)頭出隊(duì)一個(gè)元素,如果隊(duì)為空,err!=nil
   Dequeue() (int, error)

   //獲取隊(duì)頭元素,如果隊(duì)為空,err!=nil
   Front() (int, error)

   //獲取當(dāng)前隊(duì)的大小
   GetSize() int
}

?本文在實(shí)現(xiàn)的時(shí)候是實(shí)現(xiàn)了一個(gè)雙向隊(duì)列,讓雙向隊(duì)列繼承了棧和隊(duì)列的接口,這樣就可以把雙向隊(duì)列當(dāng)做棧和隊(duì)列使用啦。

?具體的,SliceList是基于Slice實(shí)現(xiàn)的雙向隊(duì)列,LinkedList是一個(gè)基于鏈表實(shí)現(xiàn)的雙向隊(duì)列,繼承關(guān)系如下圖所示。

繼承關(guān)系圖.png

2、SliceList簡(jiǎn)析

type SliceList struct {
   data []int
}

?切片是 Go 中的一種基本的數(shù)據(jù)結(jié)構(gòu),使用這種結(jié)構(gòu)可以用來管理數(shù)據(jù)集合。切片常見的操作有 append、copy。與此同時(shí),切片還具有可索引,可迭代的優(yōu)秀特性。

?切片的結(jié)構(gòu)體由3部分構(gòu)成,array 是指向一個(gè)數(shù)組的指針,len 代表當(dāng)前切片的長(zhǎng)度,cap 是當(dāng)前切片的容量。cap 總是大于等于 len 的。

?切片在append的時(shí)候,達(dá)到了切片的容量,會(huì)產(chǎn)生擴(kuò)容,擴(kuò)容的詳細(xì)解析見參考1、2。簡(jiǎn)單描述的話,也就是切片的容量小于 1024 個(gè)元素,擴(kuò)容為原來的2倍;容量大于等于1024,每次增加原來的0.25倍。擴(kuò)容會(huì)首先開辟一片內(nèi)存區(qū)域,然后把舊的數(shù)據(jù)拷貝過來,array指針指向新的內(nèi)存區(qū)域。

?所以在用切片來實(shí)現(xiàn)SliceList時(shí),在初始化SliceList的時(shí)候,可以預(yù)先指定SliceList切片數(shù)據(jù)的容量,那么之后插入數(shù)據(jù)就可以避免切片擴(kuò)容而造成了額外的時(shí)間和空間的消耗。

slice.jpg

3、LinkedList簡(jiǎn)析

type LinkedList struct {
   size  int
   first *Node
   last  *Node
}
type Node struct {
    val  int
    prev *Node
    next *Node
}

?本文的LinkedList的實(shí)現(xiàn)參考了Java中LinkedList的實(shí)現(xiàn)。

?基于鏈表實(shí)現(xiàn)的雙向隊(duì)列,每一個(gè)節(jié)點(diǎn)不僅需要保留數(shù)據(jù),還需要保留前驅(qū)和后繼節(jié)點(diǎn)的指針,所以在空間上會(huì)比基于切片的實(shí)現(xiàn)消耗更多,另外在插入新的數(shù)據(jù)的時(shí)候,需要新建Node節(jié)點(diǎn),對(duì)比于切片直接在地址上賦值,時(shí)間上也會(huì)有更多的消耗。

img

4、性能測(cè)試

?本文是基于Go testing包進(jìn)行測(cè)試的,具體的測(cè)試方法教程見參考3、4、5。

?為了更細(xì)致的對(duì)比LinkedList和SliceList的性能,本文進(jìn)行了兩方面的測(cè)試。一個(gè)是順序測(cè)試,也就是先Push一定數(shù)量的數(shù)據(jù),然后再Peek、Pop直到棧為空;另一個(gè)是隨機(jī)測(cè)試,也就是Push、Peek、Pop是隨機(jī)執(zhí)行的,當(dāng)然棧為空的時(shí)候Peek、Pop是會(huì)返回error的,這里僅測(cè)試性能,就不做處理了。

//測(cè)試命令: go test -bench=.  -benchmem  -count=1 -benchtime=1s
func TestFunc(stack utils.Stack, fun func(stack utils.Stack)) {
   fun(stack)
}

//順序測(cè)試:Push、Peek、Pop依次執(zhí)行
func orderTestStack(stack utils.Stack) {
   for i := 0; i < 10; i++ {
      for j := 0; j < 10000; j++ {
         stack.Push(j)
      }
      for stack.GetSize() > 0 {
         stack.Peek()
         stack.Pop()
      }
   }
}

//隨機(jī)測(cè)試:Push、Peek、Pop隨機(jī)執(zhí)行
func randomTestStack(stack utils.Stack) {
   for i := 0; i < 3*10*10000; i++ {
      switch r := rand.Intn(1000); r % 3 {
      case 0:
         stack.Push(r)
      case 1:
         stack.Peek()
      case 2:
         stack.Pop()
      }
   }
}

?如圖所示,是測(cè)試結(jié)果的截圖,本文關(guān)注了第三列 ns/op,第四列 B/op。

  • 第三列表示每次操作需要的時(shí)間,單位是納秒,可以基于此列來對(duì)算法的時(shí)間性能進(jìn)行對(duì)比;

  • 第四列表示每次操作需要的內(nèi)存大小,單位是Byte,可以基于此列來對(duì)算法的空間性能進(jìn)行對(duì)比。

基準(zhǔn)測(cè)試.png

?下圖是利用iData平臺(tái)對(duì)以上數(shù)據(jù)進(jìn)行簡(jiǎn)單分析可視化的結(jié)果。通過對(duì)比實(shí)驗(yàn)可以看出:

  • 在順序測(cè)試和隨機(jī)測(cè)試中,LinkedList 的時(shí)間消耗和空間消耗都會(huì)比 SliceList 多得多。
  • 有初始化容量的SliceList 會(huì)比沒有初始化容量的SliceList 在時(shí)間、空間上更有優(yōu)勢(shì)。
order.png
random.png

5、總結(jié)

?通過本文的簡(jiǎn)單對(duì)比實(shí)驗(yàn)可以發(fā)現(xiàn)SliceList在時(shí)間和空間上的性能都是優(yōu)于LinkedList;

?有初始化容量的SliceList的性能會(huì)優(yōu)于沒有初始化容量的SliceList,所以在使用Slice的時(shí)候可以注意初始化容量這一點(diǎn),對(duì)性能提升會(huì)有幫助;

?LinkedList不需要連續(xù)的內(nèi)存空間、SliceList需要連續(xù)的內(nèi)存空間,當(dāng)SliceList容量增加時(shí),需要大的連續(xù)內(nèi)存,這也是需要考慮的。

6、參考

1、go Slice 底層實(shí)現(xiàn)

2、go slice擴(kuò)容分析

3、go test 基準(zhǔn)測(cè)試

4、【Go API 開發(fā)實(shí)戰(zhàn) 21】進(jìn)階 7:go test 測(cè)試你的代碼

5、https://golang.org/cmd/go/#hdr-Testing_flags

6、SliceTricks

7、Stacks and Queues

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容