可変長配列
可変長配列のはなしだ。ありがちな実装としては、
struct vararray { int size; int pos; hoge **buffer; }; void append( vararray *arr, hoge *obj ) { if ( pos >= size ) { char *oldbuf = buffer; buffer = malloc( sizeof(hoge*) * size * 2 ); memcpy( buffer, oldbuf, sizeof(hoge*) * size ); size *= 2; free( oldbuf ); } buffer[pos] = obj; pos++; }
こんな感じだろう。話をややこしくするためにreallocは使わないとする。(ややこしくしたほうが書いてて楽しいので)
こういう実装と、メモリのフラグメンテーションについて考える。
単純に実装されたアロケータの場合、こいつだと、メモリプールの分断が起きる。単純なアロケータというのは、K&Rに書いてあったアレみたいなやつのことだとしておく。
まず、mallocする前のメモリプールの状況をこんな感じだとしておく。
古いバッファとは、領域拡張する前のbufferの内容だとする。
+------+-----------------------------+---------------------------------- |(何か)| 古いバッファ(N byte) | 使われてない領域 ... +------+-----------------------------+----------------------------------
んで、malloc呼ばれた直後がこんな感じ。
-------+-----------------------------+--------------------------------------+--- |(何か)| 古いバッファ(N byte) | 新しいバッファ(N*2 byte) |使われてない領域 +------+-----------------------------+--------------------------------------+--
んで、コピーして、freeしたあとが、
+------+-----------------------------+--------------------------------------+--- |(何か)| 使われてない領域(N byte) | 新しいバッファ(N*2 byte) |使われてない領域 +------+-----------------------------+--------------------------------------+--
こんな感じ。使われてない領域が分断される。
そこで、次のようにする、というのはどうだろうか。
struct vararray { int size; int pos; hoge **buffer; }; void append( vararray *arr, hoge *obj ) { if ( pos >= size ) { char *oldbuf = buffer; char *padding = malloc( sizeof(hoge*) * size ); char *tmp = malloc( sizeof(hoge*) * size ); memcpy( tmp, oldbuf, size * 2 ); free( oldbuf ); free( padding ); buffer = malloc( sizeof(hoge*) * size * 2 ); memcpy( buffer, tmp, size * 2 ); size *= 2; free( tmp ); } buffer[pos] = obj; pos++; }
まず、謎の領域paddingと一時領域tmpの確保。こいつのサイズは古いバッファと同じとする。
-------+-----------------------+------------------------+----------------------+----- |(何か)| oldbuf(N byte) | padding(N byte) | tmp(N byte) | 使われてない領域 +------+-----------------------+------------------------+----------------------+-----
oldbufの内容をtmpにコピーしたあと、oldbufとpaddingの解放
-------+------------------------------------------------+----------------------+----- |(何か)| 使われてない領域(N*2 byte) | tmp(N byte) | 使われてない領域 +------+------------------------------------------------+----------------------+-----
新しい領域の確保
-------+------------------------------------------------+----------------------+----- |(何か)| 新しい領域 (N*2 byte) | tmp(N byte) | 使われてない領域 +------+------------------------------------------------+----------------------+-----
tmp の内容を新しい領域にコピーしたあと、tmpの解放
-------+------------------------------------------------+---------------------------- |(何か)| 新しい領域 (N*2 byte) | 使われてない領域 +------+------------------------------------------------+----------------------------
これで、どう?
一回無駄に領域コピーが発生しちゃうんだけど、まあ、そこらへんは目をつぶる、ということで。そもそも、コピー時間が問題になっちゃうほど大きな領域の場合、上みたいにまるごと領域コピーしちゃうような可変長データ構造を使うほうが悪いのだ。(適当)
でも、今のlibcのアロケータだったら単純な実装してないと思うので、逆に断片化が増えるとか、reallocが賢いので、うまくやってくれるとか、チャンクヘッダがあるので実はうまくいかないだとか、そういうふうに考えなくもないけど、いちおう、ね、思い付いたのでもったいないなぁ、と思って書いた次第。