Goのコンパイル済み実行可能ファイルの巨大なサイズの理由


90

Linuxマシンでネイティブ実行可能ファイルを生成するHelloWorldGoプログラムをコンパイルしました。しかし、単純なHello worldGoプログラムのサイズが1.9MBであることに驚きました。

Goでこのような単純なプログラムの実行可能ファイルが非常に大きいのはなぜですか?


22
巨大な?その時はJavaをあまりやらないと思います!
リック-777

19
さて、C / C ++のバックグラウンドからのim!
Karthic Rao 2015

私はこのscala- nativehello worldを試しましたscala-native.org/en/latest/user/sbt.html#minimal-sbt-projectコンパイルしてたくさんのものをダウンロードするのにかなりの時間がかかり、バイナリは3.9ですMB。
bli 2017年

以下の回答を2019年の調査結果で更新しました。
VonC

1
C#.NET dotnet publish -r win-x64 -p:publishsinglefile=true -p:publishreadytorun=true -p:publishtrimmed=trueCore3.1のシンプルなHelloWorldアプリは、約26MBのバイナリファイルを生成します。
ジャラル

回答:


90

この正確な質問は公式FAQに表示されます:なぜ私の些細なプログラムがこんなに大きなバイナリなのですか?

答えを引用する:

GCツールチェーンのリンカー(5l6l、とは8l)静的リンクを行います。したがって、すべてのGoバイナリには、動的型チェック、リフレクション、さらにはパニック時のスタックトレースをサポートするために必要な実行時型情報とともに、Goランタイムが含まれています。

Linuxでgccを使用して静的にコンパイルおよびリンクされた単純なCの「hello、world」プログラムは、の実装を含めて約750kBですprintf。使用する同等のGoプログラムfmt.Printfは約1.9MBですが、これにはより強力なランタイムサポートとタイプ情報が含まれています。

つまり、HelloWorldのネイティブ実行可能ファイルは1.9MBです。これは、ガベージコレクション、リフレクション、およびその他の多くの機能を提供するランタイムが含まれているためです(プログラムでは実際には使用されないかもしれませんが、そこにあります)。そして、テキスト(およびその依存関係)fmtを印刷するために使用したパッケージの実装"Hello World"

次に、次のことを試しfmt.Println("Hello World! Again")てください。プログラムに別の行を追加して、再度コンパイルします。結果は2x1.9MBにはなりませんが、それでも1.9MBになります。はい。使用されているすべてのライブラリ(fmtおよびその依存関係)とランタイムが実行可能ファイルにすでに追加されているためです(したがって、追加した2番目のテキストを出力するためにさらに数バイトが追加されます)。


10
glibcが静的リンク用に明示的に設計されておらず、場合によっては適切に静的リンクすることさえ不可能であるため、glibcと静的にリンクされたAC「helloworld」プログラムは750Kです。musllibcと静的にリンクされた「helloworld」プログラムは14Kです。
クレイグバーンズ

私はまだ探していますが、攻撃者が邪悪なコードにリンクしていないように、何がリンクされているかを知っておくと便利です。
リチャード

では、GoランタイムライブラリがDLLファイルに含まれていないのはなぜですか。そうすれば、すべてのGoexeファイル間で共有できます。その場合、「hello world」プログラムは、予想どおり、2MBではなく数KBになる可能性があります。すべてのプログラムにランタイムライブラリ全体があることは、Windows上のMSVCに代わる素晴らしい代替手段としては致命的な欠陥です。
デビッドスペクター

Goは「静的にリンクされている」という私のコメントに対する反対意見を予想したほうがよいでしょう。さて、DLLはありません。ただし、静的リンクは、ライブラリ全体をリンク(バインド)する必要があるという意味ではなく、ライブラリで実際に使用されている関数のみをリンクする必要があります。
デビッドスペクター

43

次のプログラムを検討してください。

package main

import "fmt"

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

Linux AMD64マシン(Go 1.9)でこれをビルドすると、次のようになります。

$ go build
$ ls -la helloworld
-rwxr-xr-x 1 janf group 2029206 Sep 11 16:58 helloworld

サイズが約2Mbのバイナリを取得します。

この理由(他の回答で説明されています)は、非常に大きい「fmt」パッケージを使用しているが、バイナリも削除されておらず、シンボルテーブルがまだ存在していることを意味します。代わりに、バイナリを削除するようコンパイラに指示すると、バイナリははるかに小さくなります。

