小さいEXEを作りたい!!

今回のタイトルは小さいEXEを作りたい!!(リンク先はWebアーカイブ)をリスペクトさせて頂きました。

GCC Runtime Library Exception

聞いてくれ!
ついにマイハイパーユーティリティが完成したぞ!

ほほう、それはめでたい。
最後まで作り上げるのは大したものですよ、本当に。
私は途中まで作って放置してるのが大量に……。

えっへん。
もっと褒めてくれたまえ。

あんたが大将!日本一!

……なんか適当な褒め方だなあ。
ところでさ、折角だからフリーソフトとして公開したいんだ。

それはいいですね。
日本のフリーウェア文化も微妙に下火ですし。
それから、gccのランタイムは例外条項がありますから、GPLにしなくてもOKですよ。

いや、最初からGPLにする気は無いんだけど……。
フリーソフトがGNUの用語だなんて意識してないよ、そんなもの。
でもそうか、gccだからGPLなんだよね、忘れてた。
例外条項?があるならいいね。

GCC Runtime Library Exceptionとかいうやつで、各言語のランタイムに限り実行ファイルにリンクしてもGPLを適用しなくていいっぽいです。
間違ってたらごめんなさい、っていうかCOPYING.RUNTIMEを参照ってことで。

そうだよね、BSDやなんかでもコンパイラはgccだもんね。
LLVMってどうなったんだっけ?

さあ……。

20MB超えの実行ファイル

おっと、そんな話をしに来たんじゃないんだ。
実は、マイハイパーユーティリティは全部スタティックリンクすると20MBを超えてしまってね、これじゃちょっと配布するのにみっともないかなあって。

気にしなければいいじゃないですか。
今時10MBや20MB、珍しくもないですよ。

それはそうかもしれないけどさあ、君から聞きたくはなかったな。
Vectorの作者ホームページ5MBで頑張ってた仲間じゃないか。AirH"の転送量を減らすためにあれこれしてたくせに。

光回線いいですよ光回線。

殺意しか沸かない。

そんな事言われましても。

DLLに分けようにも、VBやDelphiぐらいならまだランタイムDLLをダウンロードしてください、でもいいんだろうけど、gccのDLLなんてそもそも配ってないしね。
まさかMinGWかCygwinをインストールしてくださいってわけにもいかないよ。

あれは?なんでしたっけ、名前ド忘れしました。
実行ファイルを実行できる状態のまま圧縮するやつ。

UPX?

そうそう。

あれは確かに実行ファイルは小さくなるんだけど、全部込みのアーカイブは逆に大きくなってしまうんだ。
最近のアーカイバは全部のファイルをまとめて圧縮してくれるから、圧縮済みのファイルが入ってると逆に効率悪いんだよ。
それに起動時に余計な処理が入るのも気に入らないしね。

神経質ですねえ。

だから君に言われたくないよ!
人一倍気にするくせに!

Macですとアーカイバはdmgしか使いませんし……そういえばMinGWがlzmaを使うようになって、解凍にやたらてこずった記憶が。

もう7-zip派の言うことじゃないね。
人間はここまで堕ちることができるものだろうか。

それでどうしたいんですか。

やることはひとつしかないだろ。
EXEサイズを減らしたいんだよ。
いくらマイハイパーユーティリティがエレガントな高機能を詰め込んでると言っても、所詮僕一人で日曜日に作ったものだからね。
どう考えても20MBにもなるハズはないんだ。
感じ2〜3MBがいいとこなんだよ。

なるほど。

strip

どんな風にコンパイルしてます?
まさかデバッグ情報を付けたままなんてことは無いでしょうけど。

それは君だろう。Delphiは実行ファイルにデバッグ情報は入らないからサイズ増えないんだって自慢して、デバッグビルドのままで配布してたくせに。
いくら僕でもリリース用のオプションぐらい分けるよ。

デバッグ情報なしでコンパイルしたものを配布すると、「アドレスxxxxでアクセス違反が起きました」なんて報告をもらっても、特定できないじゃないですか。

いいかい、addr2lineというものが……。

知ってますよ!Delphiの話ですよ!
……ところで、そのシンボル情報はstripしてます?

strip?

ldでデフォルトのままリンクしますと、各オブジェクトファイルのシンボル情報は全部実行ファイルに残ってます。
だからこそaddr2lineも動くわけですが、これが結構な量なんです。

ええー。そうなんだ……。

今までどうやってaddr2lineが動いてたと思ってたんですか……。

言われてみれば、VC++なんかが作るデバッグ用のファイルなんかも作られないと思ってたよ。

Delphiですと.dcuの情報を使って……。

それはもういいって。
stripを実行すればいいの?

ええ。
それか、リンク時に-sオプションでも。

どれどれ……。
おお、一気に何MBも減ったよ。
すごいな、必須じゃないかこれ。
あれ?この状態でaddr2lineは……。

動かないですね。
でも手元にstrip前のファイルを残しておけば大丈夫ですよ。

良かったあ。

スマートリンク

あ、それで気がついたけど、ldのスマートリンクのオプションは何?
使われていない関数を実行ファイルに入れないようにするやつ。
それでまただいぶ減ると思うんだ。

残念ながら、Windowsだとldでスマートリンクはできないんですよ。

ええー!!

本来コンパイル時に-ffunction-sections -fdata-sectionsを付けて、リンク時に--gc-sectionsでスマートリンクができるのですが、ldのソースを見るとこの処理はELFの時しか入ってないんです。
COFFの該当する関数なんか空っぽですよ。
流石GNUです。

……うわあ。
んー、他のリンカを使えばいいのかな。VC++のやつとか。

確かにフォーマット自体はCOFFですが、binutilsは複雑に絡んでますから、期待しないほうがいいと思いますよ……。

それでよく君も我慢してMacを使っていられるね。
確かMach-OもELFじゃないだろう。

Apple製のldはGNU ldとは別物ですからね。
-dead_stripはちゃんと動作しますよ。
ただ、Leopardのldとgcc 4.6の組みあわせがちょっとまずくて。gcc 4.5以前か、スノレパ以降のldですと問題ないんですが。

確かLeopard、Snow Leopard、Lionの順だっけ?
買い換えればいいじゃない。

……ほっといてください。
リンカでどうこうできない時は、gnatelimというのもあります。
これはASISに含まれてますよ。ただ

へえ、ソースコードレベルで使われてない関数を調べて、.oに吐かないようにpragmaの一覧を出してくれるんだ。
ちょっとやってみよう。

ただ、まともに動いたためしが……。

あらら、call graph info collection failedとか出た。

……そういうことです。

うーん、どうするかな。
いちいちpragma Eliminateを書くとか……ブツブツ……。

本当にそれをやったら尊敬します。

あ、思い出したぞ。

な、なんです?

君、ずいぶん前にツールを作ってなかったっけ。
アセンブラのソースをセクションごとに切り分けるやつ。
関数ごとにオブジェクトファイルを作ったら必ずスマートリンク状態だよね。
あれ使わせてよ。

……よく覚えてますねえ。
確かにあれはスマートリンク目的で作ったものですが、もう使えませんよ。

え?なんで?

gccがゼロコスト例外のための例外情報テーブルを吐くようになって、そのオブジェクトファイル中の全部の関数がひとつのテーブルに入っちゃいますからね。
コンパイラかリンカが直接サポートしてくれないとどうしようも。

MinGWのも最近はZCXがデフォルトなんだっけ?
--enable-sjlj-exceptionsかあ、うーん……。

いやもうあんなツールのためにgccをビルドし直す価値はないですよ、勘弁して下さい。

となると後は……あ、確か最近のgccにはLTOってあったよね!

色々知ってますね。

そりゃあもう。
でも使うのは初めてだよ。
ええと、コンパイルもリンクも両方-fltoでいいのかな。
-fwhole-programも付けたほうがいいのかなあ。

どうなりました?

そう急かすなって。
よし……あれ?逆にサイズが増えた?なんで?

……さあ。

LTO使ったこと無いの?

無いですよ。
なんでも試してるわけじゃないですから……。

仕方ないなあ、スマートリンクはあきらめるか。
でも、まだ他に色々あるんだろう?

とほほ……いやまあ、好きですけどね、こういう作業。

その他のコンパイルオプション

ではコンパイルオプションから見直していきましょうか。

よしきた。
今は-O3 -gnatnだけなんだ。なにか問題あるかい?

いや、普通は問題ないです。
ただサイズを減らすのが目的ですから-Osのほうがいいでしょうね。

あいよ。

あと-momit-leaf-frame-pointerも付けましょうか。

ん?それは何?

他の関数を呼び出さない関数ではスタックフレームを作らなくします。
必要ないですからね。

そういう関数はそこで呼び出し元が辿れなくなっても問題ないってことか。
なるほどね。

それから、バグがない自信があるなら-gnatpも付けるといいですね。

実行時チェックを省くオプションだっけ?
うーん、どうするかなあ。

Constraint_Errorを受けて何かしているのでなければ、動作は変わらないはずです。
バグの発見は遅れるかもですが……。

ようしこの際だ、付けるだけ付けてみよう。

それでビルドするとどうなります?

ちょっと待ってよ……。よし。
うーん、たしかに減ったは減ったけど、一割も変わらないなあ。

そうでしょうねえ。
コンパイルオプションを変えた程度ですと。
stripとスマートリンクは、使われていないものを削るから大きいわけで、後は使われてるものを弄るしか無いわけですし。

Ada.Containersを避ける

やっぱりスマートリンクができないのは痛すぎるなあ。
たぶん多くを占めてるのはAda.Containersのコンテナなんだよね。使いまくってるから。
ひとつインスタンス化したら結構いっぱい関数できるじゃない?
generic packageだから手でばらすわけにもいかないし。

いや、残念ながら、もしスマートリンクが使えても、コンテナの関数は削れないです。
本当に残念なのですが。

え?なんで?

コンテナがtagged型だから、全部関数ポインタがVMTに乗っちゃいます。

あー!!
そ、そうかあ!!

finalが無いのが悪いんですよ……。

てことはなに?
もしかして、普通の配列やなんかに書き換えたら……。

ええ、だいぶ減ると思いますよ……。

情報を削るpragma

コンテナをちまちま使わないように直していくしかないのはわかったけど……しかし……。
うわあ……。大変だぞ……。

お察しします。
C++のように使った時点で初めてインスタンス化なら良かったのですけどねえ。

よし、後回しにする!

おお、英断です。

もっと手軽で細かい最適化を先にやるぞ。
さあキリキリ吐いてくれたまえ。

人、それを現実逃避と呼びます。

いいだろ……。

んー、ではとりあえずpragmaからいきますか。

よし、pragma Eliminateみたいなのが他にもあるってことだね。

まず、例外はどれぐらい使ってますか?

ん?そりゃ、そこそこには。
やっぱりエラーコードを返すよりは便利だしね。

例外を使ってるなら、pragma Suppress_Exception_Locationsを付けてみてください。

これは何?

ほら、例外を受け止めなかった時、raiseしたファイル名と行数が表示されるでしょう。

raiseにwithを付けなかったときのException_Messageだね。

あれは当然文字列として持ってるわけですから、それを空文字列にします。

うーん、どうするかなあ。
いやアドレスさえわかったらaddr2lineでわかるけど。
まあ、やるだけやるか。

それから、列挙型にはpragma Discard_Namesを付ける。

それは?

列挙型の要素の名前を持たなくするpragmaです。
'Imgaeで名前ではなくて連番を文字列化したものを返すようになります。

'Imageも所々使ってるんだけどなあ。
でも使ってない列挙型もあるし、ひとつひとつ考える必要があるね。
よし、それも付けてみるよ。ただ後でゆっくりとやるかな。

あとはpragma Suppress_Initializationでしょうか。
これは注意しないと結構怖いんですが。

なに?

配列やrecordを宣言するだけで、名前の後ろにIPって付いた関数が作られるんですよ。

へえ……nmしてみよう。
本当だ。何する関数?

初期値なしの変数にデフォルトの初期化を行う関数です。
access型にnullを入れたり、Controlled型をInitializeしたり。

それが行われないって怖いなあ。
初期値を与えたら関係ないんだよね?……とはいえ。

