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

タイトルに若干偽りあり。C言語Common Lisp(ECLとMKCL)・OCamlも含んでいる。

一応、組み込みっぽい業界にいる人として、C言語の次のシステム・プログラミング向けの言語は興味の対象だ。

もちろん、今すぐ急に別の言語に移行するような事態は、可能性として低いだろう。アセンブラ全盛期から長い時間をかけてC言語や「C言語とインライン・アセンブラの混合」に変化したように、(今回も同じ程度の期間を経るか否か定かではないものの)それなりに時間がかかると思っている。

ついでにいうなら、今でもアセンブラを用いる部分が残っているのと同様に、次のシステム・プログラミング言語が主流になったらC言語が絶滅するとは思っていない。明らかにハードウェアやOSのシステムコールに近い階層では、C言語が生き残るだろう。一方で、組み込みシステムの上位階層側では、現在でもC言語からC++への移行が見られるように、次世代の言語に移行していくのではないかと思っている。

というか、PCやらスマホタブレットの世界の流儀が広まっている影響で色々とリッチなシステムを求められている影響で、抽象度の高い領域で問題をとらえて解を探らなくてはいけないのに、同時にものすごく具象寄りの問題と付き合わなくてはならないC言語縛りだなんて、色々と辛すぎる。自分、Mじゃないっすから。

組み込みといっても分野は広いが、「次世代の言語への移行」という点では、ソフトリアルタイムかつシステムリソースがそこそこ潤沢で、新しい言語が生まれやすいLinuxの近縁である組み込みLinuxBSD系のOSを採用しているシステムあたりから採用が始まる可能性が高いと思っている。人命に関わらないガジェット寄りで、商品寿命が短めのコンシューマ向け。あとIoTではないがネットワークに繋がっている。

「リッチなシステム・ネットワーク対応・商品寿命が短め」の三拍子が揃った時点で、積極的にサードパーティのソフトウェアを採用せざるをえない。でないと開発が間に合わない。発売後にセキュリティホールやらサードパーティのソフトウェアのバグやらに迅速に対応する必要が生じる結果、ソフトをカリカリにチューニングして非力なハードウェアで動かすような調整はできない。下手にチューニングに成功してしまうと、不具合対応のアップデートにて、調整済みバランスが崩れてしまう。それよりは、ハードウェア側に若干の余裕を持たせる方が安全だ。

マチュア向けでいうなら、ArduinoではなくRaspberry PiBeagleBoard/BeagleBone寄りだろうか。

次世代の言語を評価するポイントとしては、実行速度以外にフットプリントが挙げられる。バイナリサイズが大きいと外部記憶(例えば内蔵フラッシュ)を占有するし、メモリも占有するし、実行時のロード時間も長くなる。

フットプリントの面で、スクリプト言語Java/.NETのような言語は不利だ。処理系やライブラリは外部記憶をそれなりに占有する。その面では、ネイティブコードを吐き出すコンパイラ型言語の方がよい。

そんな訳で、C言語C++と、個人的に最近気になっている言語とで、バイナリサイズを比較してみることにした。よいお題が無かったので、ハローワールドだ。

検証対象のソースコード

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	(void) puts("hello, world");

	return EXIT_SUCCESS;
}
C++
#include <iostream>

int main()
{
	std::cout << "hello, world" << std::endl;
}
D言語
import std.stdio;

void main()
{
	writeln("hello, world");
}
Go言語
package main

import "fmt"

func main() {
	fmt.Println("hello, world")
}
Rust
fn main() {
	println!("hello, world");
}
OCaml
print_string "hello, world\n"
Common Lisp
(format t "hello, world~%")
(quit)

検証環境・コンパイラ

本当はARMで検証したかったのだが、諸事情でx86上のUbuntu 12.04.5 LTS 32bit上で確認した。

使用したコンパイラとビルド方法は次の通り。

# gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
# Install from Ubuntu default repository
gcc -Wall -ansi -pedantic -o hello_bin/hello_c hello.c
# g++ (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
# Install from Ubuntu default repository
g++ -Wall -ansi -pedantic -o hello_bin/hello_cpp hello.cpp 
# DMD32 D Compiler v2.067.1
# http://downloads.dlang.org/releases/2.x/2.067.1/dmd.2.067.1.linux.zip
dmd -odhello_bin -ofhello_d hello.d 
# go version go1.4.2 linux/386
# https://storage.googleapis.com/golang/go1.4.2.linux-386.tar.gz
go build hello.go
mv hello hello_bin/hello_go
# rustc 1.0.0 (a59de37e9 2015-05-13) (built 2015-05-14)
# https://static.rust-lang.org/dist/rust-1.0.0-i686-unknown-linux-gnu.tar.gz
rustc -o hello_bin/hello_rust hello.rs 
# The Objective Caml native-code compiler, version 3.12.1
# Install from Ubuntu default repository
ocamlopt hello.ml
mv a.out hello_bin/hello_ocaml
# ECL 15.2.21
# http://sourceforge.net/projects/ecls/files/ecls/15.2/ecl-15.2.21.tgz/download
ecl
# (compile-file "hello.lisp" :system-p t)
# (c:build-program "hello_ecl" :lisp-files '("hello.o"))
# :exit
mv hello_ecl hello_bin/
# MKCL 1.1.9
# http://common-lisp.net/project/mkcl/releases/mkcl-1.1.9.tar.gz
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/

C言語C++は、組み込みで使われることもなくはないGCCを使用。D言語・Go言語・Rustは各プロジェクトの公式のコンパイラを選択。OCamlもプロジェクト公式のコンパイラと言えなくもないが、Ubuntuリポジトリのものを使用したので、バージョンは古めだと思う。

