在Go生態已經有不少WEB框架,但感受沒有一個符合咱們的想法,咱們想要一個簡潔高效
的核心框架,提供路由
,context
,中間件
和依賴注入
,並且拒絕使用正則
和反射
,因而咱們開始構建Baa框架。一開始使用最簡單的通俗寫法實現了初版的功能,基本可用,可是性能爛到爆,優化之路漫漫開啓。git
最好的文章應該是每一步都加上優化先後的benchmark對比結果,給讀者以最直觀的感覺。我先BS一下本身,由於我懶了,沒有再回頭一步步去對比這個結果圖。github
這是咱們作這個框架時的一個基本原則,整個實現中沒有使用過regexp、reflect包。這是咱們對性能追求的基礎。帶來的另外一個收益是,沒有魔法,都是很是容易理解的實現,讓整個框架變得簡單。算法
在我上次翻譯的文章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)
slice的本質就是就是一個可變長度的array,根據存儲的容量會動態的從新分配內存遷移數據。若是長度不斷變化,會致使不斷的從新分配內存,在特定場景下,若是咱們可使用一個定長的array來優化內存分配。框架
var nameArr [1024]string pNames := nameArr[0:0] pNames = append(pNames, "val")
pNames 是一個slice,但數據操做老是在array nameArr上完成,在整個使用過程當中不會從新分配內存。函數
上面的僞代碼,在Baa中已經不存在了,Baa改用了下面的技巧來取代定長的array。工具
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,是根據經驗得來的一個值,儘量保證長度知足大部分狀況下的需求又不太大。
以前在黑夜路人微信羣
中還討論過一個問題:算法、數據結構,在實際工做中有用到過嗎?說實話,通常狀況下真不怎麼用到,不過這裏就是一個場景。
在初版中,路由就是一個map,路由匹配就是一個range,簡單,清晰,但性能天然很差。參考了 macaron
和echo
框架的設計,都是使用基數樹(radix tree)
來實現的,只是實現的細節不一樣,這裏咱們也有不一樣的細節實現,但思路基本沒變。具體實現能夠參考 wiki,和 Baa router部分 router.go
不少文章介紹過了,儘可能使用 []byte 替代 string,這裏咱們也是這麼作的。
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有額外的開銷來保證延遲調用甚至panic時也能執行,而大多數時候咱們能夠在程序的結束時直接終止,避免defer機制,再快一點點。
離目標愈來愈近,但還有一點差距,咱們也愈來愈瘋狂,最後竟然幹成了這樣,咱們把部分頻繁調用的函數取消,改成直接在一個函數中完成,由於咱們發現,即便只是一個函數調用,TMD也是開銷呀。
在整個過程當中,如何一步步分析性能問題,定位可優化的地方,go test -cpuprofile, go test -memprofile, go test -bench 就是最好的工具,每修改一次,bench看結果,profile看性能分析。
本文簡單總結了在優化過程當中的各類技巧,和部分代碼示例,更多使用姿式,自行體驗,歡迎交流和拍磚。