メモリプールを使ったプログラミング

C 言語を使ったことのあるほとんどすべての開発者は、ある時点で メモリ管理のことでうんざりしてため息をつくことがあるでしょう。 利用するために必要な十分なメモリを確保し、その利用状況を記録し、 いらなくなったらメモリを解放する—そういう処理は非常に 複雑です。そしてもちろん、それに失敗すると、プログラムが壊れて しまい、ひどいときにはコンピュータが壊れてしまいます。 幸運にも、Subversionが可搬性のために利用しているAPRライブラリ はapr_pool_t型を用意していて、これは メモリのプールを表現するものです。

メモリプールはプログラムによって利用するために確保されたメモリ ブロックの抽象的な表現です。標準的なmalloc() 関数 とその亜種を使ってOSから直接メモリを取得するかわりに、APRをリンクした プログラムは単にメモリプールを作る要求を出すことで行います。 (これにはapr_pool_create() 関数を使います) APRはOSから自然なサイズのメモリを確保し、そのメモリはすぐにプログラム で使うことができるようになります。プログラムがプールメモリを必要と するときにはいつでも、apr_palloc()のような APR プール API 関数のどれかを使うことができて、それはプールから 汎用的なメモリを確保して返します。プログラムは 要求ビットとプールからのメモリを要求し続けることができて、 APRはその要求を承認し続けることができます。プールはプログラムに あわせて自動的にサイズが大きくなり、最初プールに含まれていたよりも 多くのメモリを要求することができます。これはシステムにメモリが なくなるまで続けることができます。

これでプールの話が終わりなら、特別の注意を払う必要もないのですが。 幸運にも、そうではありません。プールは作られるだけではありません: それは またクリアしたり削除することもできます。これには apr_pool_clear()apr_pool_destroy() をそれぞれ利用します。 これは開発者にいくつもの—あるいは何千もの—領域をプールから 取得して、その後一度の関数呼び出して、そのすべてクリアする柔軟性を 与えます。さらに、プールは階層を持っています。既に作られたどのプール にも「サブプール」を作ることができます。プールがクリアされると、そのプール のすべてのサブプールは削除されます。もしプールを削除すると、そのプール 自身と、サブプールの両方が削除されます。

先に進める前に、開発者はSubversionソースコード中に、いま言ったような APRプール関数の呼び出しが、それほど多くないことに気づくでしょう。 APRプールは、いくつかの拡張メカニズムを持っていて、それはプールに 固有の「ユーザデータ」を接続する能力や、プールが削除されるときに 呼び出されるクリーンアップ関数を登録する仕組みなどがあります。 Subversionはこのような拡張機能を、それほど自明ではない方法で利用します。 それで Subversion は(そしてそのコードを使う人のほとんどは)ラッパ関数 である svn_pool_create(), svn_pool_clear(), そして svn_pool_destroy() を提供しています。

プールは基本的なメモリ管理にも役に立ちますが、ループや再帰的な状況で のプールの構築は本当にすばらしいものです。 ループはしばしばその繰り返し回数が不定であり、再帰的はその深さが不定 なので、このような領域でのコードのメモリ消費量は予測することができま せん。幸運にも、ネストしたメモリプールを使うと、このような潜在的な 恐ろしい状況を簡単に管理することができます。以下の例は、よくある非常に 複雑な情報でのネストしたプールの基本的な使い方を示しています。 —この状況とは、ディレクトリツリーを再帰的にたどりながら、ツリーの すべての場所である処理を実行する、といったものです。

例 8.5. 効率的なプールの利用

/* Recursively crawl over DIRECTORY, adding the paths of all its file
   children to the FILES array, and doing some task to each path
   encountered.  Use POOL for the all temporary allocations, and store
   the hash paths in the same pool as the hash itself is allocated in.  */
static apr_status_t 
crawl_dir (apr_array_header_t *files,
           const char *directory,
           apr_pool_t *pool)
{
  apr_pool_t *hash_pool = files->pool;  /* array pool */
  apr_pool_t *subpool = svn_pool_create (pool);  /* iteration pool */
  apr_dir_t *dir;
  apr_finfo_t finfo;
  apr_status_t apr_err;
  apr_int32_t flags = APR_FINFO_TYPE | APR_FINFO_NAME;

  apr_err = apr_dir_open (&dir, directory, pool);
  if (apr_err)
    return apr_err;

  /* Loop over the directory entries, clearing the subpool at the top of
     each iteration.  */
  for (apr_err = apr_dir_read (&finfo, flags, dir);
       apr_err == APR_SUCCESS;
       apr_err = apr_dir_read (&finfo, flags, dir))
    {
      const char *child_path;

      /* Clear the per-iteration SUBPOOL.  */
      svn_pool_clear (subpool);

      /* Skip entries for "this dir" ('.') and its parent ('..').  */
      if (finfo.filetype == APR_DIR)
        {
          if (finfo.name[0] == '.'
              && (finfo.name[1] == '¥0'
                  || (finfo.name[1] == '.' && finfo.name[2] == '¥0')))
            continue;
        }

      /* Build CHILD_PATH from DIRECTORY and FINFO.name.  */
      child_path = svn_path_join (directory, finfo.name, subpool);

      /* Do some task to this encountered path. */
      do_some_task (child_path, subpool);

      /* Handle subdirectories by recursing into them, passing SUBPOOL
         as the pool for temporary allocations.  */
      if (finfo.filetype == APR_DIR)
        {
          apr_err = crawl_dir (files, child_path, subpool);
          if (apr_err)
            return apr_err;
        }

      /* Handle files by adding their paths to the FILES array.  */
      else if (finfo.filetype == APR_REG)
        {
          /* Copy the file's path into the FILES array's pool.  */
          child_path = apr_pstrdup (hash_pool, child_path);

          /* Add the path to the array.  */
          (*((const char **) apr_array_push (files))) = child_path;
        }
    }

  /* Destroy SUBPOOL.  */
  svn_pool_destroy (subpool);

  /* Check that the loop exited cleanly. */
  if (apr_err)
    return apr_err;

  /* Yes, it exited cleanly, so close the dir. */
  apr_err = apr_dir_close (dir);
  if (apr_err)
    return apr_err;

  return APR_SUCCESS;
}

この例はループと再帰的な状況の両方での 効率的な プールの利用法を説明するものです。それぞれの再帰は関数に渡すプール のサブプールを作ることで始まります。このプールはループの領域で利用 され、それぞれの繰り返しでクリアされます。この結果、メモリの利用は、 大雑把にいって再帰の深さにだけ比例し、最上位ディレクトリの子供としての ファイルとディレクトリの合計数には比例しません。この再帰関数の最初の 呼び出しが終了した時点で、渡したプールに保存されたデータは実際には 非常に小さなものになります。この関数が、alloc()free()関数を一つ一つのデータに対して 呼び出さなくてはならないとしたときの複雑さを考えてみてください!

プールはすべてのアプリケーションに理想的なものではないかも知れませんが Subversionでは非常に役に立ちます。Subversion開発者として、プールの利用 に親しくなり、どうやってそれを正しく使うかに精通しなくてはなりません。 メモリ利用に関係したバグとメモリリークはAPIの種類によらず、 診断し、修正するのは難しいものですが、APR によって用意されたプール の作成は、非常に便利で、時間の節約につながる機能を持っています。