PHP アプリケーションを高速に、より高速に、最高速にする、第 2 回: PHP アプリケーションをプロファイリングして遅いコードを発見し、診断し、高速化する

PHP アプリケーションが遅い場合には、プロファイラーを使うことによって、どこで時間が消費されているかを測定することができます。そうすれば、最も遅いステートメントやループ、関数、クラス、ライブラリーなどを見つけることができます。時間ではなくメモリーの使用量が問題の場合にも、適切なプロファイラーはコンポーネントのフットプリントを示してくれます。

Martin Streicher (mstreicher@linux-mag.com), Editor in Chief, Linux Magazine

Martin StreicherはLinux Magazineの編集長です。以前はBerkeley Systemsのエグゼクティブ・プロデューサーとして、YOU DON'T KNOW JACK(ゲーム)やAfter Darkを含め、様々な賞を受けたソフトウェアを開発してきました。彼はPurdue Universityにて、コンピューター・サイエンスで修士を取得しています。卒業後初めての仕事は、CONVEXスーパーコンピューター用のUNIXプログラミングでした。



2007年 3月 06日

この「PHP アプリケーションを高速に、より高速に、最高速にする(US)」シリーズの第 1 回では、PHP のオペ・コード・キャッシュである XCache を使ってサイト全体を高速化する方法を説明しました。XCache はいくつかあるキャッシング・パッケージの 1 つにすぎませんが、コンパイル処理の出力を保持するため、出力を保持しなければ重複してしまう作業をなくすことができます。ページが変更されない限り、そのページの代わりとしては、そのページをキャッシュしたもので十分です。ページが更新されると、キャッシュされたページは無効にされ、新しいページと交換されます。

オペ・コード・キャッシュ (そして多くの場合、同じパッケージの中に提供されているオペ・コード・オプティマイザー) は、サイトの応答速度を高めるための方法として非常に安価です。キャッシュ・パッケージの多くは無料でオープンソースであり、その利点を活用するために何もコードを変更する必要がありません。

もちろん、一部のアプリケーションでは、PHP ソース・コードの 1 ファイルを対応するオペ・コードに変換するための時間は、実際の実行時間に比べると、無視できることもあります。リモート・データベース・サーバーへの接続や、効率の悪い SQL クエリーを使ったストアの照会、そしてデータを解析して操作するための大量の作業の方が、ずっと時間が長くかかり、従ってコストが高い、あるいは無駄ですらあるかもしれません。そうした場合には、ネットワークを適切に設計し、データベースをうまく構成すれば、遅延時間や遅いクエリーを軽減でき、また必要な場合には、近くにいる顔なじみの専門家に頼ることもできます。しかしコードの実行が遅い場合には、皆さんが自分で処理する必要があるかもしれません。

しかし、どこから手を付ければよいのでしょう。よく言われるように、完成する前にコードをいじるのは向こう見ずです。つまり、最初の実装でも十分に速いかもしれません。コードが確かに動作し、正確に動作しているにもかかわらず遅く思える、あるいは実際に遅い場合、最初に取るべきステップは、コードのパフォーマンスを定量化する、つまりベンチマークを行うことです。そうした診断を行わずにコードを最適化しようとするのはまさに無謀な試みです。

簡単なパフォーマンス測定として、ウォール・クロック時間を使う方法があります。つまりページに対するリクエストから描画が完成するまでの実際の遅延を測定する方法です。状況によっては、例えば Web サーバーやデータベース、ブラウザーなどを独自のワークステーションでローカルに実行するような場合には、ウォール・クロック時間は有益な情報です。しかし、ネットワーク遅延がある場合や、トラフィックがあり稼働中の Web サーバー、あるいは稼働中のデータベースなど、他のほとんどの場合には、ウォール・クロック時間は参考程度のものでしかありません。

