小さくなったGo 1.7のバイナリ

Smaller Go 1.7 binaries by David Crawshaw

はじめに

Goはサーバーを書くために設計されました。現在最も広く使われているのもサーバー用途であり、 その結果、ランタイムやコンパイラでの作業の多くは、レイテンシ、デプロイのしやすさ、正確な ガベージコレクション、高速な起動時間、パフォーマンスといった、サーバーにとって重要な 問題に焦点が当てられています。

Goがより幅広い種類のプログラムに使われるようになるにつれて、考慮すべき新しい問題が 出てきました。そのひとつがバイナリサイズです。これは長い間注目されてきた問題であり (issue #6853 は2年以上前に登録されました)、 Raspberry Piやモバイルデバイスのような小型デバイスへバイナリをデプロイすることに対する 関心の高まりによって、Go 1.7のリリースで注目を集めることになりました。

Go 1.7でなされた作業

Go 1.7には、バイナリサイズに影響を与える3つの重要な変更があります。

1つ目は、このリリースでAMD64向けに有効化された新しいSSAバックエンドです。SSAの主な狙いは パフォーマンスの向上でしたが、生成されるコードがより良くなったことでサイズも小さくなりました。 SSAバックエンドによってGoバイナリは約5%縮小します。ARMやMIPSといったよりRISCライクな アーキテクチャでは、Go 1.8でバックエンドがSSAに変換された際に、より大きな効果が得られると 見込んでいます。

2つ目の変更はメソッドの刈り込み(pruning)です。1.6までは、使用されているすべての型の すべてのメソッドが、一部のメソッドが一度も呼び出されていなくても保持されていました。これは、 それらがインターフェース経由で呼び出されたり、reflectパッケージを使って動的に呼び出されたり する可能性があるためです。現在では、コンパイラはインターフェースにマッチしないエクスポートされていない メソッドをすべて破棄します。同様に、リンカは、対応する reflectionの機能がプログラム内のどこにも 使われていない場合、reflectionを通してのみアクセス可能な、エクスポートされた他のメソッドを 破棄できます。この変更によってバイナリは5〜20%縮小します。

3つ目の変更は、reflectパッケージが使用する実行時型情報(run-time type information)の よりコンパクトなフォーマットです。このエンコーディング形式は、もともとランタイムと reflectパッケージ内のデコーダをできるだけシンプルにするために設計されたものでした。 このコードを少し読みにくくすることと引き換えに、Goプログラムの実行時のパフォーマンスに 影響を与えることなくフォーマットを圧縮できます。新しいフォーマットによってGoバイナリは さらに5〜15%縮小します。Android向けにビルドされたライブラリやiOS向けにビルドされた アーカイブは、新しいフォーマットに含まれるポインタの数が減るため、さらに縮小します。 位置独立コード(position independent code)ではポインタそれぞれに動的なリロケーションが 必要になるからです。

これに加えて、インターフェースのデータレイアウトの改善、静的データレイアウトの改善、 依存関係の単純化など、多くの小さな改善がありました。たとえば、HTTPクライアントはもはや HTTPサーバー全体をリンクしなくなりました。変更点の全リストはissue #6853 にあります。

結果

おもちゃのような小さなプログラムから大規模な本番プログラムまで、典型的なプログラムは Go 1.7でビルドすると約30%小さくなります。

定番のHello Worldプログラムは2.3MBから1.6MBになります。

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

デバッグ情報なしでコンパイルすると、静的リンクされたバイナリは1メガバイト未満になります。

このリリースサイクルのテストに使われた大規模な本番プログラムである jujud は、94MBから 67MBになりました。

位置独立バイナリ(PIE)は50%小さくなります。

位置独立実行可能ファイル(PIE)では、読み取り専用データセクション内のポインタには動的な リロケーションが必要です。型情報の新しいフォーマットはポインタをセクションオフセットに 置き換えるため、ポインタ1つあたり28バイト節約できます。

デバッグ情報を取り除いた位置独立実行可能ファイルは、モバイル開発者にとって特に重要です。 これは携帯電話に配布される類のプログラムだからです。大きなダウンロードはユーザー体験を 損なうので、ここでの削減は朗報です。

今後の作業

実行時型情報に対するいくつかの変更は、Go 1.7のフリーズには間に合いませんでしたが、 うまくいけばGo 1.8に入り、特に位置独立なプログラムをさらに縮小することになるでしょう。

これらの変更はいずれも保守的なもので、ビルド時間、起動時間、全体の実行時間、メモリ使用量を 増やすことなくバイナリサイズを削減します。バイナリサイズを削減するためのもっと過激な手段を 取ることもできます。実行可能ファイルを圧縮する upx ツールは バイナリをさらに50%縮小しますが、その代償として起動時間が増加し、メモリ使用量が増える 可能性があります。極端に小さなシステム(キーホルダーに収まるようなもの)向けには、 reflectionを持たないバージョンのGoをビルドすることもできますが、そのように制限された 言語が十分に有用かどうかは不明です。ランタイム内の一部のアルゴリズムについては、 1キロバイトが重要になる場面では、より低速だがよりコンパクトな実装を使うこともできる でしょう。これらはすべて、今後の開発サイクルでのさらなる研究を必要とします。

Go 1.7のバイナリを小さくする手助けをしてくれた多くのコントリビューターに感謝します!

By David Crawshaw