Common Lisp枠からはECL(Embeddable Common-Lisp)とMKCL(ManKai Common Lisp)を選択。理由は……両者ともKCLやGCLの系譜だけあって実行ファイルが作りやすいから。両者とも、Common Lispのコードを直接コンパイルしているのではなく、C言語に変換してからコンパイルしている(はず)。

計測結果

ビルドした実行ファイルの大きさは次の通り。

言語 ファイル名 大きさ(byte)
C言語 hello_c 7159
C++ hello_cpp 7754
D言語 hello_d 562112
Go言語 hello_go 1564840
Rust hello_rust 553685
OCaml hello_ocaml 142862
Common Lisp (ECL) hello_ecl 36667
Common Lisp (MKCL) hello_mkcl 57330

CやC++はダントツに小さい。C++は、標準ライブラリの機能をもっと使うようなプログラムでは、また結果が変わってくる気がする。

続いてECL/MKCL。C言語に変換してからコンパイルしているから、この大きさだろうか?

その次はOCaml。この時点で、C言語C++の20倍弱。

D言語とRustは、奇しくもほぼ同じくらいの大きさとなった。

そして最も大きかったのはGo言語。3.5インチ2HDのフロッピディスク(1.44MB)に収まりきらないハローワールドとは、隔世の感がある。

もっとも、Go言語のバイナリの大きさの原因は、次の実行結果を見れば分かる(かもしれない)。

$ file hello_*
hello_c:     ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0xf4b60d24e85b520638ccfb0f12c99375aa53a849, not stripped
hello_cpp:   ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x8d8cd721cadc2c5fbf6a1dbf415af891cc9d6f40, not stripped
hello_d:     ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x2335b3e557cf714c121bac13513c9264d3cf6d3c, not stripped
hello_ecl:   ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x2f0ed7a2b925f68e9225b4848980b4df676612a1, not stripped
hello_go:    ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped
hello_mkcl:  ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x9479becd387a27bf1e8fc65376b3a3ad60f32edb, not stripped
hello_ocaml: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x093f42fd42a0b08156ad4868adfa2734f2b59bb6, not stripped
hello_rust:  ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0xf87292aa16c1751b0933947847042322318900f8, not stripped
$ _

Go言語はライブラリを静的リンクしている。一方、他の言語は動的リンクしている。時間ができたら、可能ならGo言語以外でも静的リンクでビルドして比較したいところだ。

ところで、何でRustのバイナリは実行ファイルではなく共有ライブラリなのだろう? Rustのコンパイラ自体も共有ライブラリだった。

ついでに、ldd(1)の結果も残しておく。

$ ldd hello_*
hello_c:
	linux-gate.so.1 =>  (0xb77d0000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7611000)
	/lib/ld-linux.so.2 (0xb77d1000)
hello_cpp:
	linux-gate.so.1 =>  (0xb778e000)
	libstdc++.so.6 => /usr/lib/i386-linux-gnu/libstdc++.so.6 (0xb7693000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb74ea000)
	libm.so.6 => /lib/i386-linux-gnu/libm.so.6 (0xb74bd000)
	/lib/ld-linux.so.2 (0xb778f000)
	libgcc_s.so.1 => /lib/i386-linux-gnu/libgcc_s.so.1 (0xb749f000)
hello_d:
	linux-gate.so.1 =>  (0xb7743000)
	libpthread.so.0 => /lib/i386-linux-gnu/libpthread.so.0 (0xb7712000)
	librt.so.1 => /lib/i386-linux-gnu/librt.so.1 (0xb7709000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb755f000)
	/lib/ld-linux.so.2 (0xb7744000)
hello_ecl:
	linux-gate.so.1 =>  (0xb7769000)
	libecl.so.15.2 => not found
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75aa000)
	/lib/ld-linux.so.2 (0xb776a000)
hello_go:
	動的実行ファイルではありません
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)
hello_ocaml:
	linux-gate.so.1 =>  (0xb771e000)
	libm.so.6 => /lib/i386-linux-gnu/libm.so.6 (0xb76dc000)
	libdl.so.2 => /lib/i386-linux-gnu/libdl.so.2 (0xb76d7000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb752d000)
	/lib/ld-linux.so.2 (0xb771f000)
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)
$ _

ECL/MKCLを除けば、さして特別なライブラリとはリンクしていないように見える。D言語やRustにて、さりげなくlibpthreadやlibrtとリンクしている点が興味深い。標準ライブラリ相当の機能を多く使用した場合の結果がどうなるのか、気になるところだ。

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

D言語、Go言語、Rustのバイナリは結構大きかった。まあ言語だけでなく処理系(コンパイラ)もまだまだ若いから、「今後に期待」というところだろうか。

C++は、より実務に近いだろう「標準ライブラリの機能をもっと使った場合」にどうなるのか気になるところだ(ハローワールドのみ、というのは少々フェアではない)。

OCamlやECL/MKCLは予想よりも小さなバイナリだった。ちょっと驚いている。

ECL/MKCLはいったんC言語に変換してからコンパイルしている。この点がバイナリの意外な小ささの原因であるならば、Go言語を標準のコンパイラではなくgccgoで十二分に使えるようになったとき*1、バイナリの大きさの問題は解決される気がする。

id:eel3:20150627:1435402293 に続く)

*1:gccgoの実装は若干遅れ気味で、GCC 4.9の時点でGo 1.2相当。マンパワー等々の問題だとはいえ、ユーザとしてやや気になるところではある。