Delphiアプリケーションのメモリリーク検出法

投稿者:: Tomohiro Takahashi

概要: Delphi Win32アプリケーションのメモリリーク検出手順を紹介します。また、Delphi 2006(BDS 2006)から導入された新しいメモリマネージャFastMMでの方法、またその効果についても検証します。

    MemCheckによるメモリリークの検出

Delphiでは、これまで、MemCheckと呼ばれるフリーのメモリリーク検出ツールを使って、比較的簡単にメモリリーク箇所を発見できました。MemCheckは、Delphi 5からDelphi 2005までをサポートしており、http://v.mahon.free.fr/pro/freeware/memcheck/ からダウンロードできます。

MemCheckの使用方法は、簡単です。

・ダウンロードしたMemCheck.pasを、プロジェクトに追加します。
・次のようにMemChk; の呼び出しを追加します。

begin
  MemChk; // チェック開始
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

・「スタックフレームの生成」をONに設定します。
・「TD32デバッグ情報を含める」をONに設定します。

以上の準備作業が完了したら、プログラムを実行します。例えば次のような、メモリリークの原因となるコードを含むアプリケーションを実行します。

type
  TMyClass = class
  end;

procedure TForm1.Button1Click(Sender: TObject);
var
  p: TMyClass;
begin
  p := TMyClass.Create; // 破棄されていない
end;

すると、プログラム終了時に、次のようなログファイルが生成されます。

MemCheck version 2.73

Total leak: 4 bytes


*** MEMCHK: Blocks STILL allocated ***

Leak #0 Instance of TMyClass
    Size: 4
    1 Occurence
    call stack - 0 : Module Unit1.pas Routine @Unit1@TForm1@Button1Click Line 35 Find error: 0044F027
    call stack - 1 :  Routine @Controls@TControl@Click Find error: 0042F88A
    call stack - 2 :  Routine @Controls@TWinControl@WndProc Find error: 004325FC
    call stack - 3 :  Routine @Controls@DoControlMsg Find error: 00432734
    call stack - 4 :  Routine @Controls@TWinControl@WndProc Find error: 004325FC
    call stack - 5 :  Routine @Controls@TWinControl@MainWndProc Find error: 00432277
    call stack - 6 :  Routine @Classes@StdWndProc Find error: 0041B392
    call stack - 7 : (no debug info) Find error: 77CF8730
    call stack - 8 : (no debug info) Find error: 77CF8812
    call stack - 9 : (no debug info) Find error: 77CFB897
    call stack - 10 : (no debug info) Find error: 77CFB8FF
    call stack - 11 : (no debug info) Find error: 77D2FC79
    call stack - 12 : (no debug info) Find error: 77D264E4
    call stack - 13 : (no debug info) Find error: 77D077DA
    call stack - 14 : (no debug info) Find error: 77D1B056
    call stack - 15 : (no debug info) Find error: 77CF8730
    call stack - 16 : (no debug info) Find error: 77CF8812
    call stack - 17 : (no debug info) Find error: 77CFC63B
    call stack - 18 : (no debug info) Find error: 77CFE901
    call stack - 19 :  Routine @Controls@TWinControl@DefaultHandler Find error: 004326E0
    call stack - 20 :  Routine @Controls@TWinControl@WndProc Find error: 004325FC
    call stack - 21 :  Routine @Classes@StdWndProc Find error: 0041B392
    call stack - 22 : (no debug info) Find error: 77CF8730
    call stack - 23 : (no debug info) Find error: 77CF8812
    call stack - 24 : (no debug info) Find error: 77CF89C9
    call stack - 25 : (no debug info) Find error: 77CF96C3
    call stack - 26 :  Routine @Forms@TApplication@ProcessMessage Find error: 0044D694

*** MEMCHK: End of allocated blocks ***


*** MEMCHK: Chronological leak information ***

* Instance of TMyClass (Leak #0) Size: 4

*** MEMCHK: End of chronological leak information ***


*** MEMCHK: Blocks written to after destruction ***

    Bad blocks count: 0


*** MEMCHK: End of blocks written to after destruction ***

ログファイルを見れば分かるように、MemCheckは、メモリリークが発生したファイルの行番号をレポートします。これにより、簡単にメモリリーク箇所を特定し、対策を立てることができます。

    Delphi 2006のFastMMメモリマネージャ

Delphi 2006(Borland Developer Studio 2006)では、MemCheckを使用することができません。その理由は、Delphi 2006のコンパイラの仕様が変更されたことと、新たに導入されたメモリマネージャFastMMによりメモリ管理の仕組みが変更されたためです。

FastMMがどのようにDelphiアプリケーションのメモリを管理し、既存のアプリケーションにどのような影響をもたらすかについては、「Borland Developer Studio 2006の新しいメモリマネージャ」を参照してください。

FastMMは、標準でメモリリーク検出機能を備えています。アプリケーションに以下のコードを一行追加するだけで、FastMMを使用したDelphiアプリケーションは、終了時にメモリリークを報告します。

begin
  ReportMemoryLeaksOnShutdown := True; // これだけ
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

ただし、Delphi 2006に付属するFastMMは、メモリリークの検出を報告するダイアログボックスを表示するだけで、どの行でメモリリークが発生したかまでは報告しません。

Hide image

FastMMのメモリリーク検出

ただし、Delphi 2006に付属するFastMMは、メモリリークの検出を報告するダイアログボックスを表示するだけで、どの行でメモリリークが発生したかについてまでは報告しません。

    フルデバッグモードのFastMM

FastMMは、http://fastmm.sourceforge.net で公開されているDelphi Win32向けのメモリマネージャです。SourceForgeのダウンロードページ http://sourceforge.net/project/showfiles.php?group_id=130631 からプロジェクトをダウンロードすれば、最新かつフルセットのFastMMを入手できます。

ダウンロード版FastMMに含まれるフルデバッグモードFastMMを使用すれば、メモリリーク検出で、その発生箇所を特定できる詳細なログファイルを出力できます。

フルデバッグモードのFastMMを使用するには、次のようにします。

・usesに ShareMemユニット を追加して、borlndmm.dll(共用メモリマネージャ) がロードされるように変更します。

program XXXX;

uses
  ShareMem, // これだけ
  Forms,
  Unit1 in 'Unit1.pas' {Form1},
…
begin
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

・ダウンロードしたFastMMの「Debug版 BorlndMM.dll」がロードされるようにします
(FastMM_FullDebugMode.dll)が実行時に必要)。