もっと正確な測定をする (例えば、ソース・コードの 1 つのステートメントの実行に要する時間まで測定する) ためには、コード・プロファイラーを使います。プロファイラーは、通常は PHP ランタイム・エンジンの拡張として実装されますが、ステートメントの開始から終了までの時間や、プロシージャーの開始から終了までの時間、そして受信したリクエストへのレスポンスを作成するのに要する合計時間を記録します。こうしたデータがあれば、最も遅いステートメントやループ、関数、クラス、ライブラリーを調べることができます。時間が問題なのではなく、メモリーの使用量が問題の場合にも、適切なプロファイラーはコンポーネントのフットプリントも示してくれます。

PHP 用のプロファイラーとしてよく使われているものの 1 つに、Xdebug があります。Xdebug は、PHP アプリケーションを対話型でデバッグするために、サーバーにフックすることもできます。(詳しくは、囲み記事「より良いデバッグ方法」を参照してください。このシリーズの今後の記事でも対話型の高度なデバッグを取り上げる予定です。) Xdebug は容易にソースからビルドでき、Zend エクステンションとしてインストールされます。(一部のプラットフォーム用には、バイナリー・ファイルもあります。) Xdebug を利用すると、PHP ベースのページに対するリクエストごとにデータ・セットが生成され、それを KCacheGrind で見ることができます。

Xdebug をビルドし、インストールする

PHP ユーティリティー phpize と php-config を利用でき、またシステムの php.ini 構成ファイルを利用できるのであれば、Xdebug のインストールと設定には数分しか必要ありません。下記の命令は Linux® 用ですが、Mac OS X の場合のステップもほとんど同じです。(Xdebug の Web サイトには、Microsoft® Windows® 用にプリコンパイルされた Xdebug もあります。)

Xdebug の最新版は V2.0.0RC3 です (ただし皆さんがこの記事を読む頃には、最終版の V2.0.0 が入手できるかもしれません)。tarball をダウンロードして解凍し、カレント・ディレクトリーをソース・コードのサブディレクトリーに変更します。シェルの PATH に phpize と php-config があることを確認し、phpize を使ってビルドする準備をします。

リスト 1. Xdebug をセットアップする
$ wget http://www.xdebug.org/files/xdebug-2.0.0RC3.tgz
$ tar xzf xdebug-2.0.0RC3.tgz
$ cd xdebug-2.0.0RC3/xdebug-2.0.0RC3
$ phpize
Configuring for:
PHP Api Version:         20020918
Zend Module Api No:      20020429
Zend Extension Api No:   20050606

phpize を実行すると、configure という適切な名前のスクリプトが得られます。このスクリプトが、残りのビルド・プロセスの構成を行います。Xdebug をビルドするには、単純に ./configure と入力し、その直後に make を実行します。

リスト 2. Xdebug をビルドする
$ ./configure
checking build system type... i686-apple-darwin8.8.1
checking host system type... i686-apple-darwin8.8.1
checking for egrep... grep -E
...
$ make
...
Build complete.
(It is safe to ignore warnings about tempnam and tmpnam).

make コマンドによって、Xdebug エクステンション xdebug.so が作成されます。後はこれを、sudo make install を使ってインストールするだけです。

$ sudo make install
Installing shared extensions: /usr/lib/php/extensions/no-debug-non-zts-20020429/

注意: 最後のコマンドをターミナル・ウィンドウで実行した場合には、最後のステップで出力されたディレクトリーを選択してコピーします。次のステップで、これが必要になります。

最後に、プロファイル・データを見るために、KCacheGrind と GraphViz が必要です。KDE (K Desktop Environment) を含む Linux ディストリビューションであれば、おそらく既に KCacheGrind と GraphViz が含まれています。それ以外の Linux ディストリビューションでも、それぞれに適したバージョンの KCacheGrind と GraphViz を見つけるのは容易なはずです。また Debian ユーザーは、APT (Advanced Packaging Tool) を使うことで、KCacheGrind と GraphViz、そしてすべてのパッケージの依存関係を即座にインストールすることができます。

