PHP V5.2 の新機能、第 1 回: 新しいメモリー・マネージャーの使用方法

uber マニアのように PHP メモリー使用量を追跡、監視する

この「PHP V5.2 の新機能」シリーズの第 1 回では、PHP V5.2 に導入された新しいメモリー・マネージャーの使用方法を学んでください。メモリー使用量を追跡および監視する腕を磨けば、PHPV5.2 で一段と効率的にメモリーを使えるようになれます。

Tracy Peterson, Freelance Writer, Freelance Developer

Tracy Peterson は、経験豊富な LAMP 開発者であるとともに、ソフトウェア・プロジェクト・マネージャーでもあります。現在、カリフォルニア州サンホセ在住です。



2007年 3月 13日

PHP V5.2: はじめに

2006年11月、多数の新規機能とバグ修正が加えられた PHP V5.2 がリリースされました。これを機に 5.1 リリースは廃止され、PHPV5 ユーザーにはアップグレードが推奨されています。私が気に入っているラボ環境 (Windows®、Apache、MySQL、PHP(WAMP)) はすでに V5.2 対応の新規パッケージに移行しているので (「参考文献」を参照)、この環境では Windows® XP または 2003 のマシンに PHP V5.2、MySQL、そして Apache をセットアップするアプリケーションが見つかるはずです。このアプリケーションは、インストールするのが至って簡単で、細かなところまで行き届いたさまざまな管理機能が備わっていて、心底からお勧めできます。

このパッケージは、Windows ユーザーにとっては極めて簡単なパッケージですが、Linux で PHP を構成する場合には (サーバーごとに適切なオプションとは別に)--memory-limit-enabled を追加する必要があります。一方、Windows では次善策となる関数が用意されています。

PHP V5.2 ではさまざまな改良が行われていますが、なかでも極めて重要な分野はメモリー管理の改良です。README.ZEND_MM の記述をそのまま引用すると、「新しいメモリー・マネージャー(PHP5.2 以降) の目標は、メモリー割り当てのオーバーヘッドを減らし、メモリー管理を迅速化することです」。

以下に、V5.2 リリース・ノートに記載されている重要な項目をいくつか挙げます。

  • 不要な--disable-zend-memory-manager構成オプションを削除
  • デバッグ・ビルドで有効にデフォルト設定され、内部および外部メモリー・デバッガーを使用可能にする--enable-malloc-mm構成オプションを追加
  • ZEND_MM_MEM_TYPE および ZEND_MM_SEG_SIZE環境変数によるメモリー・マネージャーの微調整を許可

これらの新規機能が意味することを理解するには、高度なメモリー管理技術について少々掘り下げて考えてから、メモリー割り当てのオーバーヘッドと迅速化が重大な理由を検討する必要があります。


メモリー管理が必要な理由

コンピューティングにおいて最も開発が進んでいる技術の 1 つとして数えられるのは、メモリーとデータ・ストレージです。それは、処理速度とストレージ・サイズを絶えず増やしていかなければならないという必要性に駆られているためです。チップ技術に以降する以前の初期のコンピューターでは、カードがメモリーとして使用されていました。わずか1 KB の RAM しかないコンピューターで作業するなど今でこそ想像できませんが、初期のコンピューター・プログラマーの多くはそうしてきました。そんな先駆者たちが即刻実感するようになったのは、技術の制限の中で作業するには、できる限り不要なコマンドでシステムに負荷をかけないようにすることが必須だということです。

私をはじめとする PHP 開発者たちは、C++ などの厳格な言語でコードを作成している同僚よりもはるかにコードの作成が楽な世界にいます。PHPの世界では、開発者がシステムのメモリー処理に関与する必要はありません。なぜなら PHP が代わって対処してくれるからです。その一方、PHP 以外のプログラミングの世界では、担当のコーダーがあらゆる関数を駆使して、実行するコマンドが他のプログラムのデータを上書きしないようにしています。そうでないと、実行中のプログラムが機能しなくなってしまうことがあるためです。

