從Baa開發中總結Go語言性能漸進優化

在Go生態已經有不少WEB框架,但感受沒有一個符合咱們的想法,咱們想要一個簡潔高效的核心框架,提供路由context中間件依賴注入,並且拒絕使用正則反射,因而咱們開始構建Baa框架。一開始使用最簡單的通俗寫法實現了初版的功能,基本可用,可是性能爛到爆,優化之路漫漫開啓。git

最好的文章應該是每一步都加上優化先後的benchmark對比結果,給讀者以最直觀的感覺。我先BS一下本身,由於我懶了,沒有再回頭一步步去對比這個結果圖。github

拒絕正則和反射

這是咱們作這個框架時的一個基本原則,整個實現中沒有使用過regexp、reflect包。這是咱們對性能追求的基礎。帶來的另外一個收益是,沒有魔法,都是很是容易理解的實現,讓整個框架變得簡單。算法

使用sync.Pool重用對象

在我上次翻譯的文章CockroachDB GC優化總結中介紹過這些方法,在《Go語言聖經》中做者也介紹了這個方法,使用 sync.Pool 能夠在一次GC之間重用對象,避免對象的頻繁建立和內存分配。咱們在追求性能的過程當中,要儘量減小甚至達到內存零分配,這是一個最重要的用法。segmentfault

在Baa中有以下代碼片斷:微信

b.pool = sync.Pool{
    New: func() interface{} {
        return newContext(nil, nil, b)
    },
}

使用的時候:數據結構

c := b.pool.Get().(*Context)
c.reset(w, r)

使用完:app

b.pool.Put(c)

使用array優化slice

slice的本質就是就是一個可變長度的array,根據存儲的容量會動態的從新分配內存遷移數據。若是長度不斷變化,會致使不斷的從新分配內存,在特定場景下,若是咱們可使用一個定長的array來優化內存分配。框架

var nameArr [1024]string
pNames := nameArr[0:0]
pNames = append(pNames, "val")

pNames 是一個slice,但數據操做老是在array nameArr上完成,在整個使用過程當中不會從新分配內存。函數

上面的僞代碼,在Baa中已經不存在了,Baa改用了下面的技巧來取代定長的array。工具

slice也能重用

slice的重用,其實和上面的利用array優化基本一致,就是初始分配一個較大的容量,儘量在使用的過程當中都不會超出容量,固然也不用擔憂,萬一不夠用了,會自動擴容,只不過會進行一次內存分配。

在Baa中有以下代碼片斷:

// newContext create a http context
func newContext(w http.ResponseWriter, r *http.Request, b *Baa) *Context {
    c := new(Context)
    c.Resp = NewResponse(w, b)
    c.baa = b
    c.pNames = make([]string, 0, 32)
    c.pValues = make([]string, 0, 32)
    c.handlers = make([]HandlerFunc, len(b.middleware), len(b.middleware)+3)
    copy(c.handlers, b.middleware)
    c.reset(w, r)
    return c
}

// reset ...
func (c *Context) reset(w http.ResponseWriter, r *http.Request) {
    c.Resp.reset(w)
    c.Req = r
    c.hi = 0
    c.handlers = c.handlers[:len(c.baa.middleware)]
    c.pNames = c.pNames[:0]
    c.pValues = c.pValues[:0]
    c.store = nil
}

注意newContext中的 c.pNames和c.pValues 以及 reset中的 c.pNames和c.pValues,經過 slice[:0] 來重用以前的slice,避免內存從新分配。至於上面的長度32,是根據經驗得來的一個值,儘量保證長度知足大部分狀況下的需求又不太大。

使用Radix tree重寫路由

以前在黑夜路人微信羣中還討論過一個問題:算法、數據結構,在實際工做中有用到過嗎?說實話,通常狀況下真不怎麼用到,不過這裏就是一個場景。

在初版中,路由就是一個map,路由匹配就是一個range,簡單,清晰,但性能天然很差。參考了 macaronecho框架的設計,都是使用基數樹(radix tree)來實現的,只是實現的細節不一樣,這裏咱們也有不一樣的細節實現,但思路基本沒變。具體實現能夠參考 wiki,和 Baa router部分 router.go

string的性能不怎樣

不少文章介紹過了,儘可能使用 []byte 替代 string,這裏咱們也是這麼作的。

Map的range好低效

map和slice的range性能差一個數量級啊,因此,你會發現咱們取消了大量的map改成了slice,在slice也能重用這一節的代碼示例中 pNames和pValues就是用來取代原來的 map[string]string,由於map range的效率過低了。

凡是迭代就有開銷

slice的迭代是很快,但是總仍是迭代,是迭代就有開銷,爲了追求極致的性能也是瘋了。在路由匹配時,咱們給全部的路由pattern設置了單字節的index,若是首字母都不匹配,就沒有必要繼續後面的字符匹配了。

路由條目建立:

// newRoute create a route item
func newRoute(pattern string, handles []HandlerFunc, router *Router) *Route {
    r := new(Route)
    r.pattern = pattern
    r.alpha = pattern[0]
    r.handlers = handles
    r.router = router
    r.children = make([]*Route, 0)
    return r
}

路由條目匹配:

// findChild find child static route
func (r *Route) findChild(b byte) *Route {
    var i int
    var l = len(r.children)
    for ; i < l; i++ {
        if r.children[i].alpha == b && !r.children[i].hasParam {
            return r.children[i]
        }
    }
    return nil
}

注意 r.alpha 就是用來儘量避免迭代進一步提升性能的。

defer也僅是方便

在追求極致性能的路上,我都快瘋了,在一步步測試的過程當中,發現去掉defer也能提升一些性能,雨痕學堂微信公衆號 中的一篇文章也提到了這個問題,由於defer有額外的開銷來保證延遲調用甚至panic時也能執行,而大多數時候咱們能夠在程序的結束時直接終止,避免defer機制,再快一點點。

函數調用也是開銷

離目標愈來愈近,但還有一點差距,咱們也愈來愈瘋狂,最後竟然幹成了這樣,咱們把部分頻繁調用的函數取消,改成直接在一個函數中完成,由於咱們發現,即便只是一個函數調用,TMD也是開銷呀。

pprof是神器

在整個過程當中,如何一步步分析性能問題,定位可優化的地方,go test -cpuprofile, go test -memprofile, go test -bench 就是最好的工具,每修改一次,bench看結果,profile看性能分析。

總結

本文簡單總結了在優化過程當中的各類技巧,和部分代碼示例,更多使用姿式,自行體驗,歡迎交流和拍磚。

相關文章
相關標籤/搜索