よく使うGoのBuildオプションと、UPXの圧縮がバイナリサイズにどの程度影響しているか調べた

# はじめに

GoのBuildオプションでサイズに影響しそうなオプションで、どの程度サイズに差がでるか試しました。

以下の環境です。自分のよくあるGoのユースケースとしてWebAPI開発あるので、go-swaggerやAWS SDK Goに依存したプロジェクトで試してみました。

```bash
$cat /etc/os-release | head -n2
NAME="Ubuntu"
VERSION="18.04.4 LTS (Bionic Beaver)"

$ go version
go version go1.14.2 linux/amd64
```


# 比較

```bash
# 1. ノーオプション
go build -o nooption main.go

# 2. ldflagsでdebug情報を削除
go build -ldflags="-s -w" -o ldflags main.go

# 3. trimpathでパス情報を削除
go build -ldflags="-s -w" -trimpath -o trimpath main.go
```

* `-ldflags` 
    * [Go Document](https://golang.org/cmd/go/#hdr-Compile_packages_and_dependencies)によるとリンカーへの引数。`-s`, `-w` はそれぞれシンボルテーブルとデバック情報とのこと。
        * 参考: https://shinpei.github.io/blog/2014/12/24/make-go-binary-size-small
* `-trimpath` 
    * Go1.13から追加されたオプション。バイナリから全ファイルシステムパスを削除する
    * panic で表示されるパスからBuild時のディレクトリ構造を消すためにも指定

# UPX

https://upx.github.io/

[Goのバイナリサイズを削減する](http://tdoc.info/blog/2016/03/01/go_diet.html) で初めて知ったのですが、UPXは実行形式を保ったまま、バイナリを圧縮するツールとのことです。（ライセンスは記事にかかれている通り、GPLに例外事項をつけたライセンスですので、本番環境への利用時は念のため確認してください）

基本的には、UPXはバイナリを圧縮するツールですが、[実行ファイル圧縮](https://ja.wikipedia.org/wiki/%E5%AE%9F%E8%A1%8C%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%9C%A7%E7%B8%AE) 形式で行ってくれるらしく、元のバイナリの圧縮＋自己展開のバイナリも付与されるモデルだそうです。そのため、バイナリサイズが小さくなるトレードオフとして、（どの程度かはさておき）起動時に圧縮されたバイナリを展開するため起動速度は遅くなるようです。一度起動してしまえば後は性能差は無いと思います。

UbuntuにUPXをインストールします。

```bash:インストール
$sudo apt-get update -y
$sudo apt-get install -y upx

# Versionの確認
$upx --version | head -4
upx 3.94
UCL data compression library 1.03
zlib data compression library 1.2.11
LZMA SDK version 4.43
```

UPXを実行してみます。

https://linux.die.net/man/1/upx によると、-1から-9まで圧縮レベルを指定できるそうです。 `--best` で最善を尽くしてくれるそうです。ちなみにデフォルトは -8 だそうです。


```bash
upx -1     -o upx1     trimpath
upx -8     -o upx8     trimpath
upx -9     -o upx9     trimpath
upx --best -o upx_best trimpath
```

`-8` より上のレベルはかなりパックに時間がかかったので、リモートサーバならともかく、ローカル環境で毎Build時にやることはオススメしないなって思いました。特に`--best` は数分はかかった気がします。


`--lzma` で [LZMA(Lempel-Ziv-Markov chain-Algorithm)](https://ja.wikipedia.org/wiki/Lempel-Ziv-Markov_chain-Algorithm) を有効にできるとのこと。

```bash:LZMA利用
upx --lzma -o upx_lzma trimpath 
```

`--brute`, `--ultra-brute`のオプションもあるようなので試してみます。`brute` は `man` で確認すると..

> Compression tuning options:
>   --brute             try all available compression methods & filters [slow]
>   --ultra-brute       try even more compression variants [very slow]

...とのことで、良い圧縮率が期待できそうです。

```bash:brute利用
upx --brute -o brute trimpath
upx --ultra-brute -o ultra_brute trimpath
```

これも `--best` と同様にかなりパック時間がかかりました。


# サイズ

| No | Name                | Size [KB] | 1と比べたときのサイズ |
|----|---------------------|-----------|--------|
| 1  | nooption            | 22,501    | 100%   |
| 2  | ldflags             | 16,996    | 75.5%  |
| 3  | trimpath            | 16,968    | 75.4%  |
| 4  | 3 + UPX 1           |  6,622    | 29.4%  |
| 5  | 3 + UPX 8           |  5,635    | 25.0%  |
| 6  | 3 + UPX 9           |  5,564    | 24.7%  |
| 7  | 3 + UPX BEST        |  5,522    | 24.5%  |
| 8  | 3 + UPX LZMA        |  4232     | 18.9%  |
| 9  | 3 + UPX BRUTE       |  4218     | 18.7%  |
| 10 | 3 + UPX ULTRA BRUTE |  4198     | 18.7%  |

2,3はほぼ変わらないですが、ファイルシステムパスを削除した分、多少サイズが減っています。（30KBなので小さいシステムだとインパクト大きいかもしれません）

4以降のUPXが劇的過ぎてビビるですが、試しにパックしたバイナリを実行してみるともちろん普通に起動しました。パックするにはオプションによってそこそこ処理時間がかかります。圧縮率と処理時間のバランスで、UPXを使うのであれば `--lzma` が1番バランスが良さそうだなと思いました。 BRUTEは時間がかかるので。


# 再現性のあるBuildをするためには

https://iguchitomokatsu.com/posts/how-to-make-static-binary-of-golang/
を読んで知ったのですが、 `-ldflags=-buildid=` を足すと完全に[再現性のあるバイナリが生成できる](https://github.com/golang/go/issues/34186#issuecomment-529390306)そうです。 

```bash:build例
go build -ldflags="-s -w -ldflags=-buildid=" -trimpath -o trimpath main.go
```


# まとめ

* ライブラリ提供でなく実行バイナリをビルドするのであれば、`-ldflags="-s -w"` や `-trimpath` をつけるのがベターで、そこそこ依存関係があるPJでも25%ほどバイナリサイズを小さくすることができた
* UPXの効果が素晴らしい