メモリー管理を処理するのは通常、コーダーによるメモリーのブロック割り当て要求と解放要求です。割り当てられたブロックにはあらゆるタイプのデータを保持できます。このプロセスによって、保持されたデータのためだけに特定のメモリー量がブロックされるため、プログラムは操作のためにこのデータへのアクセスが必要になったときにデータのアドレスを指定できるようになります。プログラムは操作を完了すると割り当てられたメモリーを解放し、システムや他のプログラムがそのメモリーを使用できるようにします。プログラムがメモリーをシステムに解放しない場合、これはリークと呼ばれます。

リークは実行中のプログラムにはありきたりな問題なので、通常はある程度許容できます。例えば、実行中のプログラムがまもなく終了し、デフォルトで割り当てられたメモリーがすべて解放されることがわかっている場合などはなおさらです。

一方、ほとんどすべてのクライアント・アプリケーションのように、任意の時点で実行され、任意の時点で終了されるアプリケーションとなると話は別です。また、サーバー・アプリケーションは終了することも再起動することもなく無期限に実行されるため、メモリー管理はサーバー・デーモンのプログラミングにとって極めて重要な問題となります。リークによってメモリー・ブロックが解放されずに使用中のままになってしまうことから、長期にわたって実行されるプログラムでは、ほんの些細なメモリー・リークでさえもシステムを弱体化させる問題に発展します。


長期的な視野

他の言語の場合と同じく、PHP で作成された永続的サーバー・デーモンには多くの用途が考えられます。ただし、いずれの用途にしても、PHP を使い始める際にはメモリー使用量についても考慮しなければなりません。

大量のデータを解析するスクリプトや無限ループが潜んでいるスクリプトは、かなりのメモリー量を消費しがちです。メモリーが使い果たされると当然、サーバーのパフォーマンスが低下するので、スクリプトを実行するときにはどれだけのメモリーを使用しているかに注意を払う必要があります。スクリプトが使用するメモリー量は、システム・モニターをオンにするだけで監視できますが、これによってわかるのはシステム・メモリー全体の状況に過ぎません。そのためトラブルシューティングや最適化に役立てるには、モニターで監視するだけでは足りないことがあります。つまり、それ以上に詳細な情報が必要だということです。

スクリプトの実行内容を詳細に知る 1 つの手段としては、内部または外部デバッガーが使用されます。内部デバッガーとは、スクリプトを実行しているプロセスと同じプロセスのように見えるデバッガーのことです。OSの観点からすると別個のプロセスとなるデバッガーは、外部デバッガーと呼ばれます。いずれのデバッガーを使うにしてもメモリーの分析方法に違いはありませんが、メモリーへのアクセス方法は異なります。内部デバッガーは実行中のプロセスと同じメモリー空間に直接アクセスする一方、外部デバッガーはソケットを介してメモリーにアクセスします。

開発の手助けとなるメソッドは多数あり、使用可能なデバッグ・サーバー (外部) とライブラリー (内部) も用意されています。デバッグ用の PHPシステムを準備するのに使用できるのは、新しく提供された --enable-malloc-mm です。--enable-malloc-mm は、DEBUG ビルドで有効にデフォルト設定されます。これによって使用可能になる環境変数 USE_ZEND_ALLOC を使って、実行時にメモリー割り当てとして malloc または emalloc を選択することができます。malloc タイプのメモリー割り当てでは、外部デバッガーでメモリー使用量を監視できます。一方、emallocタイプの割り当てでは Zend メモリー・マネージャーの抽象化を利用するため、内部デバッグが必要となります。


PHP でのメモリー管理関数

