Borland Developer Studio 2006の新しいメモリマネージャ - by Pierre le Riche

By: Hitoshi Fujii

Abstract: メモリマネージャは、Delphi 2006でより高速で、機能豊富なバージョンにアップデートされました。この記事では、従来のメモリマネージャと現在のメモリマネージャ間での違いと、新しい機能がどのように影響を及ぼすかを説明します。

    はじめに

Win32メモリマネージャは、Delphi 2006で、より高速で、機能豊富なものに置き換えられました。この記事は、このメモリマネージャの新機能の概要とこれらの使用法を紹介します。新しいメモリマネージャがどのように実装されており、この新しい設計がメモリマネージャの動作と、Delphi 2006でコンパイルされたアプリケーションに影響を及ぼすかについて解説します。

    新機能

    大きなアドレス空間のサポート

メモリマネージャは、現在2GB(最大4GB)の大規模アドレス空間をサポートします。

    ブロックアラインメントの改善

メモリマネージャによって返されるブロックは、少なくとも8バイトの単位の境界に配置されます。16バイト境界を強制するには、以下のコードを実行します。

SetMinimumBlockAlignment(mba16Byte);

    インテリジェントリアロケーション

リアロケーションには、インテリジェントな管理が導入されています。新しいメモリマネージャでは、メモリブロックを移動させることのメリットがそのコストを上回らない限り、ブロックの移動を回避しようとします。

    ロック粒度の改善

ロックの粒度は、従来のメモリマネージャと比較して、より小さくなっています。そのため、新しいメモリマネージャでは、とりわけマルチスレッドアプリケーションについて、大幅なパフォーマンス改善が見られます。

    フラグメンテーション動作の改善

内部管理構造の改善により、アドレス空間のフラグメンテーションは、従来のメモリマネージャよりも問題が減少しています。

    簡略化されたメモリマネージャ共有メカニズム

新しいメモリマネージャの共有メカニズムにより、borlldfmm.dllライブラリなしで、従来のメソッドを引き続きサポートするようになりました(詳細は、SimpleShareMem.pasを参照してください)。

    メモリリークのレポート

アプリケーションで予想されるメモリリークを登録し(また登録解除し)、オプションでプログラムシャットダウン時の予想外のメモリリークをレポートできます。

    拡張状況のレポート

新しいメモリマネージャは、拡張追跡レポート機能を持っており、ブロックサイズカテゴリごとのアドレス空間の使用状況を追跡できます。また、メモリマネージャの効率性の追跡(効率性は、ブロック管理構造とフラグメンテーションのために、無駄になった割り当てアドレス空間のパーセンテージとして測定されます)も同様です。

    最小ブロック割当値の指定

メモリマネージャによって返されるメモリブロックの最小値は、8バイトです。現在の実装では、148バイトよりも大きいブロックの要求には、すべて最低16バイトを割り当てます。すべての新しい割当要求に対して、16バイト割当を強制するには、SetMinimumBlockAlignment(mba16Byte) を実行します。デフォルトの8バイト割当に戻すには、SetMinimumBlockAlignment(mba8Byte) を呼び出します。

    2GB超のアドレス空間

2GBよりも大きなアドレス空間を使用するには、3つの条件があります。

    2GB超をサポートするOS

2GBよりも大きなアドレス空間をサポートするWindowsのバージョンが必要です。現状では、Windows x64 Edition、あるいは、32-bit Windows XP/2003で、/3GB オプションをboot.iniファイルに設定している場合です。

    適当なリンカ命令

アプリケーションが2GB以上のアドレス空間をサポートすることをWindowsに知らせるために、IMAGE_FILE_LARGE_ADDRESS_AWARE フラグをEXEファイルのヘッダに設定しなければなりません。このフラグは、プロジェクトの .dprファイルに、{$SetPEFlags IMAGE_FILE_LARGE_ADDRESS_AWARE} 命令を記述することで設定されます。

    他社製コンポーネントとライブラリのサポート

