LoginSignup
5
1

Go と TinyGo の cgo 呼び出しオーバーヘッドの比較

Last updated at Posted at 2023-12-14

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 呼び出しオーバーヘッドは非常に小さいです。

リンク

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1