PHP V5.2 ではメモリー・マネージャーの柔軟性と透過性が改善されているだけでなく、メモリー使用量を表示するために使用する memory_get_usage() および memory_get_usage() に新しいパラメーターも用意されています。リリース・ノートに記載されているこの新しいブール値は、real_size です。memory_get_usage($real); 関数を $real = true に設定して呼び出すと、その結果は、呼び出した時点でシステムから割り当てられているメモリーの実サイズ (メモリー・マネージャーのオーバーヘッドを含む)となります。フラグをセットしない場合に返されるデータは、メモリー・マネージャーのオーバーヘッドを差し引いた、実行中のスクリプト内でのみ使用されているメモリーとなります。

memory_get_usage()memory_get_usage() の違いは、前者はこの関数を実行した時点でのメモリー使用量を戻す一方、後者が戻すのは、この関数を呼び出した実行中のプロセスがそれまでに達したピーク・メモリー使用量であるという点です。

リスト 1 は、php.net が提供している memory_get_usage() のコード・スニペットです。

リスト 1. memory_get_usage() の例
<?php

// This is only an example, the numbers below will 
// differ depending on your system

echo memory_get_usage() . "\n"; // 36640
$a = str_repeat("Hello", 4242);
echo memory_get_usage() . "\n"; // 57960
unset($a);
echo memory_get_usage() . "\n"; // 36744

?>

上記の単純な例では、まず memory_get_usage() をそのまま呼び出した結果をエコー出力しています。コードの注釈によると、作成者のシステムでは結果は常に 36640 バイトになるはずです。次に、「Hello」のコピー数を4,242 に設定して $a をロードし、関数を再度実行しています。この単純な使用量の出力は、図 1 のようになります。

図 1. memory_get_usage() の出力例
図 1. memory_get_usage() の出力例

memory_get_peak_usage()memory_get_usage() と非常によく似ているため、php.net に例は記載されていませんが、構文はまったく同じです。リスト 1 のサンプル・コードの場合、結果は1 つだけで、それはその時点でのピーク・メモリー使用量となります。今度はリスト 2 を見てください。

リスト 2. memory_get_peak_usage() の例
<?php

// This is only an example, the numbers below will 
// differ depending on your system

echo memory_get_peak_usage() . "\n"; // 36640
$a = str_repeat("Hello", 4242);
echo memory_get_peak_usage() . "\n"; // 57960
unset($a);
echo memory_get_peak_usage() . "\n"; // 36744

?>

リスト 2 のコードはリスト 1 とよく似ていますが、memory_get_usage() の代わりに memory_get_peak_usage() が使用されています。「Hello」の 4242 回の反復を入力するところまでは、出力にほとんど変わりはありません。ここで、メモリー使用量は57960まで一気に増え、これまでのピークを表しています。ただし、memory_get_peak_usage() でメモリー使用量のピークを調べると、これまでの最大値が示されます。そのため、$a で使用した以上のメモリーを使用するような何らかの操作を実行するまで、以降の呼び出しでは結果は常に 57960 となります (図 2 を参照)。

図 2. memory_get_peak_usage() の出力例
図 2. memory_get_peak_usage() の出力例

メモリー使用量を制限する

アプリケーションをホストしているサーバーを酷使しないようにする確実な手段の 1 つは、PHP が実行するスクリプトに対して、最大許容メモリー使用量を設定することです。これは絶対的に必要なことではありませんが、弱い型付き言語であるPHPは実行時に解析されるため、作成したスクリプトに不備があると、実動アプリケーションでスクリプトを抑制できなくなることがあります。このようなスクリプトはループを発生させたり、あるいは新しいファイルを開く前に現行のファイルを閉じ忘れて膨大な数のファイルを開いたりする可能性があります。いずれにしても、不完全なスクリプトは知らないうちに大量のメモリーを消費するのがおちです。

PHP.INI では、構成パラメーター memory_limit を使って、システム上でスクリプトが実行可能な最大メモリー量を指定できます。これは V5.2 に固有の変更ではありませんが、メモリー・マネージャーとその使用方法を説明するときには、この機能にほんの少しでも触れないわけにはいきません。また自然な話しの流れとして、この機能は最後に紹介するメモリー・マネージャーの新しい機能である、環境変数につながっていきます。