$ go build -ldflags "-s -w"
$ ls -la helloworld
-rwxr-xr-x 1 janf group 1323616 Sep 11 17:01 helloworld

ただし、次のように、fmt.Printlnではなく組み込み関数printを使用するようにプログラムを書き直すと、次のようになります。

package main

func main() {
    print("Hello World!\n")
}

そしてそれをコンパイルします:

$ go build -ldflags "-s -w"
$ ls -la helloworld
-rwxr-xr-x 1 janf group 714176 Sep 11 17:06 helloworld

最終的にはさらに小さなバイナリになります。これは、UPXパッキングのようなトリックに頼らずに取得できる限り小さいため、Goランタイムのオーバーヘッドは約700Kbです。


3
UPXは、バイナリを圧縮し、実行時にオンザフライで解凍します。いくつかのシナリオで役立つ可能性があるため、その機能を説明せずにトリックを却下することはありません。バイナリサイズは、起動時間とRAM使用量を犠牲にしていくらか減少します。さらに、パフォーマンスもわずかに影響を受ける可能性があります。例として、実行可能ファイルは(削除された)サイズの30%に縮小され、実行に35ミリ秒長くかかる可能性があります。
simlev

10

バイナリサイズの問題は、golang / goプロジェクトの問題6853によって追跡されていることに注意してください。

たとえば、コミットa26c01a(Go 1.4の場合)はhelloworldを70kBカットします

これらの名前をシンボルテーブルに書き込まないためです。

1.5のコンパイラ、アセンブラ、リンカ、およびランタイムが完全にGoに含まれることを考えると、さらなる最適化が期待できます。


2016 Go 1.7の更新:これは最適化されています:「 SmallerGo1.7バイナリ」を。

しかし、最近(2019年4月)、最も行われているのはruntime.pclntabです。
「を参照してくださいゴーの大?サイズの可視化がD3を使用して実行可能ファイル、なぜ私のゴー実行可能ファイルですから」ラファエル「kena」ポス

あまり文書化されていませんが、Goソースコードからのこのコメントはその目的を示唆しています。

// A LineTable is a data structure mapping program counters to line numbers.

このデータ構造の目的は、Goランタイムシステムがクラッシュ時またはruntime.GetStackAPIを介した内部リクエスト時に記述的なスタックトレースを生成できるようにすることです。

だからそれは便利なようです。しかし、なぜそれがそんなに大きいのですか?

URL https://golang.org/s/go12symtabゴー1.0と1.2の間で何が起こったのかを説明した文書にaforelinkedソースファイルのリダイレクトに隠されました。言い換えると:

1.2より前は、Goリンカは圧縮された行テーブルを発行していましたが、プログラムは実行時の初期化時にそれを解凍していました。

Go 1.2では、実行可能ファイルの行テーブルを、追加の解凍手順なしで、実行時に直接使用するのに適した最終形式に事前拡張することが決定されました。

つまり、Goチームは、初期化時間を節約するために、実行可能ファイルを大きくすることにしました。

また、データ構造を見ると、コンパイルされたバイナリの全体的なサイズは、各関数の大きさに加えて、プログラム内の関数の数において超線形であるように見えます。

https://science.raphael.poss.name/go-executable-size-visualization-with-d3/size-demo-ss.png


2
彼の実装言語がそれと何の関係があるのか​​わかりません。共有ライブラリを使用する必要があります。彼らがこの時代にまだいないことはやや信じられないほどです。
ローン侯爵2017

2
@EJP:なぜ彼らは共有ライブラリを使用する必要があるのですか?
Flimzy 2017

9
@ EJP、Goのシンプルさの一部は、共有ライブラリを使用しないことです。実際、Goには依存関係がまったくなく、プレーンなシステムコールを使用しています。単一のバイナリをデプロイするだけで、それは機能します。そうでなければ、それは言語とその生態系を著しく傷つけるでしょう。
creker 2017

10
静的にリンクされたバイナリを持つことの忘れられがちな側面は、完全に空のDockerコンテナでそれらを実行できることです。セキュリティの観点から、これは理想的です。コンテナが空の場合、侵入できる可能性がありますが(静的にリンクされたバイナリに欠陥がある場合)、コンテナに何も見つからないため、攻撃はそこで停止します。
Joppe 2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.