対象読者: PHP の実行速度がどれくらいか知りたい人、PHP アプリの応答が重く、原因が言語ランタイムなのかアプリの作りなのかを切り分けたい人
「PHP は遅い」という言い方には、実行速度の話と、アプリの応答が重い話が混ざったまま使われがちです。どちらも原因と対処が別なので、分けて扱います。
この記事で扱わないもの:
- 他言語との実行速度ベンチマークの数値比較
- 特定フレームワークのチューニング手順
- サーバー構成やインフラ側の最適化
- 「どの言語が一番速いか」という一般論
1. 「遅い」を2つの問いに分ける
「PHP は遅いのか」には、次の2つが混ざっています。
- 問い A: PHP という言語ランタイムは、5 系の印象どおり今も遅いままなのか
- 問い B: 自分が触っている PHP アプリの応答が遅いのは、PHP のせいなのか
この2つは別物です。問い A は言語やバージョンの性質、問い B は目の前のアプリの作りにあたります。世間で言う「PHP は遅い」の多くは、問い A の古い印象で語られがち。ところが実際に困るのは問い B のほうで、原因は言語ランタイムの外にあることが多いのです。
他の言語と比べて速いか遅いかは、PHP のバージョン・設定・扱う処理・比較相手で変わります。ここで決めるのは一律の順位ではなく、問い A の「5 系の印象を今のバージョンへそのまま当てはめてよいか」だけ。
まず「遅い」という印象がどこから来たか、次に 5 系以降で何が変わったか、最後にアプリが重いときにどこを疑うかを順に見ます。
2. 「遅い」の印象はどこから来たか
「PHP は遅い」というイメージの多くは、PHP 5 時代の実行効率と、当時よく使われた実行モデルに由来します。
- PHP 5 系の実行エンジンは、7 系以降と比べて 1 リクエストあたりの処理効率が低かった
- opcode キャッシュを有効にしない構成では、リクエストのたびにソースを opcode へコンパイルし直すコストが乗った(リクエスト単位で実行するモデル自体は今も使われる)
- 古いサンプルや古い書き方が今も検索で見つかりやすく、当時の印象が更新されにくい
5 系の時点では、この印象は的外れではありませんでした。とはいえ、ランタイムは 5 系で止まっていない。7 系でエンジンが書き直され、同じコードでも 1 リクエストの処理効率は 5 系より上がりました。「PHP は遅い」を今の 8 系にそのまま当てはめると、前提がずれてしまいます。
3. 5系以降でランタイムが変わった点
問い A(言語ランタイムの速度)に関わる変化は、大きく分けて3つ。数値の比較ではなく、何が速さに効くかという機構で見ます。
| 仕組み | 効くこと |
|---|---|
| Zend Engine の書き直し(7 系) | 内部のデータ構造と命令実行が見直され、同じコードでも 1 リクエストの処理効率が上がった |
| OPcache | 一度コンパイルした opcode をメモリに保持し、リクエストごとの再コンパイルを省く |
| JIT(8.0 以降) | PHP のコードを実行時に機械語へ変換し、PHP 側で CPU を使い続ける数値計算やループを速くする場合がある |
OPcache は、リクエストのたびにソースを opcode へコンパイルし直す無駄を消す仕組みです。PHP 5.5 から同梱されていて、7 系のエンジン書き直しとは別の軸で効きます。本番環境では有効にしておくのが前提。無効のままだと、コード量に比例して毎回のコンパイルが乗ります。
JIT には注意がいります。JIT が効くのは、PHP のコードで数値計算や長いループのように CPU を連続して使う部分です。画像変換や暗号化のように、実際の計算を C 拡張が担う関数を呼ぶだけの箇所は、JIT の対象になりません。いっぽうフォームを受けて DB に問い合わせて画面を返すような処理は、時間の多くを DB やネットワークの待ちに使う。待ちが主なコードでは、CPU を速くしても全体の応答は縮みにくいままです。JIT を入れれば何でも速くなる、とはならないのはこのためです。
4. アプリが重いとき、時間を食うのはどこか
問い B(アプリの応答が重い)に移ります。業務 Web アプリの1リクエストで時間がかかりやすいのは、言語ランタイムより次のような箇所であることが多いもの。
| 時間を食いやすい箇所 | 何が起きているか |
|---|---|
| DB クエリ | 遅いクエリ 1 本、あるいは件数分だけクエリが飛ぶ N+1 で待ちが伸びる |
| 外部 API 呼び出し | 相手の応答を待つ時間が、そのまま自分の応答に乗る |
| 大きなデータの読み書き | 一度に大量の行やファイルを扱うと、転送とメモリ確保に時間がかかる |
| 直列で待つ処理の積み重ね | 待つ処理を1つずつ順番に呼ぶと、待ち時間が足し算になる |
これらはどれも「PHP の実行速度」ではなく、アプリが何を待っているかの問題にあたります。待ちが応答時間の大半を占めるなら、同じ待ちを残したまま言語だけ替えても、大幅な短縮は見込みにくいでしょう。同じ DB に同じクエリを直列で投げる限り、その DB 待ちは言語を変えても残るからです。どれだけ縮むかは、待ち・PHP 側の処理・シリアライズなどの内訳を測って判断します。
応答が重いと言われる場面の多くは、実際にはこの待ちを見ているだけ。原因を言語に還元すると、直すべき DB や呼び出しの側が手つかずで残ってしまいます。
5. 言語ランタイムの速度が効く場面
ランタイムの速度が全く無関係というわけでもない。PHP のコードで CPU を連続して使う処理では、言語や実装の効率が応答へ直接効いてきます。
- 大きな配列を PHP のコードで繰り返し変換・集計する処理
- 自前で書いたパーサや、長い文字列を PHP で1文字ずつ扱う処理
- 外部ライブラリに渡さず、PHP のループの中で計算を積み上げる処理
いっぽう、画像変換・暗号化・圧縮のような処理は、PHP から呼んでも実際の計算は C 拡張(GD・OpenSSL・zlib など)の中で動きます。ここが重いときに見るのは、PHP ランタイムの速度ではなく、その拡張や外部プロセスにかかる時間のほうです。
PHP のコードそのものが重い場面では、対処の方向が2つあります。ひとつは、その部分を C 拡張や専用ライブラリに任せ、PHP 側は呼び出しに徹すること。もうひとつは、重い処理をリクエストの外(キューやバッチ)へ逃がし、ユーザーを待たせない方法です。重い処理をワーカーへ切り離す構成は PHP × Docker で PostgreSQL のジョブワーカーを動かす にまとめてあります。
6. まず測る——どこが遅いかを推測で決めない
問い B を解くには、遅い場所を推測ではなく計測で特定します。手をつける順番は次のとおり。
- リクエスト全体の応答時間を測り、遅いエンドポイントを1つに絞る
- その中で、DB・外部呼び出し・PHP 処理のどこに時間が寄っているかを分ける
- 一番大きい箇所から直す
どこで時間を使っているかの内訳は、リクエストの計測やプロファイラ・APM で取ります。DB が疑わしいなら、遅いクエリを実行計画から読む。読み方は PostgreSQL の遅いクエリを EXPLAIN ANALYZE で読む にまとめました。ステップ実行のほうは時間の割合そのものを測れませんが、原因の候補になったコード経路を1行ずつ確認するのに向く。その手順は VS Code で Xdebug のステップ実行を使う にあります。
計測なしで「PHP が遅いから」と決めつけると、直しても効かない場所に手を入れて時間を使うだけ。時間の大半が 1 本の遅いクエリに寄っていると分かれば、直す対象はそのクエリに絞れます。
7. まとめ
「PHP は遅いのか」は、言語ランタイムの速度(問い A)と、アプリの応答の重さ(問い B)に分けると答えやすくなる。
- 問い A は 5 系の印象で語られがちです。7 系のエンジン書き直し・OPcache・8.0 の JIT で、今のランタイムは当時とは別物。JIT は PHP のコードで CPU を使い続ける処理に効き、待ちが主の処理では効きにくいままです。
- 問い B は、DB クエリや外部呼び出しなど「何を待っているか」が主因のことが多いです。待ちが大半なら、同じ待ちを残したまま言語を替えても、応答は大きくは縮みません。
重いアプリに当たったら、言語のせいにする前に、どこで時間を使っているかを測ります。直す対象さえ決まれば、それは多くの場合、言語ランタイムの外にあります。
なお、性能ではなく「PHP をいまから学ぶのは遅いか」という時期の話なら、PHPをいまから始めるのは遅いのか? のほうが近い。