cgoがGoのコンパイル速度に与える影響

## はじめに

(タイトルは[Scala の機能がコンパイル速度に与える影響](http://tldr.hatenablog.com/entry/how-the-use-of-scalas-features-affects-compile-time)にインスパイアされました。)


Goのコンパイル速度の速さはセールスポイントの一つです。しかし、そんなGoもcgoを使い始めるとコンパイルが途端に遅くなります。あんまりこのあたりについて言及しているエントリがないので書いてみることにしました。

このエントリではサンプルプログラムがいっぱい出てくるので以下のリポジトリにまとめました。

https://github.com/cubicdaiya/cgoeffectcompilespeed

また、コンパイル速度の比較も`make`で行えるようになっています。

## cgo

cgoはGoからCのコードを実行するための仕組みです。例えばこんな風にGoからCの関数を呼び出すことができます。

```go:helloworld.go
package main

/*
#include <stdio.h>
void helloworld() {
    printf("Hello, World!\n");
}
*/
import "C"
 
func main() {
        C.helloworld()
}
```

`*/`と`import "C"`の間に空行があるとコンパイルエラーになるので注意しましょう。(地味にハマリポイント)

```console
$ go run helloworld.go
Hello, World!
$
```

## ピュアGoのプログラム

例えば以下のような全部Goで書かれたプログラムがあるとします。

```
go
├── add.go
├── div.go
├── main.go
├── mul.go
└── sub.go
```

```go:go/main.go
package main

import (
        "fmt"
)

func main() {
        fmt.Println(add(5, 2))
        fmt.Println(sub(5, 2))
        fmt.Println(mul(5, 2))
        fmt.Println(div(5, 2))
}
```

```go:go/add.go
package main

func add(a, b int) int {
        return a + b
}
```

```go:go/sub.go
package main

func sub(a, b int) int {
        return a - b
}
```

```go:go/mul.go
package main

func mul(a, b int) int {
        return a * b
}
```

```go:go/div.go
package main

func div(a, b int) int {
        return a / b
}
```

これをビルドしてかかった時間を計測します。

```console
$ cd go
$ time go build
go build  0.13s user 0.04s system 98% cpu 0.164 total
$
```

## cgoを利用したGoプログラム

続いてさきほどのGoプログラムをcgo化したのが以下になります。(main.goはさっきと同じです)

```
cgo
├── add.go
├── div.go
├── main.go
├── mul.go
└── sub.go
```

```go:cgo/add.go
package main

/*
int add(int a, int b) {
    return a + b;
}
*/
import "C"

func add(a, b int) int {
        return int(C.add(C.int(a), C.int(b)))
}
```

```go:cgo/sub.go
package main

/*
int sub(int a, int b) {
    return a - b;
}
*/
import "C"

func sub(a, b int) int {
        return int(C.sub(C.int(a), C.int(b)))
}
```

```go:cgo/mul.go
package main

/*
int mul(int a, int b) {
    return a * b;
}
*/
import "C"

func mul(a, b int) int {
        return int(C.mul(C.int(a), C.int(b)))
}
```

```go:cgo/div.go
package main

/*
int div(int a, int b) {
    return a / b;
}
*/
import "C"

func div(a, b int) int {
        return int(C.div(C.int(a), C.int(b)))
}
```

これをビルドします。

```console
$ cd cgo
$ time go build
go build  0.31s user 0.25s system 96% cpu 0.583 total
$
```

ピュアGoのプログラムに比べてコンパイルするのに3倍以上の時間がかかるようになりました。

## cgoを利用したGoプログラムのコンパイルを速くする

次にcgoを利用したGoプログラムのコンパイルを速くしてみます。さっきまでと違って各演算の関数を一つのファイル(calc.go)にまとめているのがポイントです。(このmain.goもこれまでのと同じです)

```
cgofast
├── calc.go
└── main.go
```

```go:cgofast/calc.go
package main

/*
int add(int a, int b) {
    return a + b;
}

int sub(int a, int b) {
    return a - b;
}

int mul(int a, int b) {
    return a * b;
}

int div(int a, int b) {
    return a / b;
}
*/
import "C"

func add(a, b int) int {
        return int(C.add(C.int(a), C.int(b)))
}

func sub(a, b int) int {
        return int(C.sub(C.int(a), C.int(b)))
}

func mul(a, b int) int {
        return int(C.mul(C.int(a), C.int(b)))
}

func div(a, b int) int {
        return int(C.div(C.int(a), C.int(b)))
}
```

これをビルドしてみます。

```console
$ cd cgofast
$ time go build
go build  0.23s user 0.13s system 95% cpu 0.367 total
$
```

さきほどよりも少しだけ速くなりました。

## GoとCとcgo

このことからわかるのはcgoを利用したソースファイル(.go)が多ければ多いほどGoのコンパイル(`go build`)は遅くなるということです。感覚的にはcgoを利用した1ソースファイルにつき数百msecといったところでしょうか。

`go build`はCのコンパイルと違ってソースファイル毎にオブジェクトファイルを生成して差分ビルドみたいなことはしないので、おそらく`go build`の度に毎回cgoの部分のコンパイルをやってるのがcgoを利用したプログラムの`go build`が遅くなる原因ではないかと思います。

cgofastのビルドが速いのはcgoの部分を1つのファイルにまとめたことで、Cのコンパイルが走る回数が減ったためと思われます。

(**追記**：@mattnさんからパッケージにして事前に`go install`するともっと速くなるよというコメントを頂きました)

今回の例ではプログラムが非常に小さいくて実感が湧かないかも知れませんが、昔担当していたそれなりに規模のあるGoのサーバプロダクトでは深淵な理由からcgoを多用していたこともあってコンパイルするのに10秒以上かかっていました。(開発機が非力なMacBook Airだったり、プロプライエタリなライブラリを利用する関係でLinuxで開発するためにVagrant上でコンパイルしなければならなかったりしたのもあるけど)