はじめに
(タイトルはScala の機能がコンパイル速度に与える影響にインスパイアされました。)
Goのコンパイル速度の速さはセールスポイントの一つです。しかし、そんなGoもcgoを使い始めるとコンパイルが途端に遅くなります。あんまりこのあたりについて言及しているエントリがないので書いてみることにしました。
このエントリではサンプルプログラムがいっぱい出てくるので以下のリポジトリにまとめました。
また、コンパイル速度の比較もmake
で行えるようになっています。
cgo
cgoはGoからCのコードを実行するための仕組みです。例えばこんな風にGoからCの関数を呼び出すことができます。
package main
/*
#include <stdio.h>
void helloworld() {
printf("Hello, World!\n");
}
*/
import "C"
func main() {
C.helloworld()
}
*/
とimport "C"
の間に空行があるとコンパイルエラーになるので注意しましょう。(地味にハマリポイント)
$ go run helloworld.go
Hello, World!
$
ピュアGoのプログラム
例えば以下のような全部Goで書かれたプログラムがあるとします。
go
├── add.go
├── div.go
├── main.go
├── mul.go
└── sub.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))
}
package main
func add(a, b int) int {
return a + b
}
package main
func sub(a, b int) int {
return a - b
}
package main
func mul(a, b int) int {
return a * b
}
package main
func div(a, b int) int {
return a / b
}
これをビルドしてかかった時間を計測します。
$ 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
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)))
}
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)))
}
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)))
}
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)))
}
これをビルドします。
$ 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
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)))
}
これをビルドしてみます。
$ 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上でコンパイルしなければならなかったりしたのもあるけど)