ILEのヒープ・メモリ管理をマスターする
ヒープ・メモリを使ってアプリケーションをより効率的に実行する
アプリケーションをモダン化する際に、ILEに変換するにしても、ビジネス・ロジックを変更するにしても、モジュール化されたアプリケーションを作成するにしても、管理しなければならない基本的な資源の1つが動的メモリです。このタイプのメモリは、一般的にヒープ・ベース・メモリあるいは簡単にヒープ・メモリと呼ばれています。本稿ではヒープ・メモリとは何かについて説明し、ヒープ・メモリを使用すると便利なのはどんな時か、C、C++、RPG等のILE言語でヒープ・メモリを正しく使用する方法について説明します。また、ヒープ・メモリを使用する際に一般的に起こる問題についても説明し、そのような問題をデバッグするのにIBM iが提供するサポートの利用方法について説明します。
ヒープ・メモリとは
ヒープ・メモリは、アプリケーション内で動的にメモリを割り当てる際に使用される、自由に使用できるメモリの共用プールです。動的メモリ割り当てとは、アプリケーションの実行時に使用するためにメモリ(または記憶域)を割り当てることです。動的に割り当てるメモリは、アプリケーションが明示的に割り当てて解除しなければなりません。ヒープ・メモリという用語は、動的メモリ割り当ての実装方法のほとんどが、ヒープと呼ばれるバイナリ・ツリー・データ構造を使用しているためにそう呼ばれています。
ヒープに対して実行される命令は主に2つあります。1つ目は割り当て命令です。この命令は指定されたサイズの記憶域のブロックを確保し、確保されたブロックへのポインタを返します。アプリケーションはこの記憶域のブロックに対して排他的な所有権を付与されます。つまり、アプリケーションはその必要とする目的が何であれ、この記憶域のブロックを使用することができます。2つ目の命令は解除です。この命令は、与えられた(以前にアプリケーションによって割り当てられた)記憶域のブロックをヒープに返します。記憶域のブロックが解除されるとそのブロックの所有権はヒープに戻されます。この記憶域のブロックはその後、また割り当てに再利用することもできます。
ヒープ機能が提供する一般的な命令の3つ目は再割り当てです。この命令は、与えられた記憶域のブロックのサイズを変更してその記憶域のブロックの(おそらく更新されている)ポインタを返します。この命令は基本的な割り当て命令と解除命令を組み合わせれば実装できるので、必ずしも必要な命令ではありませんが、ヒープ機能では良く提供されています。
IBM iには、シングル・レベル記憶域とテラスペース記憶域の2つのタイプのヒープ記憶域があります。アプリケーションが使用する記憶域のタイプは、プログラム作成時に指定するプログラム属性で決定します。デフォルトではシングル・レベル記憶域が使用されます。テラスペースの記憶域を使用するには、特別なコンパイル・オプションかプログラム作成オプションを使用する必要があります。さらに、シングル・レベル記憶域のアプリケーション内からAPIを使用してテラスペースの記憶域を割り当てたり解除したりすることもできます。これら2つのタイプの記憶域の詳細については、ILEの概念マニュアルの第4章を参照してください。
この二つのタイプのヒープ記憶域にはいくつか重要な違いがあります。ヒープからのシングル・レベル記憶域の割り当てサイズは最大で16メガバイトです。ヒープからのテラスペース記憶域の割り当てサイズは数テラバイト可能です。シングル・レベルの記憶域のアドレスには16バイトのポインタを使用しなければなりません。テラスペースの記憶域用のアドレスには、8バイトのポインタをサポートしている言語(CやC++)のために8バイトのポインタが使用可能です。シングル・レベル記憶域のヒープのサイズは1つのジョブ当たり4ギガバイトが上限です。テラスペースのヒープにはそのような制限はありません。テラスペース記憶域のヒープのサイズは、システムの記憶域で利用可能なサイズの制限を受けるだけです。
なぜヒープ・メモリを使用するのか?
ハイレベルのプログラミング言語を使用してアプリケーションを開発する際に、実装する上で決めなければならない項目の1つにメモリの管理をどのように行うのかがあります。サイズの小さなプログラム変数は、通常は静的(大域)メモリや自動(スタック・ベースの)メモリ内に割り当てられます。こうした変数用の記憶域は言語のランタイム・システムによって割り当てられたり解除されたりするので、プログラマは必要な記憶域を明示的に管理する必要はありません。この方法は、固定長で比較的サイズの小さな変数(つまり1つあるいは少数のハードウェア・レジスタ内に収まる変数)についてはうまく扱うことができます。
静的記憶域または自動記憶域内に割り当てられる変数のサイズは、コンパイル時にわかっていなければなりません。変数のサイズがもっと大きいあるいは可変長である場合は、そのような変数をより効率的に持つには通常はヒープ・メモリとなります。サイズの大きな変数を静的あるいは自動記憶域に割り当てることも可能ですが、アプリケーションに不必要なオーバーヘッドが生じることがあります。たとえば、静的記憶域に大きなサイズの変数を割り当てると、アプリケーションがその変数を必要とする期間が短い場合には無駄になります。静的変数の記憶域はアプリケーションの起動時に割り当てられ、アプリケーションが終了するまで解除されません。自動記憶域にサイズの大きな変数を割り当て、関数が再帰呼び出しをしていたり、自動記憶域にサイズの大きな変数を持っている他の関数をたくさん呼び出したりすると、プログラムのコール・スタックのサイズに問題が生じる場合があります。
たとえば、ファイルから要約レポートを生成するアプリケーションがあったとしましょう。このアプリケーションはファイル内にあるレコードを順番に一度に1件ずつ処理します。おそらくアプリケーションの開発時にはレコードのサイズはわかっているでしょうし、それ故に処理されるレコード1件を格納する1つの変数を自動または静的記憶域に割り当てることができます。アプリケーションは現在のレコードをこの変数に読み込んで処理し、次のレコードを読んで処理する、という具合に続けていきます。レコード・サイズが非常に大きくても、1つの静的変数を効率的に使用することができます。このようなタイプのアプリケーションでは、静的あるいは自動的データで十分です。
しかし、指定されたストリーム・ファイル中のテキスト・データをすべて処理しなければならないアプリケーションを考えてみてください。このアプリケーションは、ファイル全体を読み込んでファイル中のデータを処理する必要があります。このアプリケーションで静的データを使用するには1つの静的変数が必要となるでしょうが、そのサイズは入力ファイルとして可能性のある最大のものと同じだけ大きなものとなってしまいます。これでは、サイズの小さなファイルに対しても大量のメモリが割り当てられて、そのメモリのほんの一部だけを使用することになりますから、記憶域の無駄使いと言えます。入力ファイルとして可能性のある最大のサイズを決定するのも難しいかもしれません。アプリケーションが選択したサイズが十分な大きさでない場合、入力ファイルのサイズに勝手な制限を加えることになります。入力ファイルを処理するアプリケーションとしてもっとずっと効率的な方法は、入力ファイルのサイズを調べてからその入力ファイルを読み込むのに必要なヒープ記憶域の量を割り当てるというものです。この方法では必要な量の記憶域だけが要求され、アプリケーションがファイルのサイズに対して勝手な制限をかけることもありません。このタイプのアプリケーションにはヒープ・メモリを使用するのが理に適っています。
ヒープ・メモリの割り当てと解除の方法
ヒープ・メモリはすべてのILE言語で使用可能ですが、各言語における割り当て、解除の方法は異なります。C言語ではmalloc()関数とfree()関数が使用され、C++言語ではnew命令とdelete命令が、RPGでは%ALLOC組み込み関数とDEALLOC命令が使用されます。COBOLとCLにはヒープ・メモリを管理する組み込み関数はありませんが、ILEモデルを使用すると任意のILE言語からその関数を呼び出すことができるので、COBOLやCLでもmalloc()関数やfree()関数を呼び出してヒープ・メモリを管理することができます。実際、すべてのILE言語でmalloc()関数やfree()関数を呼び出してヒープ・メモリを管理することができます。図―1a、図―1b、図―1c、図―1dおよび図―1eの例はこれらすべての言語でヒープ・メモリを割り当て、使用、解除しているところを示しています。
これらの例は、少量のヒープ記憶域(16バイト)しか割り当てていません。通常は、割り当てられる記憶域のブロックはずっと大きなサイズです。例として、CL変数の最大サイズが32767バイトであるという事実を考慮してみてください。ヒープ記憶域と*PTR変数を使用すると、CL言語内でもっと大きなブロックの記憶域を管理することができます。
ヒープ・メモリに関する一般的な問題
ヒープの割り当ておよび解除はアプリケーションが明示的に実行しなければなりませんので、これらの命令を誤って使用する可能性があります。ヒープ・メモリを誤って使用する一般的なシナリオには、割り当てられている記憶域のサイズよりも大きなデータを書きだしてしまうこと(メモリ・オーバーライト)、割り当てられた記憶域のサイズよりも大きな量のデータを読み込んでしまうこと(メモリ・オーバーリード)、すでに解除した記憶域への書き込みや記憶域からの読み込み(解除したメモリの再利用)、記憶域の複数回解除(重複解除)、使用されなくなったメモリの解除忘れ(メモリ・リーク)などです。こうしたヒープ・メモリの使用上の問題を説明したプログラム例を見てみましょう。
上記の最初の2つのシナリオはヒープ割り当てのサイズに関連しています。ヒープ・メモリ・オブジェクトのサイズが割り当て時にヒープ機能に与えられ、ヒープ機能は要求されたサイズを満たすのに十分な記憶域のブロックを返します。もしアプリケーションがヒープのサイズを誤って計算してしまったら、現在の割り当てには属していないヒープの部分から無意識にデータを読んだりデータを書き込んだりしてしまうことがあります。これではプリケーションに問題が生じることがありますし、実際にしばしば問題が発生しています。
図―2にC++でのメモリ・オーバーライトの例を示します。16バイトのサイズしかないヒープ割り当てに合計17バイトのデータを書き込んでいます。strcpy()関数が16バイトの文字列をコピーしているだけでなく、後ろに続く0バイト(NULL文字)もコピーしています。
図―3はC言語でのメモリ・オーバーライトの例を示しています。13バイトしかないヒープ割り当てに合計14バイトのデータを書いています。strcpy()関数が13バイトの文字列をコピーしているだけでなく、後ろに続く0バイト(NULL文字)もコピーしています。
図―4はRPGでのメモリ・オーバーライトの例を示しています。
13バイトしかないヒープ割り当てに合計14バイトのデータを書いています。ベース変数は14バイトです。
次のシナリオはヒープ記憶域をまだアプリケーションが使用しているのに、早く解除しすぎた例です。ヒープ記憶域が解除され、その記憶域が再利用のためにヒープ機能側に戻された後に、ロジックの間違いでアプリケーションが解除されたヒープ記憶域を参照してしまっています。このような割り当てで参照されたデータは、予想しているデータである場合もあるしそうでない場合もありますので、アプリケーション中で断続してエラーが発生する恐れがあります。図―5はC++言語で記述されており、もはや割り当てられていないヒープ記憶域に書き込んでいる例です。
次のシナリオは同じ記憶域を複数回解除している例です。図―6はC言語で書かれており、メモリの解除を重複して呼び出している例です。
最後のシナリオは、割り当てられたヒープ・メモリを解除し忘れるというものです。これはメモリがヒープから「リーク」してアプリケーションが使えなくなってしまうのでメモリ・リークと呼ばれています。そのメモリは2度と参照できませんが、ヒープ機能はそうなってしまったことを判断できず、そのメモリがまだアプリケーションによって使用されていると判断するため再利用ができません。メモリ・リークはしばらくすると大量のメモリがリークしてしまうので大きな問題となりえます。これによりパフォーマンス上の問題が発生したり、アプリケーションが使用できるメモリがなくなったりしてしまうこともあります。これは、サーバー・アプリケーションのように長期間にわたって実行し続けるアプリケーションの場合に特に問題となります。アプリケーション終了後のある時点(アクティベーション・グループの終了時)で、システムはそのアプリケーション用のすべてのヒープ記憶域を再要求しますので、その時点以降、メモリ・リークは問題となりません。
図―7はRPG言語で記述されており、メモリ・リークを説明した例です。アプリケーションは記憶域を解除しようとしていますが、解除関数に渡されたポインタ値が割り当て関数によって返された元のポインタではないので、メモリは解除されません。
ヒープの使用方法を誤るとアプリケーションが断続的にエラーを起こしたり、不正な動作をしたり、データを破壊したりすることさえあります。ヒープに関連した問題のデバッグを困難にしている特徴の1つに、ヒープ・エラーによってすぐにその影響が出ないことが往々にしてあることが挙げられます。例として、割り当てられたメモリ・バッファの領域を超えてデータが書かれてしまうというバッファ・オーバーライトを考えてみてください。問題の兆候は通常は、アプリケーションの実行が進んだずっと後になって、オーバーライトされた(通常は他の割り当てに属している)メモリを参照してそこに期待するデータが入っていないとわかった時に現れます。
IBM iヒープ・メモリ・マネージャ
IBM i 6.1およびそれ以後のリリースでは、デフォルト・メモリ・マネージャ、Quick Poolメモリ・マネージャ、デバッグ・メモリ・マネージャの3種類のヒープ・メモリ・マネージャが実装されています。アプリケーションにとってメモリ・マネージャは、実際は実行時にQIBM_MALLOC_TYPE環境変数の設定によって制御されます。代替えのヒープ・メモリ・マネージャへの環境変数アクセスはリリース6.1のPTF 5761SS1-SI33945で提供され、それ以後のIBM iのリリースに含まれています。デバッグ・メモリ・マネージャは、環境変数アクセスが追加されたとの同じ時に追加されました。Quick Poolメモリ・マネージャもIBM i 5.4および6.1でAPIを呼び出してサポートを設定することで使用することができます。ヒープ・メモリ・マネージャのサポートの詳細な記述については、"マニュアルILE C/C++ ランタイム・ライブラリ関数"を参照ください。
デフォルト・メモリ・マネージャ
デフォルト・メモリ・マネージャは、汎用のメモリ・マネージャでパフォーマンスとメモリに対する要件のバランスを取ろうとします。デフォルト・メモリ・マネージャはほとんどのアプリケーションに対して十分なパフォーマンスを提供しながら、オーバーヘッドに必要な追加メモリの量を最小限に抑えようとします。デフォルト・メモリ・マネージャはほとんどのアプリケーションにとって好ましい選択肢で、デフォルトで有効になっているメモリ・マネージャです。QIBM_MALLOC_TYPE環境変数が設定されていない場合や認識できない値に設定されている場合は、デフォルト・メモリ・マネージャが使用されます。
Quick Poolメモリ・マネージャ
Quick Poolメモリ・マネージャはメモリを一連のプールに分割します。Quick Poolメモリ・マネージャは、小さなサイズの割り当て要求を大量に発行するようなアプリケーションに対してヒープのパフォーマンスを向上させるという意図で設けられたものです。Quick Poolメモリ・マネージャが有効になると、一定の範囲の割り当てサイズ以内の割り当て要求に対してプール内の固定サイズのセルが割り当てられます。こうした要求は、上記の範囲外のサイズに対する要求よりもずっと速く処理することが可能です。この範囲外の割り当て要求はデフォルト・メモリ・マネージャと同じ方法で処理されます。
Quick Poolメモリ・マネージャはデフォルトでは有効になっていませんが、以下の環境変数を設定することで有効にすることができます。
QIBM_MALLOC_TYPE=QUICKPOOL
また、Quick Poolメモリ・マネージャはアプリケーション内からAPIを呼び出すことでも有効にすることができます。詳細については"マニュアルILE C/C++ランタイム・ライブラリ関数"を参照ください。
デバッグ・メモリ・マネージャ
デバッグ・メモリ・マネージャは、主にアプリケーションの不正なヒープの使用を見つけるために使用します。デバッグ・メモリ・マネージャはパフォーマンス向上を目的とした最適化はされておらず、アプリケーションのパフォーマンスに悪影響を与えることがあります。しかし不正なヒープの使用を判定するには貴重なマネージャです。
デバッグ・メモリ・マネージャによって検知されるメモリの問題は、以下の2つの動作のいずれかを示します。
- メモリの問題が不正使用をした時点で検知された場合は、MCH例外メッセージ(MCH0601、MCH3402、MCH6801が典型的)が生成されます。このような場合、通常はエラー・メッセージによりアプリケーションが停止します。不正使用がされてから処理が進むまで問題が検知されない場合は、C2M1212メッセージが生成されます。この場合、メッセージによりアプリケーションが停止することは通常はありません。
デバッグ・メモリ・マネージャは以下の2つの方法でメモリの問題を検知します。
- まず、デバッグ・メモリ・マネージャはアクセスが制限されたメモリ・ページを使用します。アクセスが制限されたメモリ・ページを、割り当てられたメモリの前後に配置します。各メモリ・ブロックは16バイト毎の境界に揃えられており、ページの終わりにできるだけ近いところに置かれています。メモリ保護はページの境界上だけで許可されているので、16バイト毎の境界に揃えられていることでメモリのオーバーライトやオーバーリードをうまく検知することができます。このアクセスが制限されたメモリ・ページに対して読み出しや書き込みが行われると、すぐにMCH例外が発生します。
- 次に、デバッグ・メモリ・マネージャは各メモリ割り当ての前後にパディング・バイトを使用します。メモリを割り当てる際に、割り当てられたメモリの直前の数バイトがプリセットされたバイト・パターンに初期化されます。割り当てサイズが16バイトの倍数になるように概算するのに必要な、割り当てメモリに続くパディング・バイトは、メモリ割り当て時にプリセットされたバイト・パターンに初期化されます。この割り当てが解除されるときに、すべてのパディング・バイトに期待通りのプリセットされたバイト・パターンが依然として残っているかどうかを検証します。もしいずれかのパディング・バイトが修正されていた場合、デバッグ・メモリ・マネージャが理由コードX'80000000'でC2M1212メッセージを生成します。
デバッグ・メモリ・マネージャはデフォルトでは有効になっていませんが、以下の環境変数を設定することで有効にすることができます。
QIBM_MALLOC_TYPE=DEBUG
ヒープ・メモリに関する一般的な問題をデバッグする
ヒープ・メモリの使用に関する一般的な問題点とさまざまなヒープの問題を説明するためのプログラム例についていくつか前述しました。デバッグ・メモリ・マネージャを使用すると、メモリ・オーバーライト、メモリ・オーバーリード、解除されたメモリの再利用、重複解除などといったヒープ・メモリで発生する多くの一般的な問題を検知することができます。デバッグ・メモリ・マネージャはメモリ・リークを検知しません。(ILEアプリケーション内でのメモリ・リークの検知についてはまたの機会に触れる予定です。)
ヒープに関する問題について説明した前述のプログラム例を、デバッグ・メモリ・マネージャを使用して実行した際にどのような動きになるのかについて以下に説明します。
図―2はメモリ・オーバーライトを説明したプログラム例です。16バイトの倍数のサイズをもったデータ項目の終端を超えて書き込もうとしています。このプログラム例をシングル・レベル記憶域プログラムとしてコンパイルし、デバッグ・メモリ・マネージャを使用して実行すると、メモリ・オーバーライトが発生した時点でMCH0601メッセージが生成されます。このプログラム例をテラスペース記憶域プログラムとしてコンパイルし、デバッグ・メモリ・マネージャを使用して実行すると、メモリ・オーバーライトが発生した時点でMCH6801メッセージが生成されます。いずれの場合においても、メモリ・オーバーライトを実際に起こしている文を指摘する詳細がメッセージに書かれています。この例はメモリ・オーバーライトについて説明していますが、メモリ・オーバーリードについても同様の結果となります。
図―3はメモリ・オーバーライトを説明したプログラム例です。16バイトの境界を挟まないメモリ・オーバーライトが発生しています。このプログラム例をシングル・レベル記憶域プログラムあるいはテラスペース記憶域プログラムとしてコンパイルし、デバッグ・メモリ・マネージャを使用して実行すると、理由コードX'80000000'でC2M1212メッセージが生成されます。free()を呼び出している文を指摘する詳細がメッセージに書かれています。16バイトの境界を挟まないメモリ・オーバーリードはデバッグ・メモリ・マネージャでは検知されません。
図―4はメモリ・オーバーライトを説明したプログラム例です。デバッグ・メモリ・マネージャはRPGアプリケーションでは有効ではないので、メモリ・オーバーライトは検知されません。RPGの今後の機能強化で、デバッグ・メモリ・マネージャをRPGアプリケーション用に使用できるようになると思います。
図―5はもはや割り当てられていないヒープ記憶域に書き込もうとしているプログラム例です。このプログラム例をシングル・レベル記憶域プログラムとしてコンパイルし、デバッグ・メモリ・マネージャを使用して実行すると、メモリ・ライトが発生した時点でMCH3402メッセージが生成されます。このプログラム例をテラスペース記憶域プログラムとしてコンパイルし、デバッグ・メモリ・マネージャを使用して実行するとメモリ・ライトが発生した時点でMCH6801メッセージが生成されます。いずれの場合においても、メモリ・ライトを実際に起こしている文を指摘する詳細がメッセージに書かれています。この例はメモリ・ライトについて説明していますが、メモリ・リードについても同様の結果となります。
図―6 はメモリの解除を重複して呼び出しているプログラム例です。このプログラム例をシングル・レベル記憶域プログラムあるいはテラスペース記憶域プログラムとしてコンパイルし、デバッグ・メモリ・マネージャを使用して実行するとC2M1212メッセージが生成されます。free()を呼び出している文を指摘する詳細がメッセージに書かれています。
図―7はメモリ・リークを説明したプログラム例です。前述した通り、デバッグ・メモリ・マネージャはメモリ・リークを検知しません。
デバッグ・メモリ・マネージャを使用しない時は、メモリに関する上記の問題は検知されないままになり、アプリケーションが断続的にエラーを起こしたり、不正な動作をしたり、データを破壊したりすることがあります。
重要な知識
ヒープ・メモリとはどんなものか、どんなときにヒープ・メモリを使用すべきものなのか、ヒープ・メモリを正しく使用する方法は何かを理解することは、堅牢で効率的なILEアプリケーションを作成する際に非常に重要なことです。ヒープに関する一般的な問題点を認識し意識することで、デバッグ・メモリ・マネージャを使用してアプリケーション内のヒープに関する問題を容易に検知し、IBM i ILEアプリケーション内のメモリ・オーバーライト、メモリ・オーバーリード、解除メモリの再利用、重複解除などの問題に終止符を打つことができます。