2006年4月 3日

Doug Lea の malloc (dlmalloc)

小さなオブジェクトを大量に new しまくるプログラムを C++ で書いたところ、処理時間の多くが malloc() に費やされていることがわかりました。このような場合、自前でメモリ管理を行って最適化するという方法がありますが、なかなか大変です。

そこで、安易に高速な malloc に置き換えてみようということで、 Doug Lea の malloc (通称 dlmalloc) の最新版を試してみました。

 

dlmalloc の使い方

dlmalloc は 1ファイルをダウンロードしてビルドすれば使えます。次のように実行すると共有ライブラリ libdlmalloc.so を作れます。現時点でのバージョンは 2.8.3 でした。

% wget ftp://g.oswego.edu/pub/misc/malloc.c
% gcc -O2 -shared -o libdlmalloc.so -fPIC malloc.c

上で作った libdlmalloc.so を Linux 上で使うには普通にリンクするか、 LD_PRELOAD=./libdlmallo.so を指定して任意のプログラム (libc が静的リンクされているものはダメです) を実行します。

もちろん gcc -O2 -c malloc.c で malloc.o を作ってリンクしても使えます。

なお、dlmalloc はデフォルトではスレッドセーフではありません。USE_LOCKS マクロを定義してコンパイルすればスレッドセーフになりますが、性能はいまひとつのようです。今回の実験に使うプログラムはシングルスレッドで動くものなのでデフォルトのままで構いません。

実験結果

くだんの new しまくるプログラムを LD_PRELOAD=./libdlmalloc.so で実行したところ、4.5秒かかっていた実行時間が 3.6秒になりました。20% の高速化です。ltrace -e malloc で調べたところ malloc の呼び出し回数は 1,376,066 回でした。今回の実験では、 dlmalloc は GLIBC の malloc よりもかなり高速という結果が出ました。手元の環境は GLIBC 2.3.2 です。

ソースコードを見てみると GLIBC 2.3.2 の malloc は Doug Lea の実装 (2.7.0) を元に、マルチスレッド環境で性能を出すために手を入れられた ptmalloc2 という実装が使われていました。

ちなみに、頻繁に呼ばれている new を Boost の boost::object_pool で置き換えてみたところ、ほんの少しだけ速くなりましたが、ほとんど誤差程度だったため、今回は利用を見送りました。

なお、実験に使ったプログラムは近々公開する予定の超ニッチなツールです。

追記

革命の日々!さんから「glibc mallocはDoug Lea mallocほぼそのままなんで、20%も性能が異なるなんて普通ありえないんですよ」というコメントをいただきました。確かに変だと思って調べてみると、原因がわかりました。

原因は Boost の正規表現ライブラリ libboost_regex が、手元の Debian GNU/Linux sarge の場合、libpthread.so に依存していたことにありました。GLIBC の ptmalloc2 は libpthread.so がリンクされていると挙動を変えて、マルチスレッド用にロックを行うようになります。

そこで、libboost_regex を使わないようにプログラムを修正すると、 実行時間は 3.9 秒になりました。GLIBC 2.3.2 の ptmalloc2 のベースになっている malloc-2.7.0.c を使った場合も 3.9秒です。一方、最新の dlmalloc (2.8.3) を LD_PRELOAD=./libdlmalloc.so した場合は 3.6 秒でした。

というわけで、4.5 秒から 3.6 秒への 0.9 秒の高速化のうち 0.6 秒はマルチスレッド用のロックが消えたのが原因で、0.3 秒は dlmalloc 2.7.0 と 2.8.3 の性能の差のようです。dlmalloc に置き換えただけで 20% も速くなった、というのはややぬか喜びだったようです。とはいえ、 0.3 秒速くなったのはうれしい収穫ではあります。

まとめ

今回の実験では GLIBC の malloc を最新の dlmalloc に置き換えるだけで 20% の高速化が得られました。しかし、「追記」のところに書いたように、高速化の主な理由はマルチスレッド用のロックが消えたことにあります。シングルスレッドで動作するプログラムには不用意に libpthread.so をリンクしないこと、ということが重要なポイントでした。

参考ページ