すべてのライブラリと他社製コンポーネントは、2GB超のアドレス空間をサポートしていなければなりません。2GBのアドレス空間では、ポインタの上位ビットは常に0です。そのため、より大きなアドレス空間では、これまで全く兆候がなかったポインタ演算バグが露見するかもしれません。このようなバグは、一般的に、ポインタ演算や比較を行うときに、ポインタをCardinal(符号なし整数)ではなく、Integer(符号付き整数)に型キャストすることで発生します。

    メモリリークのレポート

新しいメモリマネージャは、アプリケーションがシャットダウンされる前に解放されなかったメモリブロック(つまりメモリリーク)をレポートできます。メモリブロックを故意にリークさせ、この特定のリークの報告を抑止したいケースがあるかもしれません。この目的のために、リークしたブロックへのポインタを引数として、RegisterExpectedMemoryLeak関数を呼び出します。RegisterExpectedMemoryLeakで登録されるリークは、メモリリークレポートから除外されます。

メモリリークレポートは、デフォルトで無効になっていますが、グローバル変数ReportMemoryLeaksOnShutdownをtrueに設定することで、有効になります。たとえ、ReportMemoryLeaksOnShutdownがtrueに設定されていたとしても、予想外のメモリリークがない場合には、メッセージボックスは表示されません。アプリケーションがデバッガ内で実行されているときだけ、メモリリークレポートを有効にする便利なコードは、次のとおりです。

ReportMemoryLeaksOnShutdown := DebugHook <> 0

リーク登録データベースが、16Kのエントリに限定されている点に注意してください。そのため、RegisterExpectedMemoryLeakは失敗し、falseを返すこともあります。データベースからエントリを削除するには、UnregisterExpectedMemoryLeakを呼び出してください。

以下は、メモリリークレポートの例です(アプリケーションシャットダウン時に表示されます)。

Hide image

これを発生させたコードは、以下です。

Hide image

    メモリマネージャの状態レポート

新しいメモリマネージャは、完全に後方互換性を維持しており、GetHeapStatus呼び出しをサポートしています。さらに、より詳細な情報を取得できる、2つの新しい状態レポート関数があります。

    GetMemoryManagerState

すべての内部ブロックのサイズと、各々に割り当てられたブロックの数を返します。また、それぞれの内部ブロック用に確保されたアドレス空間の合計を返すので、ブロックのオーバーヘッドとメモリプールのフラグメンテーションの割合を計測できます。

    GetMemoryMap

メモリ空間にあるすべての64K Chunkのリストとその現在の使用方法(つまり、未使用、OSにより割当ないし予約、Delphiメモリマネージャによって使用など)を返します。

    アプリケーションとライブラリ間でのメモリマネージャの共有

アプリケーションとライブラリ間でメモリマネージャを共有するには、ShareMem.pasユニットとborlndmm.dllライブラリを通して行うことが推奨されました。この方法は、現在でもサポートしており、パフォーマンスの向上した新しいborlndmm.dllを用いることができます。

新しいメモリマネージャは、外部のライブラリを使用する必要がない、もうひとつの共有オプションを追加しました。この方法は、2つの関数呼び出しによって管理されます。

    AttemptToUseSharedMemoryManager

共有メモリマネージャの現在のプロセスを探します。もし、メモリがまだデフォルトメモリマネージャによって割り当てられていなければ、モジュールは、共有メモリマネージャを代わりに使用するようにスイッチします。もし、他のメモリマネージャが見つかり、モジュールがこれに切り替われば、trueを返します。

    ShareMemoryManager

現在のプロセスを構成する他のモジュールでも共有可能なメモリマネージャを作成します。メモリマネージャは、プロセスごとに1つしか共有できません。そのため、関数は失敗するかもしれません(その場合、falseを返します)。

新しいSimpleShareMem.pasユニットは、この新しい共有メカニズムを実装しており、ShareMem.pasの代替として使えます。

    代替メモリマネージャの使用