・「スタックフレームの生成」をONに設定します。
・「TD32デバッグ情報を含める」をONに設定します。
・アプリケーション実行時には、Borland Developer Studio 2006のIDEを起動しておきます。

以上の準備が完了したら、プログラムを実行します。プログラムを実行すると、終了時にダイアログボックスが表示されます。以上で、以下に示したような、borlndmm_MemoryManager_EventLog.txt というファイル名のログファイルが出力されます。

-- borlndmm_MemoryManager_EventLog.txt ----------------------
A memory block has been leaked. The size is: 4

Stack trace of when this block was allocated (return addresses):
402C9A [System][@GetMem]
40383B [System][TObject.NewInstance]
403BAA [System][@ClassCreate]
403870 [System][TObject.Create]
403A33 [System][@IsClass]
453693 [Unit1.pas][Unit1][TForm1.Button1Click][34]
437052 [Controls][TControl.Click]
426537 [StdCtrls][TButton.Click]
426635 [StdCtrls][TButton.CNCommand]

The block is currently used for an object of class: TMyClass
…
…

プログラム終了時に、まれにAVエラーが発生してプログラムが停止してしまうことがあります。この場合でも、ログファイルは生成されていますので、タスクマネージャやIDE上で強制終了させてかまいません。

    FastMMの効果

本稿では、主にDelphiアプリケーションのメモリリーク検出手順について取り上げましたが、FastMMが具体的にどの程度メモリ管理を最適化しているかについて、市販のプロファイリングツールを使用してテストしてみたいと思います。

ここで使用するのは、Lightweight Technologies社のLTProfという49.95ドルのDelphi/C++Builderに対応したCPUプロファイラ製品です。TD32デバッグ情報を付加すれば、Delphi/C++Builderいずれのアプリケーションのプロファイリングも可能です。詳細は、http://www.lw-tech.com/ をご覧ください。

LTProfを使用するのは、簡単です。オプションで、「TD32デバッグ情報を含める」と「デバッグ版DCUを使う」をONに設定するだけです。アプリケーションを実行すると、図のようなプロファイリング情報を表示します。

Hide image

LTProfのプロファイリング情報

Delphi 7とDelphi 2006(Borland Developer Studio 2006)での、メモリマネージャの動作を比較するために、以下のようなコードを準備しました。

procedure TForm1.Button1Click(Sender: TObject);
var
  i,j:  Integer;
  s,e:  Cardinal;
  list: TStringList;
begin
  s := GetTickCount;
  for i := 1 to 100 do
  begin
    list := TStringList.Create;
    for j := 1 to 10000 do
    begin
      list.Add(IntToStr(J));
    end;
    FreeAndNil(list);
  end;
  e := GetTickCount;

  Label1.Caption := IntToStr(e-s);
end;

ここでは、TStringListを使った重いループ処理を記述し、メモリの再割当が頻繁に発生する事態を人工的に作り出しました。

実行結果は環境にも大きく依存すると思いますが、手元の環境で実行した場合、Label1に表示されるカウンターは、Delphi 7の場合1943、Delphi 2006の場合941と、2倍以上の開きが出ました。

Delphi 7の場合、大量のTStringList.Addに伴うメモリの再配置処理(ReallocMem、Move)が発生し、これが最も大きいウェイトを占めています。IntToStrメソッド(Sysutils::CvtInt)は、メモリ再配置処理の次に時間がかかる処理でした。

Hide image

Delphi 7のプロファイリング結果

一方、Delphi 2006では、IntToStr(Sysutils::CvtInt)が最も大きいウェイトを占めています。Sysutils::CvtIntの実装内容は、Delphi7とBDS2006とで同一であるため、この差は、単純に、TStringList.Addに伴うメモリの再配置処理が激減し、結果としてIntToStrが目立つことになった、言い換えれば、メモリの再配置処理で著しいパフォーマンス向上があったということです。

Hide image

Delphi 2006のプロファイリング結果

    まとめ

このように、フリーで入手できるツールや廉価なツールを組み合わせれば、Delphiアプリケーションの品質向上を効率的に行うことができます。アプリケーションの品質を高めるには、コーディングの定石を覚えたり、背後のアーキテクチャを理解することも重要ですが、ケアレスミスや発見しにくいミスはなかなか防ぐことはできません。このようなケースにも対応できるように、メモリリーク検出の手立てを講じておくことは重要です。


関連情報:

第2回デベロッパーキャンプ – 資料ダウンロード

次からのサーバー応答:: ETNASC03