続 C++、D言語、Go言語、Rustのハローワールドのバイナリサイズ

id:eel3:20150622:1434981139 の続きというかやり直しというか。

使用したソースファイルの内容や、ビルド時のコマンドなどについては、前回のエントリを参照してほしい。

前回の落穂拾い (1):Go言語で動的リンク

Go言語は、本家プロジェクトのコンパイラで普通にビルドすると、静的リンクの実行ファイルが生成される。

# go version go1.4.2 linux/386
go build hello.go
mv hello hello_bin/hello_go
$ ldd hello_go
	動的実行ファイルではありません
$ _

オプション -ldflags を使ってリンカにオプションを設定することで、動的リンクの実行ファイルを生成できるようだ。

# go version go1.4.2 linux/386
go build -ldflags '-linkmode external' hello.go
mv hello hello_bin/hello_go
$ ldd hello_go
	linux-gate.so.1 =>  (0xb774b000)
	libpthread.so.0 => /lib/i386-linux-gnu/libpthread.so.0 (0xb7719000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7570000)
	/lib/ld-linux.so.2 (0xb774c000)
$ _

実行ファイルの大きさを比較すると、こんな感じ。

link type not stripped stripped (strip -s)
静的リンク 1564840 1089912
動的リンク 1582347 1091428

……あれ? 動的リンクの方が微妙に大きいぞ?

前回の落穂拾い (2):Rustをより動的リンクな感じにビルドする

前回、Rustのコードを次のようなコマンドでビルドした。

# rustc 1.0.0 (a59de37e9 2015-05-13) (built 2015-05-14)
rustc -o hello_bin/hello_rust hello.rs

このビルドでは、動的リンクする対象は、システム(Linux)に用意されているごく普通のライブラリのみだった。

$ ldd hello_rust
	linux-gate.so.1 =>  (0xb7752000)
	libdl.so.2 => /lib/i386-linux-gnu/libdl.so.2 (0xb76e7000)
	libpthread.so.0 => /lib/i386-linux-gnu/libpthread.so.0 (0xb76cc000)
	librt.so.1 => /lib/i386-linux-gnu/librt.so.1 (0xb76c2000)
	libgcc_s.so.1 => /lib/i386-linux-gnu/libgcc_s.so.1 (0xb76a4000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb74fb000)
	/lib/ld-linux.so.2 (0xb7753000)
	libm.so.6 => /lib/i386-linux-gnu/libm.so.6 (0xb74cf000)

後で気づいたのだが、このビルド方法では、Rust本体にかかわるライブラリは静的リンクされ、実行ファイルに含まれている。

オプションとして prefer-dynamic を追加すると、Rustの言語本体のライブラリも動的リンクの対象となる。

# rustc 1.0.0 (a59de37e9 2015-05-13) (built 2015-05-14)
rustc -C prefer-dynamic -o hello_bin/hello_rust hello.rs

ldd(1)してみると、動的リンクの対象として libstd-4e7c5e5c.so が増えている。

$ ldd hello_rust
	linux-gate.so.1 =>  (0xb7721000)
	libstd-4e7c5e5c.so => /usr/local/rust/lib/libstd-4e7c5e5c.so (0xb735b000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb719e000)
	libdl.so.2 => /lib/i386-linux-gnu/libdl.so.2 (0xb7198000)
	libpthread.so.0 => /lib/i386-linux-gnu/libpthread.so.0 (0xb717d000)
	librt.so.1 => /lib/i386-linux-gnu/librt.so.1 (0xb7174000)
	libgcc_s.so.1 => /lib/i386-linux-gnu/libgcc_s.so.1 (0xb7156000)
	/lib/ld-linux.so.2 (0xb7722000)
	libm.so.6 => /lib/i386-linux-gnu/libm.so.6 (0xb712a000)
$ _

prefer-dynamic の有無で実行ファイルの大きさを比較すると、こんな感じ。

prefer-dynamic 有無 not stripped stripped (strip -s)
prefer-dynamic なし 553685 321648
prefer-dynamic あり 7593 5576

prefer-dynamic ありでnot strippedな状態での7,593 Byteという大きさは、gccで「動的リンク・最適化なし」でビルドしたC言語C++の実行ファイルと同じくらいだ。

前回の落穂拾い (3):MKCLの言語本体のライブラリを実行ファイルに含めてしまう