リスト 3. KCacheGrind をインストールする
$ apt-cache search kcachegrind
valgrind-callgrind - call-graph skin for valgrind
kcachegrind - visualisation tool for valgrind profiling output
kcachegrind-converters - format converters for KCachegrind profiling visualisation tool
$ apt-cache search graphviz
graphviz - rich set of graph drawing tools
graphviz-dev - graphviz Libs and Headers against which to build applications
graphviz-doc - additional documentation for graphviz
libdeps-renderer-dot-perl - DEPS renderer plugin using GraphViz/dot
...
$ sudo apt-get install kcachegrind graphviz 
...

システムに KDE がインストールされていない場合には、KCacheGrind とGraphViz、そしてすべての前提条件をインストールするために、約 90 MB のディスク・スペースが必要です。


Xdebug を構成する

Xdebug エクステンションがインストールできると、このエクステンションを有効にし、構成を行うことができます。テキスト・エディターで php.ini を開き、下記の行を追加します。

リスト 4. エクステンションを有効にし、構成を行う
zend_extension = /usr/lib/php/extensions/no-debug-non-zts-20020429/xdebug.so
xdebug.profiler_output_dir = "/tmp/xdebug/"
xdebug.profiler_enable = Off
xdebug.profiler_enable_trigger = 1

1 行目の zend_extension は Xdebug エクステンションをロードします。2 行目は、プロファイラーの出力を置くためのディレクトリーに名前を付けます。もし必要であれば、名前付きのディレクトリーを作成し、そのモードを、Web サーバー・ユーザーの書き込みアクセスを許可するように変更します。

3 行目はプロファイラーを無効にします。しかし 4 行目は、HTTP の GET あるいは POST パラメーター XDEBUG_PROFILE が設定されている場合には、必ずプロファイラーを有効にします。(プロファイラーを連続的にオンにしておきたい場合には、3 行目の Off を On に変更します。)

上記の各行を追加し、出力ディレクトリーが書き込み可能なことを確認したら、Web サーバーを再起動します。他の PHP エクステンションと同様、Xdebug がインストールされて利用可能なことを確認するためには、phpinfo() をコールする最小限の PHP プログラムを作成し、その結果をスキャンします。そうすると、図 1 のようなものが表示されるはずです。(簡単にするために、出力の一部を省略してあります。)

図 1. Xdebug がインストールされているかどうかを phpinfo で示す
図 1. Xdebug がインストールされているかどうかを phpinfo で示す

Zend のロゴのところまで、下にスクロールすることもできます。Xdebug が適切にロードされ、構成されていれば、Xdebug がロゴの隣に表示されるはずです。


プロファイラーを使う

コードをプロファイリングするには、単純にブラウザーで PHP アプリケーションにアクセスします。トリガーごとにケース・バイ・ケースでプロファイラーを動作させたい場合には、URL に XDEBUG_PROFILE=1 を付加するか、あるいは下記のように、フォームの中にパラメーターを埋め込みます。

一例として、単純な ACME Fibonacci Maker (fibonacci.php) をプロファイリングしてみましょう (図 5)。便利なように、フォームの隠し変数の中に XDEBUG_PROFILE パラメーターが設定されています。(コードが実稼働用に移行すると、おそらく Xdebug は無効にされるため、この変数は無害なものになります。)

リスト 5. Fibonacci.php
<?php
function fib($nth = 1) {
if ( $nth < 2 ) {
return( $nth ); 
}

return( fib( $nth - 1) + fib( $nth - 2 ) );
}
?> 

<html>
<head>
<title>ACME Fibonacci Maker</title>
</head>
<body>
<h2>Try the ACME Fibonacci Maker!</h2>
<form action="fibonacci.php" method="POST">
<input type="hidden" name="XDEBUG_PROFILE" value="1" />
Enter a number: <input type="text" name="n"></input>
</form>
<hr />

<?php  
if ( ! empty( $_REQUEST['n'] ) ) {
$n = $_REQUEST['n'] % 10;
$suffix = array( 1 => "st", 2 => "nd", 3 => "rd" );
if ( $_REQUEST['n'] < 4 || $_REQUEST['n'] > 20 ) {
$suffix = $suffix[$n];
}
else {
$suffix = 'th';
}

echo '<p>The ' . $_REQUEST['n'] . $suffix .' Fibonacci number is ';
echo fib( $_REQUEST['n'] ) . '</p>';
}
?>
</body>
</html>

