从Baa开发中总结Go语言性能渐进优化
在Go生态已经有很多WEB框架,但感觉没有一个符合我们的想法,我们想要一个简洁高效
的核心框架,提供路由
,context
,中间件
和依赖注入
,而且拒绝使用正则
和反射
,于是我们开始构建Baa框架。一开始使用最简单的通俗写法实现了第一版的功能,基本可用,但是性能烂到爆,优化之路漫漫开启。
最好的文章应该是每一步都加上优化前后的benchmark对比结果,给读者以最直观的感受。我先BS一下自己,因为我懒了,没有再回头一步步去对比这个结果图。
拒绝正则和反射
这是我们做这个框架时的一个基本原则,整个实现中没有使用过regexp、reflect包。这是我们对性能追求的基础。带来的另一个收益是,没有魔法,都是非常容易理解的实现,让整个框架变得简单。
使用sync.Pool重用对象
在我上次翻译的文章CockroachDB GC优化总结中介绍过这些方法,在《Go语言圣经》中作者也介绍了这个方法,使用 sync.Pool 可以在一次GC之间重用对象,避免对象的频繁创建和内存分配。我们在追求性能的过程中,要尽可能减少甚至达到内存零分配,这是一个最重要的用法。
在Baa中有如下代码片段:
b.pool = sync.Pool{ New: func() interface{} { return newContext(nil, nil, b)
},
}
使用的时候:
c := b.pool.Get().(*Context)
c.reset(w, r)
使用完:
b.pool.Put(c)
使用array优化slice
slice的本质就是就是一个可变长度的array,根据存储的容量会动态的重新分配内存迁移数据。如果长度不断变化,会导致不断的重新分配内存,在特定场景下,如果我们可以使用一个定长的array来优化内存分配。
var nameArr [1024]stringpNames := 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,简单,清晰,但性能自然不好。参考了 macaron
和echo
框架的设计,都是使用基数树(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 itemfunc 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 routefunc (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看性能分析。
总结
本文简单总结了在优化过程中的各种技巧,和部分代码示例,更多使用姿势,自行体验,欢迎交流和拍砖。
- 高可用架构-- MySQL主从复制的配置
- 零基础入门深度学习 | 第二章:线性单元和梯度下降
- 比特币价,黄金和无稽之谈 - 怎样不去给比特币估值
- 在PHP中,cookie和session的使用
- 剑指 offer代码解析——面试题29数组中出线次数超过一半的数字
- 剑指offer代码解析——面试题25二叉树中和为某一值的路径
- Spring MVC 4.2 CORS 跨域访问
- 剑指offer代码解析——面试题31连续子数组的最大和
- 在VS2010上使用C#调用非托管C++生成的DLL文件(图文讲解) 背景
- 剑指offer代码解析——面试题25二叉树中和为某一值的路径
- IntPtr 转 string
- 微信开发中网页授权access_token与基础支持的access_token异同
- angularJS constant和value
- 让你的HTTPS更安全
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- MySQL如何管理客户端的连接?
- 鸿蒙 Ability 讲解(页面生命周期、后台服务、数据访问)
- MySQL如何管理客户端连接?线程池篇
- 让python装饰器不再晦涩难懂
- Android开发3年,九月份面试12家大厂跳槽成功,我有一些面试经验想分享给你们
- MySQL的防火墙功能
- Java中线程池的参数有几个?
- MySQL企业版备份工具MEB
- python生成器函数的应用场景举例---为copy过程添加进度条显示
- 短网址程序YOURLS安装及配置教程与设置中文
- MGR用哪个版本?5.7 vs 8.0
- 同事直呼666!小姐姐仅用3行代码就能玩出花来
- MySQL升级至8.0需要考虑哪些因素?
- 某云Music——JS破解全过程
- 和低效 IO 说再见,回头补一波 Java 7 的 NIO.2 特性