golang编译提速优化

2016-04-30 fishedee 后端

1 概述

golang编译提速优化

2 程序员的自我修养

大家还记得周星驰的自我修养么?

其实程序员界也有一本自我修养,关于编译的整个过程,也许大家动态语言用多了,或者IDE用多了,关于一个源文件最后是怎么变成一个可执行文件的过程,可能并不是太熟悉。

这是c++中编译单个文件的流程

3 go build

go build server2

平时情况下,我们对项目是这么编译的。它会将整个项目的每个包重新编译,然后链接成一个可执行文件。那么,即使改掉其中一个包的文件,也需要将所有的包重新编译一次,这样真的很慢好么。

4 go install减少编译

go install mymanager

改用go install指令来编译,当项目的其中一个包编译后,其会将生成的.a文件放倒pkg文件夹下,最后将所有.a文件链接成一个可执行文件就好了。这相当于编译器在文件纬度对源码编译的结果做了缓存,当你只改掉其中一个包下的文件后,go install就会只编译这个包,然后将之前已编译好的包链接起来即可,这样就能大幅减少编译时间了。go install相当于增量编译了。

那么go怎么知道哪些文件是需要重新编译,哪些文件是不需要重新编译的呢?答案是,文件的修改时间!

package main

import (
    "package1"
    "package2"
    "package2"
)

func main(){
    //xxxxxx
}

例如,入口包包含了这三个包文件。go install时会从main.go中读取到这三个包,然后到这三个依赖的包去找package1.a与package1的源代码。如果package3.a的更新时间比package3.go的时间要新,就去重新编译package3.a,否则就不重新编译package3.a。同时,由于package2.a依赖了package3.a,那么即使package3.a的更新时间比package3.go的时间要新,也要重新编译package2.a,那是因为依赖的package3包更新了。最后是package2.a与package3.a重新编译后,重新生成执行文件即可。

总结,一个包是否重新编译在两点,该包的源文件是否更新了,该包的依赖包是否更新了?

5 go indirect减少链接

5.1 为什么链接会慢

package routers

import (
    "net/http"
)

type MM *http.Request

我们的代码中包含一段上面的这个代码,会发现上面的一行代码会导致生成的routers.a文件高达110k。

package routers
        import http "net/http"
        import url "net/url" // indirect
        type @"net/url".Userinfo struct { @"net/url".username string; @"net/url".password string; @"net/url".passwordSet bool }
        func (@"net/url".u·3 *@"net/url".Userinfo "esc:0x22") Password () (? string, ? bool) { if @"net/url".u·3.@"net/url".passwordSet { return @"net/url".u·3.@"net/url".password, bool(true) }; return string(""), bool(false) }
        func (@"net/url".u·2 *@"net/url".Userinfo "esc:0x22") String () (? string)

打开routers.a文件,发现里面会被indirect了一大堆第三方的包,net/url,reflect包等等。由于.a文件的大幅膨胀,直接导致了链接的速度超级慢。

5.2 为什么会有indirect包

什么时候会触发indirect包导入?明明我只导入了一个http的包呀,为什么golang会在.a文件上帮我indirect了net/url这些包呢,经过多次实践和试验后发现,这个问题的关键是在于导出包的对象

package routers

import (
    "net/http"
)

例如,该包已经包含了一个net/http包

var a *http.Request
type b *http.Request

而代码如果是以上a或b的情况,则都不会促进该包的代码膨胀

var A *http.Request
type B *http.Request
type C struct{
}
func (this *C) doSomething()*http.Request{
}

而代码如果是以上a,b或c的情况,则都会促进该包的代码膨胀,会直接导致http的包所有接口导入到该包上,导致代码严重膨胀,编译的链接效率超低。

原因,.a包的目的有两个,声明该包的导入符号,这个在编译.a包就能确定下来,需要的代码膨胀并不会大幅提高,golang能做到按需导入。而声明该包的导出符号,由于在编译.a包时并不能确定调用方需要多少导出的符号,所以,该包的导出变量,类型,方法等等都会全部写入到.a包的导出符号上,而这些导出符号如果依赖其它包的话,这些其它包就会indirect地包含进来。当然,golang在导出符号处理时也是比较智能的,只处理那些导出符号的所依赖indirect信息,而不是导出符号所在包的所依赖的indirect信息

5.3 解决

根据上面的讨论可以得知,减少链接时间的关键就在于,控制导出符号的变量,类型与方法。如果这些符号都是基本类型,如string,int,interface{}等等,则生成出来的.a包就小,如果这些符号是依赖于第三方的,则生成出来的.a包甚至会包含第三方的所有导出符号,导致链接时间大幅提高。

net/http 61k 1.36s
net/url 7.7k 0.69s
beego 112k 2s
xorm 128k 2s

附上常见的第三方包的链接时间

fish@iZ2820y71jjZ:~/Project/fishgo$ time go install -v server2
server2/models/cms/content
server2/models/cms/activity
server2/models/remain/brush
server2/models/cms/question
server2/models/remain/invite
server2/models/remain/point
server2/models/cms/dish
server2/models/remain/remind
server2/controllers
server2/routers
server2

real    0m20.558s
user    0m18.835s
sys 0m1.137s

这是优化前的修改content文件的重编译时间20s

fish@iZ2820y71jjZ:~/Project/fishgo$ time go install -v server2
server2/models/cms/content
server2/models/cms/question
server2/models/remain/invite
server2/models/remain/point
server2/models/cms/activity
server2/models/cms/dish
server2/models/remain/remind
server2/controllers
server2/routers
server2

real    0m12.463s
user    0m11.282s
sys 0m0.727s

这是优化后的修改content文件的重编译时间12s

6 touch 减少依赖编译

//$GOPATH/gotest/main.go
package main

import (
    "gotest/util"
)

func main() {
    util.Do()
}

main.go

//$GOPATH/gotest/util/util.go
package util

import (
    "gotest/util2"
)

func Do() {
    a := util2.GetMM()
    a.Do()
}

util.go

//$GOPATH/gotest/util2/util2.go
package util2

import (
    "fmt"
)

type MM interface {
    Do()
}

type mmInpement struct {
}

func (this *mmInpement) Do() {
    fmt.Println("uu4")
}

func GetMM() MM {
    return &mmInpement{}
}

util2.go

很明显,当util2的代码发生变更时,util包会重新编译,最后main.go也会重新编译。但是,util2只是实现发生变化,而不是接口发生变化时,util包是不需要重新编译的。但目前golang编译中没有对这一步进行智能化的判断,导致我们只是改contentao实现而不改接口时,所以依赖contentao的代码都会重新编译,导致了严重的编译时间。这个问题已经提交到golang的issue,他们仍然在考虑当中,主要是担心会忽略编译。幸好的是,这个问题我们可以通过touch来强制让golang忽略util来达成免除util重编译的效果。我也在实现这个工具中,再看看出来的效果怎么样了。

7 总结

使用了上面的方法,我们一个大概10w行的代码库,每次改一个文件的重新编译时间

  • go install优化,从3min降低到20s。
  • go indirect优化,从20s降低到12s。

基本达到了热编译的效果,大幅提高了开发人员调试golang代码的效率,爽。

相关文章