ブラウザーで http://localhost/fibonacci.php (あるいは相当する URL) にアクセスし、例えば 16 という数字を入力します。そうすると、その結果、つまりフィボナッチ数列の 16 番目の要素が図 2 のように表示されます。

図 2. フィボナッチ・アプリケーションの例
図 2. フィボナッチ・アプリケーションの例

プロファイラーの出力ディレクトリー (php.ini の中で名前が付けられています) の内容をリストすると、cachegrind.out.951917687 のような名前のファイルがあるはずです。接頭辞の cachegrind.out. は固定です。接尾辞の数字は、デフォルトで fibonacci.php ファイルへのディレクトリー・パスの CRC32 ハッシュです。従って、各アプリケーションが独自のディレクトリーの中にある場合には、各アプリケーションからの出力をファイル名で区別することができます。 (もし出力を時刻と関連付けたい場合には、php.ini に下記行を追加します。)

xdebug.profiler_output_name = timestamp

ターミナル・ウィンドウから KCacheGrind を起動し、cachegrind.out.951917687 を開きます。そうすると、すぐに図 3 のような新しいウィンドウが開くはずです。

図 3. KCacheGrind アプリケーション
図 3. KCacheGrind アプリケーション

Callees タブをクリックし、強調されているソース・コード行をダブル・クリックし、そして Grouping リストから Source File を選択します。そうすると、ビューが図 4 のような表示に変わるはずです。

図 4. 結果を見る
図 4. 結果を見る

想像したとおり、処理時間のほとんどすべて (70,989 ミリ秒の 99.87%) が、関数 fib() を 3,193 回呼び出すために使われています。このアプリケーションはフィボナッチ数列の数が大きくなるのに比例して遅くなりますが、これを高速化するためには、フィボナッチ数の再計算というコストがかかる作業を避けなければなりません。実は ACME Fibonacci Maker は、計算の再利用を行える優れたツールなのです。

もっと賢い fib() 関数を下記に示します。この新しいバージョンでは、後で使えるように計算の中間結果を保持するため、メモリーと引き換えに時間を節約しています。図 5 は、その分析結果です。必要な関数コールは、3,192 回ではなく 30 回のみです (しかもこれらのコールのうち、実際に結果を計算しているのは半分しかありません)。そして時間は 20 ミリ秒に短縮されています。

リスト 6. 更新された fib() 関数
function fib($nth = 1) {
static $fibs = array();

if ( ! empty ($fibs[$nth] ) ) { 
return( $fibs[$nth] );
}

if ( $nth < 2 ) {
$fibs[$nth] = $nth;
}
else {  
$fibs[$nth - 1] = fib( $nth - 1 );
$fibs[$nth - 2] = fib( $nth - 2 );
$fibs[$nth] = $fibs[$nth - 1] + $fibs[$nth -2];
}

return( $fibs[$nth] );
}
?>
図 5. 高速なフィボナッチ関数
図 5. 高速なフィボナッチ関数

アプリケーションを 1 度実行しただけで有効な問題を見つけられる (上記の、元のアプリケーションで、フィボナッチ数列の 50 番目の要素を試してみてください) こともありますが、通常は何回かアプリケーションを実行して統計を収集し、パターンを調べます。

デフォルトの「crc32」命名機構をそのまま使っている場合には、fibonacci.php を実行する度にデータ・ファイルが上書きされてしまいます。しかし php.ini に xdebug.profiler_append = 1 と設定することにより、この動作を変更して、後続の実行結果が同じファイルに追加されるようにすることができます。この変更を行った後、Web サーバーを再起動します。

