きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

Go で hello-world の減量

Go言語ってビルドサイズが大きいですよね。 バイナリサイズを削減する ldflagsupx について、どの程度の減量が見込めるのか確認してみます。 ついでに標準ライブラリへの理解を兼ねて hello-world の書き方もいくつかのパターンで比べてみます。

% go version
go version go1.15.5 darwin/amd64

fmt

一般的な hello-world の書き方です。

func main() {
    fmt.Print("Hello, world!")
}

いつも使っている表現ですが fmt パッケージで標準出力するのにちょっと違和感を持ってました。

os

func main() {
    os.Stdout.WriteString("Hello, world!")
}

fmt に比べて、こちらの方が標準出力している感がありますね。 でも fmt の方が色んな型を渡せて便利なので fmt を使っちゃいますね。

syscall

func main() {
    syscall.Write(syscall.Stdout, []byte("Hello, world!"))
}

使ったことはありませんでしたが、意外とシンプルに書けました。 syscall.Stdoutuintptr 型の変数で os.File 型の os.Stdout とは別物です。

バイナリサイズ比較

ビルドしてサイズを比べてみます。

  • ldflags を指定して go build -ldflags="-s -w" するパターン
  • ビルドしたバイナリに upx -9 を適用したパターン
  • その両方を適用したパターン

を比較したところ以下のようになりました。

default ldflags upx ldflags+upx
fmt 2.0M 1.6M 1.1M 620K
os 1.5M 1.1M 840K 448K
syscall 1.2M 915K 708K 376K

syscall が一番小さくなりましたが、それでも何も工夫しなければ1MBを超えています。 御膳立て部分のコードが大きそうですね。

ldflags と upx はそれぞれバイナリサイズを半分程度まで削減できるみたいです。 両方使うと元の大きさの 1/3 程度の小ささになりました。 まだ何をやっているツールか把握していませんが、upx すごいですね。

本当は各ライブラリの表現を部分的に引っ張ってきてより低級な表現に展開する、みたいなことをしたかったのですが、internal なパッケージをユーザ側で利用することができず諦めました。

おまけ

ディレクトリを跨いだファイルサイズの比較に tree -h が便利

tree -h でファイルサイズを表示しつつツリー表示してくれます。 --du オプションをつけると du 的にフォルダの合計サイズも表示してくれます。

% tree --du -h
.
├── [ 303]  build.sh
├── [5.3M]  fmt
│   ├── [2.0M]  hello-world
│   ├── [1.6M]  hello-world.ldflags
│   ├── [620K]  hello-world.ldflags.upx
│   ├── [1.1M]  hello-world.upx
│   └── [ 176]  main.go
├── [3.8M]  os
│   ├── [1.5M]  hello-world
│   ├── [1.1M]  hello-world.ldflags
│   ├── [448K]  hello-world.ldflags.upx
│   ├── [840K]  hello-world.upx
│   └── [  83]  main.go
└── [3.2M]  syscall
    ├── [1.2M]  hello-world
    ├── [915K]  hello-world.ldflags
    ├── [376K]  hello-world.ldflags.upx
    ├── [708K]  hello-world.upx
    └── [ 104]  main.go

  12M used in 3 directories, 16 files