旧バージョンのように、メモリマネージャは、他社製メモリマネージャに差し替え可能です。他社製メモリマネージャを有効にするために、また、このリリースでサポートされた新しい機能を実装するために、SetMemoryManagerプロシージャは、オーバーロードされ、新しいTMemoryManagerExレコードを指定できるようになりました。TMemoryManagerExレコードは、従来のTmemoryManagerレコードを拡張し、以下のフィールドを追加しています。

    AllocMem

要求したサイズの0で埋められたブロックを返す関数をポイントします。

    RegisterExpectedMemoryLeak

予想されるメモリリークを登録する関数をポイントします。代替メモリマネージャにメモリリークのチェックとレポートの機能が実装されていない場合には、単純にfalseを返す関数をポイントします。

    UnregisterExpectedMemoryLeak

以前登録した予想されるメモリリークの登録を解除する関数をポイントします。代替メモリマネージャにメモリリークのチェックとレポートの機能が実装されていない場合には、単純にfalseを返す関数をポイントします。

古いTMemoryManagerレコードを用いて独自にインストールした旧型のメモリマネージャでは、デフォルトのAllocMem、RegisterExpectedMemoryLeak、UnregisterExpectedMemoryLeakハンドラがインストールされます。デフォルトのRegisterExpectedMemoryLeakとUnregisterExpectedMemoryLeakは、何もせずにfalseを変えしますが、デフォルトのAllocMemハンドラは、単純にGetMemを呼び出しメモリブロックをクリアします。

    実装の詳細

新しいメモリマネージャは、実際には、3つのメモリマネージャが一箇所に同居しています。2.5K未満のスモールブロック、260K未満のミディアムブロック、そして、それ以上のラージブロックが並列して管理されています。

ラージブロックの要求には、OSを通して(VirtualAlloc)、アドレス空間の上位からメモリが割り当てられます(ミディアムまたはスモールブロックには、アドレス空間の下位から割り当てられます。両者を分けておくことで、フラグメンテーションの動作を改善します)。

ミディアムブロックマネージャは、1.25MB ChunkでOSからメモリを取得します。このChunkは、「ミディアムブロックプール」と呼ばれ、アプリケーションの要求に応じて、ミディアムブロックに細分化されます。使われていないミディアムブロックは、ダブルリンクリストに保持されます。こうしたリストは1024あり、ミディアムブロックの粒度が256バイトなので、ここには、あらゆる可能性のミディアムブロックサイズのプールがあることになります。メモリマネージャは、2つのレベルの「ビットマップ」としてこれらのリストを管理するので、最適な未使用ブロックを探すために、この中で複雑な処理をする必要はありません。「ビットマップ」上で、わずかなビット操作があるだけです。ミディアムブロックが解放されるときはいつでも、メモリマネージャは近隣ブロックが未使用かどうかをチェックして、その結果、解放されたブロックと結合します(両方の近隣ブロックが使用中かもしれません)。メモリマネージャは、「クリーンアップ」バックグラウンドスレッドを持ちません。すべては、freemem/getmem/reallocmem呼び出しの一部として実行されます。

Delphiのようなオブジェクト指向言語では、メモリ割り当てとその解放の多くは、小さいオブジェクトに対してです。さまざまなDelphiアプリケーションの実地試験で、平均して99%以上のメモリ操作が、2K未満のブロックを要求しているということが分かりました。つまりは、このようなスモールブロックに特化した最適化が意味を持つということです。スモールブロックは、「スモールブロックプール」から割り当てられます。スモールブロックプールは、実際には、スモールブロックのサイズに均等に分割されたミディアムブロックです。特定のスモールブロックプールには、同じサイズブロックだけが含まれ、隣接した未使用ブロックは決して結合することはありません。これにより、スモールブロックの割り当ては、劇的に単純化されより高速化されます。メモリマネージャは、すべてのスモールブロックサイズに対して有効なブロックを持つプールのダブルリンクリストを保持します。そのため、GetMemリクエストに応答して有効なメモリブロックを発見するのは、非常に高速です。

