几周前,我读了一篇名为“ Good Code vs Go Code中的错误代码 ”的文章,做者指导咱们逐步完成实际业务用例的重构。html
本文的重点是将“坏代码”转变为“良好代码”:更具惯用性,更易读,利用go语言的细节。但它也坚持将性能做为项目的一个重要方面。这引起了个人好奇心:让咱们深刻挖掘!git
几周前,我读了一篇名为“ Good Code vs Go Code中的错误代码 ”的文章,做者指导咱们逐步完成实际业务用例的重构。html
本文的重点是将“坏代码”转变为“良好代码”:更具惯用性,更易读,利用go语言的细节。但它也坚持将性能做为项目的一个重要方面。这引起了个人好奇心:让咱们深刻挖掘!git
该程序基本上读取一个输入文件,并解析每一行以填充内存中的对象。github
$ go test -bench =。

所以,在个人机器上,“好代码”的速度提升了16%。咱们能得到更多吗?golang
根据个人经验,代码质量和性能之间存在有趣的关联。当您成功地重构代码以使其更清晰且更加分离时,您一般最终会使其更快,由于它不会使以前执行的无关指令变得混乱,而且还由于一些可能的优化变得明显且易于实现。正则表达式
另外一方面,若是你进一步追求性能,你将不得不放弃简单并诉诸于黑客。你确实会刮掉几毫秒,但代码质量会受到影响,由于它会变得更难以阅读和推理,更脆弱,更不灵活。算法
这是一个权衡:你愿意走多远?数据库
为了正确肯定您的绩效工做的优先顺序,最有价值的策略是肯定您的瓶颈并专一于它们。要实现这一点,请使用分析工具!Pprof和Trace是你的朋友:canvas
$ go test -bench =。-cpuprofile cpu.prof
$ go tool pprof -svg cpu.prof> cpu.svg
$ go test -bench =。-trace trace.out
$ go工具跟踪trace.out
跟踪证实使用了全部CPU内核(底线0,1等),这在一开始看起来是件好事。但它显示了数千个小的彩色计算切片,以及一些空闲插槽,其中一些核心处于空闲状态。咱们放大:缓存
每一个核心实际上花费大量时间闲置,并在微任务之间保持切换。看起来任务的粒度不是最优的,致使许多上下文切换以及因为同步而致使的争用。网络
让咱们检查一下竞争检测器是否同步是正确的(若是没有,那么咱们的问题比性能更大):
$ go test -race
PASS
是!!看起来是正确的,没有遇到数据争用状况。测试函数和基准函数是不一样的(参见文档),但在这里他们调用相同的函数ParseAdexpMessage,咱们可使用-race
。
“好”版本中的并发策略包括在其本身的goroutine中处理每行输入,以利用多个核心。这是一种合法的直觉,由于goroutines的声誉是轻量级和廉价的。咱们多少得益于并发性?让咱们与单个顺序goroutine中的相同代码进行比较(只需删除行解析函数调用以前的go关键字)
哎呀,没有任何并行性,它实际上更快。这意味着启动goroutine的(非零)开销超过了同时使用多个核心所节省的时间。
天然的下一步,由于咱们如今顺序而不是同时处理行,是为了不使用结果通道的(非零)开销:让咱们用裸片替换它。
咱们如今从“好”版本得到了大约40%的加速,只是简化了代码,删除了并发(差别)。
如今让咱们看一下Pprof图中的热函数调用:
咱们当前版本的基准(顺序,带切片)花费86%的时间实际解析消息,这很好。咱们很快注意到,总时间的43%用于将正则表达式与(* Regexp).FindAll匹配 。
虽然regexp是从原始文本中提取数据的一种方便灵活的方法,但它们存在缺陷,包括内存和运行时的成本。它们很强大,但对于许多用例来讲可能有点过度。
在咱们的程序中,模式
patternSubfield =“ - 。[^ - ] *”
主要用于识别以短划线“ - ” 开头的“ 命令 ”,而且一行可能有多个命令。经过一些调整,可使用bytes.Split完成。让咱们调整代码(commit,commit)以使用Split替换regexp:
哇,这是40%的额外增益!
CPU图如今看起来像这样:
没有更多正则表达式的巨大成本。从5个不一样的功能中分配内存花费了至关多的时间(40%)。有趣的是,总时间的21%如今由字节占.Trim 。
bytes.Trim指望一个“ cutset string”做为参数(对于要在左侧和右侧删除的字符),但咱们仅使用单个空格字节做为cutset。这是一个例子,您能够经过引入一些复杂性来得到性能:实现您本身的自定义“trim”函数来代替标准库函数。在自定义的“微调”的交易,只有一个割集字节。
是的,另外20%被削减了。当前版本的速度是原始“坏”速度的4倍,而机器只使用1个CPU内核。至关实质!
以前咱们放弃了在线处理级别的并发性,可是经过并发更新仍然存在改进的空间,而且具备更粗略的粒度。例如,在每一个文件在其本身的goroutine中处理时,处理6,000个文件(6,000条消息)在个人工做站上更快:
66%的胜利(即3倍的加速),这是好的但“不是那么多”,由于它利用了我全部的12个CPU内核!这可能意味着使用新的优化代码,处理整个文件仍然是一个“小任务”,goroutine和同步的开销不可忽略不计。
有趣的是,将消息数量从6,000增长到120,000对顺序版本的性能没有影响,而且下降了“每一个消息的1个goroutine”版本的性能。这是由于启动大量的goroutine是可能的,有时是有用的,但它确实给go运行时调度程序带来了一些压力。
咱们能够经过仅建立少数工做人员来减小执行时间(不是12倍因素,但仍然是这样),例如12个长期运行的goroutine,每一个goroutine处理一部分消息:
与顺序版本相比,大批消息的调优并发性删除了79%的执行时间。请注意,只有在确实要处理大量文件时,此策略才有意义。
全部CPU核心的最佳利用包括几个goroutine,每一个goroutine处理至关数量的数据,在完成以前没有任何通讯和同步。
选择与可用CPU核心数相等的多个进程(goroutine)是一种常见的启发式方法,但并不老是最佳:您的里程可能会根据任务的性质而有所不一样。例如,若是您的任务从文件系统读取或发出网络请求,那么性能比CPU核心具备更多的goroutine是彻底合理的。
咱们已经达到了这样的程度,即经过本地化加强很难提升解析代码的效率。如今,执行时间由小对象(例如Message结构)的分配和垃圾收集主导,这是有道理的,由于已知内存管理操做相对较慢。进一步优化分配策略......留给狡猾的读者练习。
这就是今天,我但愿你喜欢这个旅程。如下是一些免责声明和外卖: