🐯

古いDOS系Cコンパイラで printf の %e%f%g が未使用だと exe サイズが小さくなる仕組

2023/12/31に公開

watcom c で遊んで いて。

printf で浮動小数点数指定 %e %f %g が未使用であれば小さく、使用すれば結構大きくなることがありました。

#include <stdio.h>
int main() {
    printf("Hello %d World!\n", 1);
    return 0;
}

を wcl -0 -ms hello.c で Small model ビルド、 16bit exe を作ると 8632 バイト。

printf の部分を

printf("Hello %f World!\n", 1.0);

に置き換えて 27392バイト(ソフト浮動小数点版)。
8087専用指定(-fpi87)して 16018 バイト。
8KBバイトくらいがソフト浮動小数点の差として、8KBくらいは%e%f%gに絡んだ処理。

MB/GBのメモリ環境では些細な差だけれど、数百KBで動くプログラムでは、多少気になるサイズ差です。(Tiny model 64KB, Small model 128KB では気になる差)

確認すると、VC11(VS2012) 以前や、Digital Mars C/C++、古いBoland C/C++(5.5.1) のようなDOS系のコンパイラが同様の挙動になっていました。

浮動小数点を使えばその処理分が増え、無ければ減る、というのは利用者にとっては納得の挙動です。

しかし通常の C 言語の範囲で考えれば、printf 関数処理内で書式文字列をパースして%e%f%gで浮動小数点数の文字列変換をする処理がある以上、整数のみかどうかに関わらず exe に含まれるはず。

それが %e %f %g を使わなければサイズが小さくなるのはコンパイラが何か特殊処理をしてくれているのでしょう。

Open Watcom C のソースをみる

で、少し open watcom のソースを見てみました。
以下 printf 系だけ言及しますが、実際には scanf 系についても同時に行われています。

  • printf系ライブラリソースの実際の処理(prtf.c) では、関数ポインタ __EFG_printf 経由で %e %f %g 専用の文字列変換ルーチンを呼び出している。
  • その関数ポインタ __EFG_printf は、cソース noefgfmt.c で定義され、何もしない関数へのポインタで初期化。
    浮動小数点数を使わない場合の、%e %f %g 処理のダミーになります。
  • 浮動小数点数が使われた時用の処理としては、setefg.c の関数 __setEFGfmt() で ポインタ __EFG_printf に 実際の %e %f %g 処理を行う関数へのポインタを代入。
  • __setEFGfmt() は、fltused.c にて AXI( __setEFGfmt, INIT_PRIORITY_LIBRARY ) マクロで起動時初期化用テーブルにポインタが設定される。
    • コンパイラ拡張の機能で、アセンブラ疑似命令でのセグメント配置指定(CPUのセグメントとは別モノ)をして、リンク時に同名セグメント名のデータを複数オブジェクトファイルから集めて一塊の初期化用テーブルにする …… c++ でのグローバル変数のコンストラクタ呼出と同様の仕組みを使っていると思います。
    • あと、このこのファイルには_fltused_という整数変数が定義されています。
  • watcomコンパイラのソースでは、コンパイル中のcソースで実際に浮動小数点数(演算)が使われていた場合に_fltused_への参照をオブジェクト・ファイルを出力。

なので、プログラムで浮動小数点演算が使われると 変数_fltused_の参照が発生し、exe生成のリンク時に fltused.c がリンクされ、そこにある起動時初期化テーブルも登録されるため、exe 実行開始時に __setEFGfmt() が呼び出され __EFG_printf に %e %f %g 処理の実体が設定されます。
逆に_fltused_の参照がなければ fltused.c はリンクされず実体化の初期化も行われないので、__EFG_printf はダミー空関数のまま。

ソース眺めただけで細かいこと調べず大分端折ってるけど、雰囲気としては、こんな感じでしょうか。

起動時初期化の仕組みがあるところに、変数参照一つで、実装の選択ができるのは低コストだと思います。
_fltused_は浮動小数点が使われたら参照が生成されるみたいなので、%e %f %g 書式の使用の有無は正確には関係がないですが、現実的な対応でしょう。(整数オンリーで作ったときに printf の浮動小数点実装が含まれなければ とりあえず吉と)

他のコンパイラについて

アセンブラ出力をしてみただけですが、

コンパイラ 追加の変数参照
dmc 8.4.2 extrn __fltused
vc 9.0-14.3 EXTRN __fltused:DWORD
bcc32 5.5.1 extrn __turboFloat:word

のように Watcom の_fltused_と似たような変数名の参照が生成されているので、同様の仕組みを採用しているようです。

ちなみにVC12(VS2013)以降は ラベル生成はするけど、%e%f%gの有無による差は無し。
※もちろん dll なランタイムを使う場合(mingw/msys含)は、関係なし。

dmc で dmc -0 -msd でコンパイルして、整数hello 12376バイト、浮動小数点hello 24688バイト。
LSI-C86試食版でコンパイルして、整数hello 12376バイト、浮動小数点hello 12278バイト。

LSI-C86はラベル参照生成をしていないので、printf は%e%f%g切離無しでこのサイズ差。サイズ優先な実装なのかな?

※ 以前 printf を組んだことがあって、%e%f%g は浮動小数点演算使わず実装できたような

あまり、細かいこと気にしだすと底深そうなので、このあたりで終わっておきます。

Discussion