一般的に、メモリ内でデータを移動させることは、非常に高価な代償を払います。従って、メモリマネージャは、可能な限りメモリの移動を回避するために、洗練された再配置アルゴリズムを用いています。ブロックが拡大するときには、将来の拡大も見越してブロックサイズを調整します。こうして、次の再配置が適切な場所に行われる確率を高めます。ポインタがより小さいサイズに変更されるときには、メモリマネージャは、以前のサイズよりも著しく小さくなる以外は、ブロックを変更しません。ブロックは、特定のサイズ以下(現在は、およそ64バイト)にも、決して縮小されません。これにより、スモールブロックを含む操作(一般的には長い文字列操作など)の処理速度を向上させます。

処理速度は、改善されたロック機構でさらに高速化しています。すべてのスモールブロックのサイズは、ミディアムブロック、ラージブロックは、別々にロックされます。GetMemリクエストの要求に応えるとき、最適なブロックのタイプが他のスレッドによってロックされていると、メモリマネージャは、最大3倍のブロックサイズを試します。この設計は、スレッド競合の数を劇的に削減し、マルチスレッドアプリケーションの速度を改善します。

    動作の変更

新しいメモリマネージャは、これまで徴候のなかったバグが表面化するかもしれないという点に注意してください。新しいメモリマネージャで異なる兆候が出る可能性があるのは、以下のバグです。

    二重解放バグ

これは、同じポインタを2回以上解放するバグです。新しいメモリマネージャでは、すべてのブロックについて、それが使用中かどうかを示すフラグを、そのヘッダに保持しています。新しいメモリマネージャは、すでに解放されたブロックに対して解放を試みると、「Invalid Pointer Operation」例外をスローします。従来のメモリマネージャは、このエラーが発生しませんでした。

    解放されたメモリブロックの使用

この種のバグは、一般的にオブジェクトが解放されたあとに参照することで発生します。新しいメモリマネージャは、CPUキャッシュをより活用できるので、従来のメモリマネージャよりも積極的に解放されたメモリブロックを再利用します。そのため、解放されたブロックは、従来のメモリマネージャよりも、より早く再利用されることがあります。恐らくこれが、従来は表面化していなかったバグが明らかになってしまう原因になります。

    メモリ上書きバグ

新旧いずれのメモリマネージャも、すべてのブロックの先頭で、ヒープ管理用の32ビットヘッダを使用します。このヘッダがアプリケーションによって破壊されると、大抵クラッシュを引き起こします。新しいメモリマネージャは、頻繁にブロックを再利用するので、従来のメモリマネージャよりも、このクラッシュが早く起きるかもしれません。

上記の注意点から、新しいメモリマネージャが、アプリケーションに起因するヒープ破壊バグをより許容しないということを明らかにしておかなければならないでしょう。これに対応するには、開発サイクル中に、より簡単にバグを補足する必要があるということです。さもなければ、こうしたバグを含むアプリケーションを使用することで、ユーザーにフラストレーションを与える結果になります。

Delphi 2005(およびそれ以前のDelphi)で使用されていた従来のメモリマネージャは、代替メモリマネージャモジュールのかたちで、CodeCenterlにアップされています。このモジュールは、新旧のメモリマネージャのパフォーマンス比較に役立ちますし、従来のメモリマネージャが新しいものよりも要求にかなうような場合に利用できます。


Hide image
devcamp256

9月7日に開催される第2回デベロッパーキャンプでは、チュートリアルセッション#2「Java, Delphi, C++Builderユーザのためのメモリリーク, ボトルネックの検出手順」にて、新旧のメモリマネージャの比較ベンチマークを実施する予定です。参加費は無料です。ふるってご参加ください。


Server Response from: ETNASC04