動機
Erlang の消費メモリを減らせないかと思い試してみた。
ターゲットは Linux, x86_64 に限定する。
- Erlang は動的型付け言語
- 「自然に表現可能な最も大きな型」に合わせて汎用型が定義される
- つまり CPU のレジスタサイズ
- 配列が存在せずリストに頼りがち
- 必ず値とポインタの 2 要素に膨らむ
- ポインタなのでメモリ上に散らかる
- メモリをシーケンシャルにアクセスできない
汎用型は内部的に Eterm と定義されていて、所謂 variant 的なもの。どんなに小さな値を表現するのにも Eterm になるので 64 ビット OS では 8 バイト消費する。これではメモリの消費が激しいし CPU キャッシュにも乗りにくい。
ひどいのが文字列で、リストとして表現されるので "Hello World!" だけで 192 バイト消費する。リストの長さを知るには散らかった値を全て舐める必要がある。リストはとにかくいいことがない。全くないわけではないのだが、まあないと言い切っていい。
話が逸れた。とにかく Erlang プログラムに手を加えず メモリ消費を抑える方法を考えた結果、32 ビットで動かしてみようと考えた。
想定されるメリットは次の 2 つで、根本的な解決にはなり得ないことを先に忠告しておく。
- 搭載すべきメモリの量を最大 50% 削減する
- CPU のキャッシュヒット率を上げる
方法
方法は次の 3 つある。
- 32 ビット CPU 向けにビルド
- 32 ビットエミュレーターを使う
- x32 向けにビルド
32 ビット CPU 向けにビルド
configure 時に --enable-m32-build を指定すれば良い。
このオプションは gcc に -m32 を渡し、i386 という 32 ビット CPU 向けにコンパイルしてくれる。これですべてうまくいくように思えるが、実際は依存ライブラリも -m32 でコンパイルされていなければならないためハードルが高い。x86_64 向けの OS ではわざわざ 32 ビット CPU 向けのライブラリは用意されていないのだ。
そんなことをするくらいなら、初めから i386 向けの OS をダウンロードしてきた方が良いだろう。今の時代なら VM もある。
32 ビットエミュレーターを使う
configure 時に --enable-halfword-emulator を指定すれば良い。すべての選択肢の中でこれが一番簡単だ。
このオプションは Erlang が持つ Halfword Heap Emulator という機能を使うように指示する。Halfword Heap Emulator は Eterm を 32 ビットとして定義する。Heap と書いてある通り、目的はヒープのサイズを 32 ビットに制限することだ。Erlang ではポインタも Eterm で表現されるため 4 GB を超えて表現することができなくなる。
しかし 64 ビットの OS ではメモリのアドレスは 64 ビットで表現されるのだ。これを 32 ビットで表現できるわけがなく、多数の #ifdef HALFWORD_HEAP で涙ぐましい努力がされている。これは流石にまずいと思っているようで、 configure が終わると最後に
This is a DEPRECATED feature scheduled for removal in a future major release.
(意訳: この機能は将来削除すっぞ)
と釘を刺される。
x32 向けにビルド
正確には x32 ABI と呼ぶのだが、x86_64 上でアドレスの表現を 32 ビットにするインターフェースだ。多くの場合アプリケーションに 4 GB を超えるようなメモリは過剰であり 32 ビットで十分足りる。しかもハードウェアも OS も 64 ビットであるため
- OS は 4 GB を超えるメモリが扱える
- i386 に比べて多くのレジスタが使える
- サブルーチンの引数がレジスタで渡される (i386 はメモリで渡す)
- i386 にはない高速な syscall が実現出来る
といった利点がある。上の Halfword Heap Emulator はソフトウェアによる努力の賜物だが、x32 は x86_64 アーキテクチャに存在するハードウェアによる機能をふんだんに使うため無駄が少ない。後述するが、いくつかの理由により i386 向けにビルドするのと同程度の手間が必要になる。
以降、この方法を掘り下げて解説する。
x32 向けビルドの手順
環境を用意する
アドレスの表現が 32 ビットになるということは OS の API も 32 ビットのアドレスを渡さなければならないため、OS のサポートが必要になる。
主に気をつけるのは次の 3 つだ
- Linux 3.4 以降
- カーネルコンフィグの
CONFIG_X86_X32 - x32 向けのライブラリ
Linux 3.4 以降のディストリビューションを探すのは簡単だが CONFIG_X86_X32 が有効になっていないものが多いため注意する。カーネルコンフィグを書き換えてビルドするのが当たり前だった時代の人たちには壁など見えていないはずなので突き破ってほしい。
ライブラリも x32 向けのものが必要になるが CONFIG_X86_X32 が無効のディストリビューションにライブラリが提供されるわけがないため、この点からも CONFIG_X86_X32 が有効なディストリビューションを選ぶのが賢い。ただし足りないものは自分でビルドする必要がある。
CONFIG_X86_X32 が有効なディストリビューションは Debian 系だ。他にもあるかもしれないが、今回は Ubuntu 14.04 LTS を使用した。
最小構成でインストールした場合、ざっと次のパッケージが必要になる
$ sudo apt-get install make autoconf automake git gcc g++ clang libx32gcc-4.8-dev libc6-dev-x32 libx32ncurses5-dev
今回は gcc を使うので clang は必要ないが、ただの手癖だ。
次に Erlang/OTP を git clone する
$ git clone https://github.com/erlang/otp.git
ビルドする前に
今まで黙っていたが、実は x32 は最も危険な選択肢だ。なぜならば Erlang のプログラムに手を入れる。冒頭の
とにかく Erlang プログラムに手を加えず メモリ消費を抑える方法を考え、32 ビットで動かしてみようと考えた。
は嘘じゃないかって? いや .erl ファイルには手を加えない。手を加えるのは Erlang VM だ。もっとも、軽微な問題を修正するだけなので、パッチでも送って相手の機嫌さえ良ければ本流に取り込まれるかもしれない。
コンパイラオプションの変更
まず --enable-m32-build 向けのビルドオプションをベースにするのが簡単なので -m32 で grep してみる
$ cd otp
$ grep -r '\-m32' .
20 くらい引っかかるが、ファイル数は多くない。これら -m32 を -mx32 に片っ端から置換する。一括置換でいい。実際には _m32 という名前もあるのだが、重要なのは CFLAGS と LDFLAGS なので、ほかのシンボルの置換は漏れても問題ない。
プログラムの変更
-mx32 を指定するとアドレスが 32 ビット表現になるため、x32 の事を知らないプログラムが環境をチェックすると 32 ビット CPU であると勘違いしてしまう事がある。Erlang は内部でインラインアセンブリが記述されていて、勘違いした結果 32 ビットレジスタを使用してしまうが、実際は 64 ビット CPU なのでエラーになる。
直すべきは次の 2 箇所だ。
erts/lib_src/pthread/ethread.c: ethr_x86_cpuid__():
-#if ETHR_SIZEOF_PTR == 4
+#if ETHR_SIZEOF_PTR == 4 && !defined(__ILP32__)
-#if ETHR_SIZEOF_PTR == 4 && defined(__PIC__) && __PIC__
+#if ETHR_SIZEOF_PTR == 4 && defined(__PIC__) && __PIC__ && !defined(__ILP32__)
いよいよビルド
もう全ては終わった。あとはビルドするだけだ。
--enable-m32-build をベースに -mx32 に置換したはずなので、./configure --help を行うと --enable-mx32-build が現れるはずだ。
$ ./otp_build autoconf
$ ./configure --prefix=/opt_x32 --enable-mx32-build
$ make
$ sudo make install
x32 になっているのかを確認してみよう
$ file /opt_x32/lib/erlang/erts-6.4.1/bin/erlexec
/opt_x32/lib/erlang/erts-6.4.1/bin/erlexec: ELF 32-bit LSB executable, x86-64
x86-64 と言っているのに 32-bit だ。
動かしてみる
$ /opt_x32/bin/erl
最後に
メモリの消費を抑えるため x32 ABI 向けに Erlang をビルドする方法を紹介した。
時間の都合で実際に消費量が半分になったことを確認するベンチマークは用意していない。結果は自分の目で確認してほしい。
今回が Qiita を使った初めての記事になる。今後の展望としては Eterm の仕組みや list, atom の内部データ構造といった、Erlang のより内側を紹介できればと思う。