今度は、前項のRustとは正反対のケース。

前回、MKCLでは次のような感じでビルドしていた。

# MKCL 1.1.9
mkcl
# (compile-file
#  #P"./hello.lisp"
#  :output-file #P"./hello.o"
#  :fasl-p nil)
# (compiler::build-program
#  "hello_mkcl"
#  :lisp-object-files '(#P"./hello.o"))
# :exit
mv hello_mkcl hello_bin/

結果として、動的リンクするライブラリに mkcl_1.1.9.so が含まれていた。

$ ldd hello_mkcl
	linux-gate.so.1 =>  (0xb76e6000)
	mkcl_1.1.9.so => /usr/local/mkcl/lib/mkcl_1.1.9.so (0xb72da000)
	libpthread.so.0 => /lib/i386-linux-gnu/libpthread.so.0 (0xb72ab000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7101000)
	libgmp.so.10 => /usr/lib/i386-linux-gnu/libgmp.so.10 (0xb7082000)
	librt.so.1 => /lib/i386-linux-gnu/librt.so.1 (0xb7079000)
	libdl.so.2 => /lib/i386-linux-gnu/libdl.so.2 (0xb7074000)
	libm.so.6 => /lib/i386-linux-gnu/libm.so.6 (0xb7048000)
	/lib/ld-linux.so.2 (0xb76e7000)
$ _

次のようにビルドすると、 mkcl_1.1.9 のみ静的リンクされ、実行ファイルに含まれた状態となる。

# MKCL 1.1.9
mkcl
# (compile-file
#  #P"./hello.lisp"
#  :output-file #P"./hello.o"
#  :fasl-p nil)
# (compiler::build-program
#  "hello_mkcl"
#  :lisp-object-files '(#P"./hello.o")
#  :use-mkcl-shared-libraries nil)
# :exit
mv hello_mkcl hello_bin/

ldd(1)してみると、動的リンクの対象から mkcl_1.1.9.so が消えている。

$ ldd hello_mkcl
	linux-gate.so.1 =>  (0xb770c000)
	libgmp.so.10 => /usr/lib/i386-linux-gnu/libgmp.so.10 (0xb7677000)
	librt.so.1 => /lib/i386-linux-gnu/librt.so.1 (0xb766e000)
	libdl.so.2 => /lib/i386-linux-gnu/libdl.so.2 (0xb7668000)
	libm.so.6 => /lib/i386-linux-gnu/libm.so.6 (0xb763c000)
	libpthread.so.0 => /lib/i386-linux-gnu/libpthread.so.0 (0xb7621000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7478000)
	/lib/ld-linux.so.2 (0xb770d000)
$ _

実行ファイルの大きさを比較すると、こんな感じ。

link type not stripped stripped (strip -s)
mkcl_1.1.9 を動的リンク 57330 9760
mkcl_1.1.9 を静的リンク 11922144 3470684

mkcl_1.1.9 を静的リンクすると、一気にでかくなりますな。strip前が11.9 MByte、strip -sの後でも3.3 MByte。うーん……。

計測結果:動的リンクの場合

気を取り直して、ビルドした実行ファイルの大きさをまとめてみた。

まずは、最適化無しで動的リンクでビルドした場合の大きさ。

言語 ファイル名 大きさ(byte)
C言語 hello_c 7159
C++ hello_cpp 7754
D言語 hello_d 562112
Go言語 hello_go 1582347
Rust (prefer-dynamic あり) hello_rust 7593
Rust (prefer-dynamic なし) hello_rust 553685
OCaml hello_ocaml 142862
Common Lisp (ECL) hello_ecl 36667
Common Lisp (MKCL:mkcl_1.1.9 を動的リンク) hello_mkcl 57330
Common Lisp (MKCL:mkcl_1.1.9 を静的リンク) hello_mkcl 11922144

MKCL(mkcl_1.1.9 静的リンク版)が最も大きいというか、このでかさはなんなんでしょう?

次に、strip(1)でシンボルを削った状態での大きさ。strip -sで全てのシンボルを削除している。

言語 ファイル名 大きさ(byte)
C言語 hello_c 5516
C++ hello_cpp 5588
D言語 hello_d 360500
Go言語 hello_go 1091428
Rust (prefer-dynamic あり) hello_rust 5576
Rust (prefer-dynamic なし) hello_rust 321648
OCaml hello_ocaml 105080
Common Lisp (ECL) hello_ecl 5616
Common Lisp (MKCL:mkcl_1.1.9 を動的リンク) hello_mkcl 9760
Common Lisp (MKCL:mkcl_1.1.9 を静的リンク) hello_mkcl 3470684