メモリー・マネージャーを微調整する

最終的にプログラミングに必要不可欠となるのは、完璧主義者となって目的を完全に達成する能力ですが、まさにそれを可能にするのが新しい環境変数、ZEND_MM_MEM_TYPEZEND_MM_SEG_SIZE です。

メモリー・マネージャーが大規模なメモリー・ブロックを割り当てる際には、変数 ZEND_MM_SEG_SIZE にリストされた規定のサイズで割り当てます。これらのメモリー・セグメントのデフォルト・サイズは、ブロックあたり 256 KB ですが、この値は必要に応じて調整することができます。例えば、よく使用するいずれかのスクリプトの操作によって大量のメモリーが無駄になっていることに気付いた場合、空のメモリーをそのままにするのではなく、メモリーの割り当て量を減らしてスクリプトに必要なサイズに近くなるように調整します。条件が合っていれば、構成をこのように慎重に微調整することによって大きな違いが出てくるはずです。


Windows でのメモリー使用量を読み出す

PHP の Windows 用バイナリーが事前ビルドされたもので、ビルド時に --enable-memory-limit オプションが有効になっていなかった場合は、このセクションを読んでから先に進んでください。Linux® の場合は、PHP ビルドを構成する際に--enable-memory-limit オプションを有効にして PHP をビルドします。

Windows 用バイナリーを使ってメモリー使用量を読み出すには、以下の関数を作成します

リスト 3. Windows でのメモリー使用量の取得
<?php			

function memory_get_usage(){
    $output = array();
    exec('tasklist /FI "PID eq '.getmypid().'" /FO LIST', $output );
    return preg_replace( '/[^0-9]/', '', $output[5] ) * 1024;
}

?>

この関数は function.php ファイルに保管してください。後は、このファイルを使用したいスクリプトに組み込めばいいだけです。


実践に移す

ここからは、以上の設定をうまく利用する実際的な例を見ていくことにします。よくある事態として、スクリプトの終了時にメモリーの割り当てがどういうわけか正しく解放されていないことがあります。原因は、一部の関数自体がメモリー・リークを引き起こしているためです。とくに組み込みPHP 関数だけを使用している場合は、リークが発生することがよくあります。そこで取り上げるのが、このような問題を突き止める方法です。メモリー・リークを検出する作業は、リスト4 に示すテスト用 MySQL データベースを作成するところから始めます。

リスト 4. テスト用データベースの作成
mysql> create database memory_test;

mysql> use memory_test;

mysql> create table leak_test
       ( id int not null primary key auto_increment,
         data varchar(255) not null default '');

mysql> insert into leak_test (data) values ("data1"),("data 2"),
       ("data 3"),("data 4"),("data 5"),("data6"),("data 7"),
       ("data 8"),("data 9"),("data 10");

上記のコードによって、ID フィールドとデータ・フィールドを持つ単純なテーブルが作成されます。

次のリストでは、大胆なプログラマーが mysql_query() を使用して結果を変数に適用するなど、いくつかの MySQL 関数を実行していると想像してください。このプログラマーは関数を実行するうちに、mysql_free_result() を呼び出してもメモリーの一部が解放されないため、Apache プロセスのメモリー使用量が増えていくことに気付きました (リスト 5 を参照)。

リスト 5. メモリー・リークの検出例
for ( $x=0; $x<300; $x++ ) {
    $db = mysql_connect("localhost", "root", "test");
    mysql_select_db("test");
    $sql  = "SELECT data FROM test"; 
    $result = mysql_query($sql); // The operation suspected of leaking
    mysql_free_result($result);
    mysql_close($db);
 }

リスト 5 は、どこにでもあるような単純な MySQL データベース操作です。スクリプトを実行してみると、メモリー使用量に関する異常な動作が見つかりました。この異常な動作は調べないわけにはいきません。メモリー管理関数を使ってエラーがどこで発生しているかを確認するには、以下のコードを使用します。