Fibonacci Maker を 3 回実行した後、集約したデータの例を図 6 に示します。合計時間は 2 秒を少し越える程度であり、そのうちの 99.97% の時間が fib() に使われています。図 6 は Call Graph タブを示しています。このタブは GraphViz の dot ユーティリティーによって生成されます。KCacheGrind の使い方の詳細はこの記事の範囲外ですが、完全なドキュメンテーションがオンラインで閲覧できます。KCacheGrind は、データをさまざまな形式で切り取ることができます。それをどのように表示すれば適切なのかは、どの問題を解決したいかに依存します。

図 6. プロファイリング・データを集約する
図 6. プロファイリング・データを集約する

より良いデバッグ方法

Xdebug エクステンションは (その名前のとおり) 構成の仕方によって、PHP アプリケーションをプロファイリングすることの他に、エラーが発生した際に詳細なスタック・トレースや有用なエラー・メッセージを提供することができ、また対話型のデバッグが可能です。スタック・トレースやエラー・メッセージは、エラーの原因を正確に特定できます。また対話型デバッグは、1 つの命令ごとにコードをたどることができ、プログラム変数の型や値を問い合わせることができ、また受信されるリクエスト・パラメーターを含めて、PHP のすべてのスーパーグローバル変数を調べることができます。

このシリーズの次回の記事では、対話型のデバッグを詳細に調べます。それまでの間、下記のような Xdebug の機能をいくつか有効にすることで、エラーが発生した時のアプリケーションの状態を解明することができます。

  • xdebug.default_enable=On と設定すると、プログラムがエラーを起こした時に必ずスタック・トレースを表示することができます。既に Xdebug をインストールしてある場合には、コードが開発中のあいだは、この機能を有効にします。
  • また、xdebug.show_local_vars=1 と設定すると、最上位スコープにあるすべての変数が詳しく表示されます。
  • xdebug.var_display_max_children と xdebug.var_display_max_data、そして xdebug.var_display_max_depth は関連した設定であり、それぞれ、xdebug.show_local_vars が使われている場合に表示される変数のプロパティーの数、ストリングの長さ、ネストの深さをコントロールします。

詳細については Xdebug の Web サイトを参照してください。


クラスをプロファイリングする

大きなコード本体がないと、意味のあるプロファイリングを示すのは難しいものです。しかし次の例を見ると、より典型的なコードから、どれだけの種類と量の情報が得られるかがわかると思います。リスト 7 は、おもちゃのロケットを組み立てるための (創作) アプリケーションを示しています。このロケットはいくつかの部分から構成され、それぞれの部分を作成するために一定の時間が必要です。PHP に当てはめると、クラスは各部分を表し、インスタンス・メソッドは各部分の作成時間を表します。このおもちゃをアプリケーションと考えることができ、それぞれの部分を各機能と考えることができます。

リスト 7. オモチャの作成をエミュレートした一連の PHP クラス
<?php
define( 'BOOSTER', 5 );
define( 'CAPSULE', 2 );
define( 'MINUTE', 60 );
define( 'STAGE', 3 );
define( 'PRODUCTION', 1000 );

class Part {
function Part() {
$this->build( MINUTE );
}

function build( $delay = 0 ) {
if ( $delay <= 0 )
return;

while ( $delay-- > 0 ) {
}
}
}

class Capsule extends Part {
function Capsule() {
parent::Part();
$this->build( CAPSULE * MINUTE );
}
}

class Booster extends Part {
function Booster() {
parent::Part();
$this->build( BOOSTER * MINUTE );
}
}

class Stage extends Part {
function Stage() {
parent::Part();
$this->build( STAGE * MINUTE );
}
}

class SpaceShip {
var $booster;
var $capsule; 
var $stages;

function SpaceShip( $numberStages = 3 ) {
$this->booster = new Booster();
$this->capsule = new Capsule();
$this->stages = array();

while ( $numberStages-- >= 0 ) {
$stages[$numberStages] = new Stage();
}
}
}

$toys = array();
$count = PRODUCTION;

while ( $count-- >= 0  ) {
$toys[] = new SpaceShip( 2 );
}
?>

