在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看性能分析。
本文简单总结了在优化过程当中的各类技巧,和部分代码示例,更多使用姿式,自行体验,欢迎交流和拍砖。