リスト 6. 問題の原因を見つけるための処理の例
<?php

if( !function_exists('memory_get_usage') ){ 
    include('function.php');
}

echo "At the start we're using (in bytes): ",
     memory_get_usage() , "\n<br>";
    
$db = mysql_connect("localhost", "user", "password");
mysql_select_db("memory_test");

echo "After connecting, we're using (in bytes): ",
     memory_get_usage(),"\n<br>"; 

for ( $x=0; $x<10; $x++ ) {
    $sql  =
        "SELECT data FROM leak_test WHERE id='".$x."'"; 
    $result = mysql_query($sql); // The operation
                                 // suspected of leaking.
    echo "After query #$x, we're using (in bytes): ",
         memory_get_usage(), "\n<br>";
    mysql_free_result($result);
    echo "After freeing result $x, we're using (in bytes): ",
         memory_get_usage(), "\n<br>";
}
  
mysql_close($db);
echo "After closing the connection, we're using (in bytes): ",
     memory_get_usage(), "\n<br>";
echo "Peak memory usage for the script (in bytes):".
     memory_get_peak_usage();

?>

現行のメモリー使用量のチェックは、指定の間隔で行っています。メモリー・リークの陽性テストによる以下の出力を見ると、スクリプトが必要なタイミングでメモリーを解放せずに関数にメモリーを割り当て続けているため、呼び出しごとにメモリー使用量が増えていることがわかります。

リスト 7. テスト・スクリプトの出力
At the start we're using (in bytes): 63216
After connecting, we're using (in bytes): 64436
After query #0, we're using (in bytes): 64760
After freeing result 0, we're using (in bytes): 64828
After query #1, we're using (in bytes): 65004
After freeing result 1, we're using (in bytes): 65080
After query #2, we're using (in bytes): 65160
After freeing result 2, we're using (in bytes): 65204
After query #3, we're using (in bytes): 65284
After freeing result 3, we're using (in bytes): 65328
After query #4, we're using (in bytes): 65408
After freeing result 4, we're using (in bytes): 65452
After query #5, we're using (in bytes): 65532
After freeing result 5, we're using (in bytes): 65576
After query #6, we're using (in bytes): 65656
After freeing result 6, we're using (in bytes): 65700
After query #7, we're using (in bytes): 65780
After freeing result 7, we're using (in bytes): 65824
After query #8, we're using (in bytes): 65904
After freeing result 8, we're using (in bytes): 65948
After query #9, we're using (in bytes): 66028
After freeing result 9, we're using (in bytes): 66072
After closing the connection, we're using (in bytes): 65108
Peak memory usage for the script (in bytes): 88748

ここで行った手順を説明すると、まずスクリプトを実行して問題のあるアクティビティーが見つかったので、わかりやすいフィードバックを戻すスクリプトを作成しました。それからmemory_get_usage() を使用してテスト・スクリプトを再度実行し、それぞれの繰り返し処理中にメモリー使用量がどのように変化するかを調べました。メモリー割り当て値の増加は、スクリプトのどこかしらが原因でリークが発生していることを示唆します。mysql_free_result()関数でメモリーが解放されていないことから、mysql_query() が誤ってメモリーを割り当てていると想定できます。


まとめ

PHP V5.2 リリースには、スクリプトのシステム・メモリー割り当てをより詳細に理解し、メモリー管理全体を微調整するための素晴らしいツールの数々が含まれています。これらの新しいメモリー管理ツールを効果的に使用すれば、システム・リソースを再利用するためのデバッグ作業の助けとなるはずです。

参考文献

学ぶために

製品や技術を入手するために

  • IBM ソフトウェアの試用版を使用して、次のオープン・ソース開発プロジェクトを革新してください。ダウンロード、あるいは DVD で入手できます。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source
ArticleID=249837
ArticleTitle=PHP V5.2 の新機能、第 1 回: 新しいメモリー・マネージャーの使用方法
publish-date=03132007