Go程序提速42%,只需改变一个字符

梦晨发自凹非寺量子位公众号 QbitAI

Go 语言本来就以轻量快速著称,一位 GitHub 员工却偶然发现:只改变一个字符的位置,能把一段代码运行速度提高足足 42%。

简直就像是……

这个简单有效的技巧一经发布,就引来众多程序员围观。

原作者自己也调侃,一般这种情况都是事先犯了个愚蠢的错误,后面才能提升这么大。

不过顺着这个思路发现有人发现,就连 Go 开发团队的核心人物 Russ Cox 都在标准库中犯过同样的错误。

什么样的错误?

发现这个问题的 Harry 在大型程序员交友平台 GitHub 工作。

他在开发一个把 GitHub 仓库中每个文件的所有者列出来的小工具。

功能很简单,就是根据 CODEOWNERS 文件中定义的规则匹配,写在越下面的规则优先级越高。

原理也很简单,就是从后往前一条一条处理,匹配到了就停止。

但就是这样一个简单的程序却出现了性能问题,处理中等大小的仓库就很慢了。

他打印出火焰图,发现大部分时间都花在了 Go 语言的正则表达式引擎中。

另外在内存动态分配 malloc垃圾回收gc上面的花费也值得注意。

要减少 malloc 的时间,就需要用到 Go 语言的逃逸分析(Escape Analysis)了。

简单来说,就是尽量把变量分配到栈上,让编译器自动管理内存的释放。

只有在“逃逸”也就是变量的作用域超出所在的栈时,才把变量分配到堆上,减轻运行时 GC 的压力。

在这次的程序中,Harry 确定了逃逸的变量是 rule 这个结构体(struct)。

但问题是,rule 存储在 RuleSet 这个切片(slice)里,按 Go 语言的规则可以确信他已经在堆中了。

再分析一下代码,发现在给 rule 赋值的时候实际上是做了一次不必要的拷贝,后面用“&”取地址时候创建了一个逃逸的指针指向它的副本。

最后解决办法也很容易想出,只需要把&移动到上面。

这样就引用了切片中的结构体,避免了拷贝。

如何彻底避免?

在热议中,有网友分享了自己是怎么避免出现这个问题的。

对于每个结构体,把它看作纯值或纯指针,压根就不去使用&这种取地址的操作,避免隐式的内存分配。

如果你想要深入理解这个问题,也有人贴心的给出了需要提前了解的一些背景知识。

最后有人指出,Rust 语言为避免这个问题,直接规定必须显式操作才能拷贝一个数据结构。

当你不习惯的时候这规定烦得要命,但是总的来看还是值得。

方便 or 规范,你更倾向于哪种做法?

参考链接:
[1]https://hmarr.com/blog/go-allocation-hunting/

[2]https://news.ycombinator.com/item?id=33594676