cgo はビルドも遅くなるし、クロスコンパイルに問題が出ることもあるし、呼び出しオーバーヘッドも大きいです。
ただ、それでも既存の C 言語資産を使いたい場合など、使いどころは多数あります。
以下の記事では、 cgo 使用に対して次のように書かれています。
- Slower build times
- Complicated builds
- You lose access to all your tools
- Performance will always be an issue
- C calls the shots, not your code
- Deployment gets more complicated
Go では散々な言われようですが、 TinyGo ではどうでしょうか?
なお、、、 TinyGo については以下の書籍や記事などを参照してください。
cgo 呼び出しオーバーヘッドを測定する
以下のようなコードを書いてみます。
package main
/*
int max(int x, int y) {
return x < y ? y : x;
}
*/
import "C"
import (
"flag"
"fmt"
"log"
)
func main() {
x := flag.Int("max", 1000000, "max")
flag.Parse()
err := run(*x)
if err != nil {
log.Fatal(err)
}
}
func run(x int) error {
xx := C.int(x)
sum := C.int(0)
for i := C.int(0); i < xx; i++ {
sum += C.max(i, i+1)
}
fmt.Printf("sum %d\n", sum)
return nil
}
このコードを Go と TinyGo の両方でビルド && 実行してみます。
cgo で書かれた関数は処理時間がほとんどかからないため、 x で指定されるループ回数が十分に大きい時、 cgo の呼び出しオーバーヘッドが支配的になります。
実際に動かして測ってみましょう。
Go でのビルド + 実行ログはこちら。
実行環境は、 Ubuntu 22.04 + Core i5-5200U / 8GB です。
$ go build && time ./main --max 100000000
sum 987459712
real 0m8.700s
user 0m8.706s
sys 0m0.016s
TinyGo はこちら。
$ tinygo build -o tmain && time ./tmain --max 100000000
sum 987459712
real 0m0.001s
user 0m0.001s
sys 0m0.000s
どちらも 100,000,000 回実行しています。
処理時間に注目すると以下。
Go はたったこれだけの処理で 8.7 秒かかっています。
Go (cgo) | TinyGo |
---|---|
8.700s | 0.001s |
ちなみに Go で cgo を使わずに以下の関数定義をしてみます。
func maxGo(x, y C.int) C.int {
if x < y {
return y
}
return x
}
この場合は Go でも十分に早いです。
$ go build && time ./main --max 100000000
sum 987459712
real 0m0.061s
user 0m0.058s
sys 0m0.004s
まとめると以下。
Go も cgo を使わなければ十分高速です。
Go | Go (cgo) | TinyGo |
---|---|---|
0.061s | 8.700s | 0.001s |
さて、 TinyGo は cgo を使っているにも関わらず、なぜこんなに高速なのでしょうか?
TinyGo の cgo 呼び出しオーバーヘッドが小さい理由
TinyGo でも cgo はサポートされています。
TinyGo は LLVM を内包していて、 C 言語で書かれている部分も内包している LLVM clang で処理します。
外部でコンパイラを用意する必要がありません。
更に、 TinyGo は Go で書かれたソースコードを Go SSA を経て LLVM IR に変換します。
同様に C で書かれたコードは LLVM clang を用いて LLVM IR に変換します。
この時点で、 Go とは異なりすべて LLVM IR となります。
そして、このあと最適化などを実施します。
ここで大事なのは、 Go のコードも C のコードも両方 LLVM IR になっている、ということです。
これにより cgo 呼び出しではなく、単に LLMV IR 内での関数コールという扱いになります。
Go の場合は、
- goroutine stack の情報の退避
- C 呼び出し規約に合わせた変換
- 結果を goroutine stack に下記戻し
といった処理が必要となります。
一方で TinyGo の場合は LLVM IR 同士の計算のなるため上記のようなオーバーヘッドがありません。
以上により非常に高速に C 言語関数をコールすることができる、ということです。
まとめ
TinyGo の cgo 呼び出しオーバーヘッドは非常に小さいです。
リンク