<html>
<head>
<title>
Toy Factory Output
</title>
</head>
<body>
<h1>Toy Production</h1>
<p>Built <? echo PRODUCTION . ' toys' ?></p>
</body>
</html>

このコードを実行すると、新しいデータ・ファイルが作成されます。この場合も、このデータを KCacheGrind にロードします。Source タブと Call Graph タブに切り替えると、図 7 のようなビューが表示されるはずです。

図 7. 宇宙船アプリケーションのプロファイル
図 7. 宇宙船アプリケーションのプロファイル

Flat Profile ペイン (左) は、アプリケーションの実行中に呼び出されるすべての関数 (メソッド) を示しています。一番左側の列は累計時間を擬似的に示しており、2 番目の列は各メソッドを独立に測定した結果を示し、3 番目の列はメソッドが呼びだされた回数をリストしています。便利なことに、色付きの四角がコール・グラフに反映されているため、イベントのシーケンスとタイミングとを容易に関連付けることができます。

当然ですが、最もコストが高いのは、発射台 (stage) を建設する時間です。次にコストが高いのは、各部分の構築に必要なオーバーヘッド (Part のコンストラクターで表現されています) です。また興味深いことに、PHP 自身の define() 関数にはほとんどコストがかかっていません。

最後に、メモリーがどのように使われているかも見ることができます。最上部の近くにあるドロップダウン・メニューから MemoryClass を選び、最上部にある Types タブと最下部にある Caller Map タブに切り替えます。そうすると画面は図 8 のようになるはずです。

図 8. 宇宙船アプリケーションでのメモリーの使用状況
図 8. 宇宙船アプリケーションでのメモリーの使用状況

サイクルを探求する

Xdebug は、他の多くの PHP エクステンションと同様、容易にビルドでき、素早くインストールでき、そして構成も容易にできるため、すべてを約 10 分で行うことができます。もし Apache のインストールが既に最適化されており、アプリケーションが既にキャッシュされているにもかかわらずパフォーマンスが低く思える場合には、コードがどのように動作しているかを考えてみてください。アルゴリズムは効率的でしょうか。コードが複雑すぎないでしょうか。PHP が既に提供している関数を再実装していないでしょうか。

当然ながら、もしボトルネックの名前をあげられない、あるいはボトルネックを特定できない場合には、速度低下の原因を見つけ、修正する必要があります。その場合には、推測するのではなく、プロファイリングしてください。貴重な計算サイクルがどう使われているかがわかり、驚かされることになるかもしれません。

そして、実稼働のサーバーでは Xdebug を無効にすることを忘れないでください。Xdebug が有効になっていると、必ずオーバーヘッドが追加されてしまいます。

参考文献

学ぶために

  • PHP.net は PHP 開発者のためのリソースです。
  • developerWorks の「Recommended PHP reading list」を調べてみてください。
  • developerWorks には他にも PHP 関連の資料が豊富に用意されています。
  • IBM developerWorks の PHP project resources を調べ、PHP のスキルを磨いてください。
  • developerWorks podcasts では、ソフトウェア開発者のための興味深いインタビューや議論を聞くことができます。
  • developerWorks Technical events and webcasts で最新情報を入手してください。
  • IBM オープンソース開発者にとって関心のある、世界中で今後開催される会議や業界展示会、ウェブキャスト、その他のイベントについて調べてみてください。
  • developerWorks の Open source ゾーンをご覧ください。オープンソース技術を使った開発や、IBM 製品でオープンソース技術を使用するためのハウ・ツー情報やツール、プロジェクトの更新情報など、豊富な情報が用意されています。
  • Safari Books Online には、オープンソース技術のためのリソースが豊富に取り揃えられています。

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

  • Xdebug ソフトウェアをダウンロードしてください。
  • 皆さんの次期オープンソース開発プロジェクトを IBM trial software を使って革新してください。ダウンロード、あるいは 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=249834
ArticleTitle=PHP アプリケーションを高速に、より高速に、最高速にする、第 2 回: PHP アプリケーションをプロファイリングして遅いコードを発見し、診断し、高速化する
publish-date=03062007