どれも小さくなっている。

Go言語が1.5 MByteから1.0 MByteと0.5 Mbyteも削れたが、それでもやはり大きい。ECLはC言語C++並みの大きさになった。MKCL(mkcl_1.1.9 静的リンク版)は元の3分の1弱になる脅威の削減率を示したが、それでも3.3 MByteである。

計測結果:静的リンクの場合

次に、静的リンクでビルドした実行ファイルの大きさをまとめてみた。

残念ながら肝心のD言語とRustで静的リンクの実行ファイルを作れなかったので除外している(情報求ム)。ECLは、静的リンクで実行ファイルを作るには処理系をビルドし直す必要があったので、面倒なので止めた。

まずは、ビルド時のコマンド。

# gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
gcc -Wall -ansi -pedantic -static -o hello_bin/hello_c hello.c
# g++ (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
g++ -Wall -ansi -pedantic -static -o hello_bin/hello_cpp hello.cpp
# go version go1.4.2 linux/386
go build hello.go
mv hello hello_bin/hello_go
# The Objective Caml native-code compiler, version 3.12.1
ocamlopt -ccopt -static hello.ml
mv a.out hello_bin/hello_ocaml
# MKCL 1.1.9
mkcl
# (compile-file
#  #P"./hello.lisp"
#  :output-file #P"./hello.o"
#  :fasl-p nil)
# (compiler::build-program
#  "hello_mkcl"
#  :lisp-object-files '(#P"./hello.o")
#  :use-mkcl-shared-libraries nil
#  :extra-ld-flags "-static")
# :exit
mv hello_mkcl hello_bin/

最適化無しで静的リンクでビルドした場合の大きさ。

言語 ファイル名 大きさ(byte)
C言語 hello_c 751138
C++ hello_cpp 1441695
Go言語 hello_go 1564840
OCaml hello_ocaml 912359
Common Lisp (MKCL) hello_mkcl 13119102

Go言語以外、軒並み動的リンクの時よりも大きくなっている。

最小はC言語C++が1.4 MByte弱と、Go言語に肉薄する大きさとなった。OCamlC++より小さく、C言語に次ぐ2番目の大きさ。最大はMKCLの12.5 MByte。

次に、strip(1)でシンボルを削った状態での大きさ。strip -sで全てのシンボルを削除している。

言語 ファイル名 大きさ(byte)
C言語 hello_c 684780
C++ hello_cpp 1192864
Go言語 hello_go 1089912
OCaml hello_ocaml 808560
Common Lisp (MKCL) hello_mkcl 4341672

最小のC言語、2番手のOCamlと、順序は変わらず。C++がGo言語よりもわずかに大きくなった結果、3〜4番手の順序が逆転。最大がMKCLなのは変わらないが、12.5 MByteから4.2 MByte弱へと、大幅なダイエットに成功している。

まとめ(という名の感想再び)

Go言語で動的リンクでビルドしても実行ファイルが小さくならない(それどころか微増する)のは衝撃的。おそらく言語本体や標準ライブラリの機能が丸ごと静的リンク状態で、現状ではどう頑張ってもその辺のライブラリが動的リンクにならないからだろう。この辺は、やはりgoogoの今後に期待、かなあ。

C++で静的リンクしたら、Go言語といい勝負な感じにファイルサイズが膨れ上がった。D言語やRustも静的リンクでビルドして、C++やGo言語と有意な差があるか見たかったが、結局できなかったのが残念。

OCamlはトップでも最下位でもなく、常に「中の上」に位置する謎の安定感があった。意外だが、実行ファイルの大きさという点では、もしかしたらD言語・Go言語・Rustよりマシかもしれない。

そしてMKCLに発覚した隠れ肥満疑惑。でもCommon Lispの処理系の中では小さいほうか?

動的リンク・静的リンクともに、当該言語のランタイムの大きさがキモなようだ。Rustのように、言語のランタイムまで動的リンクにするとCやC++相当の大きさになるケースや、MKCLのように言語のランタイムをリンクしたら激増したケースなど。判断がなかなか難しいなあ。