私も、これを使うのはprivateなものに限定してますね。
それぐらいならひとつの.adbの中だけ注意してればいいですから。

……なるほどねえ。

あとControlled型はなるべくlimitedにしたほうがAdjust分減るのですが、build-in-placeのためのコードで逆に増える場合もあって悩ましいです。

それから、Ada.Task_Attributesは使ってます?

んー、使ってないなあ。

ですか。

ですかで済ますなよ……。気になるじゃないか。
全部言え、キリキリ言え。

あー、えーと、最近のgccは全てのプラットフォームでTLSエミュレーションができるようになってますので、pragma Thread_Local_Storageに置き換えることもできる、と、そんな豆知識です。

packageの初期化関数

nmして気がついたんだけど、elabsとかelabbってのも勝手に作られてるね。
何する関数?

packageの初期化ルーチンです。

それってbodyの最後にbeginでコードが書けるやつだろう?
そんなの書いた覚えは無いんだけど。

それ以外でも実行時に初期化が必要なのは無いですか?
グローバル変数や定数を関数を呼んで初期化してるとか。

知っているだろう。
僕のコードは君と違ってピュアなのさ☆
グローバル変数なんて無いね。

コイツうぜえ。

……ごめんごめん。
えーと、確かAda.ContainersのコンテナにはEmptyなんとかやNo_Elementって定数があるのはわかるよ。
でもコンテナを使ってないpackageでもelabsができてるよ。

残念ながら、勝手に初期化ルーチンが作られる場合も多々あります。
例えば例外。

_system__exception_table__register_exceptionってのが呼ばれてるけど……まさか、例外を宣言するだけで?

ええ。
例外の情報はストリームに読み書きできるのですが、プログラムに含まれる全部の例外の情報を持っておかないと、ストリームから呼んだ時に復元できないですから……。

_ada__tags__register_tagってのももしかして、おなじ?

ええ。同じ用途です。

うわあ……。
例外やtagged型を保存なんかするわけないだろう。
VMT付きオブジェクトを保存したいなら専用の仮想関数でも付けるよ。
余計な機能だなあ……。

RMで要求されてますのでgccを責めるのもどうかと思いはしますが、これをやめるオプションも欲しいですね。
一応、ランタイムのソースを弄ってこれらの関数をis nullにしてしまえば呼ばれなくなりますが……。

gccの再コンパイルが必要、と。

ええ。よくわかりましたね。

そりゃわかるよ。ランタイムはgccと一緒にビルドされるんだから。器用にランタイムだけ再ビルドするなんて、そりゃ詳しい人ならできるんだろうけど、僕には無理だよ。
で、他には?
こっちのpackageは例外もtagged型も作ってないけど、やっぱりelabsができてるよ。

どれどれ。
あー、Controlled型を指すaccess型を宣言してますね。

え!?
なんでそれで初期化が必要になるの?

gccのAdaはガベージコレクタこそ持ってませんが、動的に割り当てたControlled型も終了時にFinalizeが呼ばれるようになってはいます。開放し忘れていても。
そのためのリンクリストが作られますね。

……。
律儀なんだかなんなんだか。
いっそメモリリークしてるって教えてくれたらいいのに。

そのためのツールもありますよ。gnatmem。

……。
律儀だなあ。

ま、valgrindが動く環境ならvalgrindのほうがいいですけどね。

valgrindっていいの?
現代のオーバーテクノロジーだとかなんとかは聞くけど。

素晴らしいですよ、一度使ってみましょう。

僕はWindowsでいいよ。

後はバグで、generic packageをインスタンス化してると無駄に空っぽの初期化ルーチンが作られてしまうとか、それぐらいでしょうか。
探せば他にも初期化ルーチンを作る理由はあるかも。taskやprotectedも初期化は必要でしょうし。

色々無駄なことしてるなあとは思うけど、それでも初期化ルーチンを削ったところで精々ファイル数個分の小さな関数が無くなるだけだよね……。
やっぱり一番はコンテナかあ。

頑張ってください。
サイズなんて気にしないのが一番とは思いますけど。

だからそれを言うなー!!