ホームページ  >  記事  >  バックエンド開発  >  PHP7 の革新性とパフォーマンスの最適化を学ぶ

PHP7 の革新性とパフォーマンスの最適化を学ぶ

coldplay.xixi
coldplay.xixi転載
2020-06-24 17:26:102979ブラウズ

PHP7 の革新性とパフォーマンスの最適化を学ぶ

PHP は 20 年の歴史を経ており、PHP7 は前シリーズの PHP5 と比較して大規模な革新と言え、特にパフォーマンスの面で最高のパフォーマンスを達成しました。飛躍的な大幅な改善。 PHP は世界中で広く使用されている Web 開発言語であり、PHP7 の革新はこれらの Web サービスにさらに大きな変化をもたらすことは間違いありません。

これは、Bird Brother の PPT のグラフからの引用です (Web サイトの 82% が開発言語として PHP を使用しています):

(注: Web サイトは開発言語として複数の言語を使用できます)
(注: この記事にはニアオ兄弟の PPT のスクリーンショットが多数含まれており、写真の著作権はニアオ兄弟に属します)

最初に 2 つのエキサイティングなパフォーマンス テストの結果を見てみましょう。


PHP7 のパフォーマンス テストの結果。パフォーマンス ストレス テストの結果、消費時間は 2.991 から 1.186 に低下しました。 60%の大幅な減少。

WordPress QPS ストレス テスト (PPT からの画像):

WordPress プロジェクトでは、PHP7 は PHP5.6 と比較して、QPS が 2.77 倍増加しました。

パフォーマンス テスト結果の興味深い比較を読んだ後、本題に入りましょう。 PHP7 には多くの新機能がありますが、主な変更点に焦点を当てます。

1. 新機能と変更点

1. スカラー型宣言とスカラー型宣言

PHP 言語 非常に重要な機能は「弱い型付け」です。 , そのため、PHP プログラムは非常に簡単に作成でき、初心者でもすぐに PHP に触れることができますが、いくつかの論争も伴います。変数の型定義のサポートは革新的な変更と言え、PHP はオプションの方法で型定義をサポートし始めます。さらに、switch 命令declare(strict_type=1); も導入されており、この命令がオンになると、現在のファイルの下のプログラムが関数パラメータの転送型と戻り値の型に厳密に従うように強制されます。

たとえば、add 関数と型定義は次のように記述できます。

必須の型切り替え命令と組み合わせると、次のようになります。

strict_type がオンになっていない場合、PHP は必要な型への変換を支援します。これをオンにすると、PHP が変更され、型変換は実行されなくなります。型が一致しない場合は、型の不一致がスローされます。これは、「厳密に型指定された」言語を好む学生にとって朗報です。

より詳細な紹介: PHP7 スカラー型宣言 RFC [翻訳]

2. より多くのエラーがキャッチ可能になる例外

PHP7 は、グローバルなスロー可能なインターフェイスを実装しています。オリジナルの Exception と一部の Error は、このインターフェイス (インターフェース) を実装し、例外の継承構造をインターフェースの形式で定義します。その結果、PHP7 ではより多くのエラーがキャッチ可能な例外となって開発者に返され、キャッチされなかった場合はエラー、キャッチされた場合はプログラム内で処理できる例外となるようになりました。これらのキャッチ可能なエラーは通常、存在しない関数など、プログラムに致命的な害を及ぼさないエラーです。 PHP7 は開発者の処理をさらに容易にし、開発者がプロ​​グラムをより詳細に制御できるようにします。デフォルトでは、エラーによりプログラムが直接中断されますが、PHP7 はエラーをキャプチャして処理する機能を提供し、プログラムの実行を継続できるため、プログラマはより柔軟な選択肢を得ることができます。

例えば、存在するかどうか不明な関数を実行する場合、PHP5互換のメソッドでは関数を呼び出す前に判定 function_exist を付加しますが、PHP7では例外をキャッチするハンドリングメソッドがサポートされています。 。

下の図に示すように (スクリーンショットは PPT からのものです):

3. AST (抽象構文ツリー、抽象構文ツリー)

AST は、PHP コンパイル プロセスでミドルウェアの役割を果たし、インタープリターからオペコードを直接吐き出す元の方法を置き換え、インタープリター (パーサー) とコンパイラー (コンパイラー) を分離します。コードをハックすると同時に、実装の理解と保守が容易になります。
PHP5:

PHP7:

その他の AST 情報: https://wiki.php.net/rfc/abstract_syntax_tree

4. ネイティブ TLS (ネイティブ スレッド ローカル ストレージ、ネイティブ スレッド ローカル ストレージ)

PHP は、スレッドが共有するため、マルチスレッド モード (たとえば、Web サーバー Apache のワーカー モードとイベント モードはマルチスレッドです) で「スレッド セーフ」(TS、スレッド セーフ) の問題を解決する必要があります。プロセスのメモリ空間であるため、他のスレッドとの相互汚染を避けるために、各スレッド自体が何らかの方法でプライベート スペースを構築し、独自のプライベート データを保存する必要があります。 PHP5 で採用されている方法は、大規模なグローバル配列を維持し、各スレッドに独立した記憶領域を割り当てることであり、スレッドは独自のキー値を通じてこのグローバル データ グループにアクセスします。

この一意のキー値は、PHP5 でグローバル変数を使用する必要があるすべての関数に渡す必要があります。PHP7 では、この渡し方法は不親切で、いくつかの問題があると考えられています。したがって、このキー値を保存するには、スレッド固有のグローバル変数を使用してみてください。
関連するネイティブ TLS の問題:
https://wiki.php.net/rfc/native-tls

5. その他の新機能

PHP7 の新機能と変更点は、申し訳ありませんが、ここでは詳しく説明しません。
(1) Int64 のサポート。さまざまなプラットフォームで整数の長さを統一し、2GB を超える文字列とファイルのアップロードをサポートします。
(2) 統一された変数構文。
(3) 一貫した foreach 動作
(4) 新しい演算子 <=>、??
(5) Unicode 文字形式のサポート (\u{xxxxx})
(6) 匿名クラスsupport (匿名クラス)
#… …

2. 飛躍的なパフォーマンスのブレークスルー: 全速前進

1. JIT とパフォーマンス

Just In Time (ジャストインタイム コンパイル) は、実行時にのみバイトコードをマシンコードにコンパイルするソフトウェア最適化テクノロジです。直感的に考えると、マシンコードはコンピュータが直接認識して実行できるため、オペコードを 1 つずつ読み込んで実行する方が Zend よりも効率的であると考えられます。その中でも、HHVM (HipHop Virtual Machine、HHVM は Facebook のオープンソース PHP 仮想マシンです) は JIT を使用しており、PHP のパフォーマンス テストが桁違いに向上し、衝撃的なテスト結果が公開されています。これも、JIT が強力であると直感的に思わせるものです。石を金に変える技術。
実際、2013 年に、Niao 兄弟と Dmitry (PHP 言語コアの開発者の 1 人) は、PHP5.5 バージョン (未リリース) で JIT を試みたことがありました。 PHP5.5 の本来の実行プロセスは、字句解析および構文解析を通じて PHP コードをオペコード バイトコードにコンパイルし (形式はアセンブリに似ています)、その後、Zend エンジンがこれらのオペコード命令を読み取り、1 つずつ解析して実行します。

そして、オペコードリンクの後に型推論 (TypeInf) を導入し、JIT を通じて ByteCode を生成して実行しました。

その結果、ベンチマーク(テストプログラム)では、JIT導入後はPHP5.5と比較して8倍のパフォーマンス向上という素晴らしい結果が得られました。しかし、この最適化を実際のプロジェクト WordPress (オープンソース ブログ プロジェクト) に導入したところ、パフォーマンスの向上はほとんど見られず、不可解なテスト結果が得られました。
そこで、彼らは Linux でプロファイル タイプ ツールを使用して、プログラム実行の CPU 時間消費を分析しました。
WordPress を 100 回実行したときの CPU 消費量の分布 (PPT のスクリーンショット):

注:
CPU 時間の 21% がメモリ管理に費やされます。
CPU 時間の 12% は、主に PHP 配列の追加、削除、変更、チェックなどのハッシュ テーブル操作に費やされます。
CPU 時間の 30% は、strlen などの組み込み関数に費やされます。
CPU 時間の 25% が VM (Zend Engine) に費やされます。

分析の結果、次の 2 つの結論が得られました。

(1) JIT によって生成された ByteCode が大きすぎると、CPU キャッシュ ヒット率の低下 (CPU キャッシュ ミス) が発生します

PHP5.5 コードでは、明確な型定義がないため、型推論のみに依存できます。可能な限り推論できる変数の型を定義し、型推論と組み合わせて、その型以外の分岐コードを削除し、直接実行可能な機械語コードを生成します。ただし、型推論ではすべての型を推論できるわけではなく、WordPress では推論できる型情報は 30% 以下に限られており、削減できる分岐コードも限られています。その結果、JIT 後にマシン コードが直接生成され、生成された ByteCode が大きすぎるため、最終的に CPU キャッシュ ヒット (CPU キャッシュ ミス) が大幅に減少します。

CPU キャッシュ ヒットとは、CPU が命令を読み取って実行するときに、必要なデータが CPU の 1 次キャッシュ (L1) に読み込めない場合、下方への検索を継続する必要があることを意味します。2 次キャッシュ ( L2) と 3 次キャッシュ (L3) を使用すると、最終的にはメモリ領域で必要な命令データを見つけようとし、メモリと CPU キャッシュの読み取り時間の差は 100 倍に達することがあります。したがって、ByteCode が大きすぎて実行される命令の数が多すぎる場合、多値キャッシュではそれほど多くのデータを収容できず、一部の命令をメモリ領域に格納する必要があります。

すべてのレベルの CPU キャッシュのサイズも制限されており、次の図は Intel i7 920 の構成情報です:

したがって、CPU キャッシュ ヒットの減少は、一方で、時間のかかる増加は、JIT によってもたらされるパフォーマンスの向上によっても相殺されます。

JIT により VM のオーバーヘッドが削減されると同時に、命令の最適化によりメモリ割り当ての数が削減されるため、間接的にメモリ管理の開発を削減できます。ただし、実際の WordPress プロジェクトの場合、VM に費やされる CPU 時間はわずか 25% であり、主な問題やボトルネックは実際には VM にありません。したがって、JIT 最適化プランは、このバージョンの PHP7 機能には含まれていませんでした。ただし、今後のバージョンで実装される可能性は高いので、期待しておきましょう。

(2) JITパフォーマンスの改善効果はプロジェクトの実際のボトルネックに依存します

JIT は、コード量が比較的少なく、最終的に生成される ByteCode も比較的小さく、主なオーバーヘッドが VM にあるため、ベンチマークで大幅に改善されました。ただし、実際の WordPress プロジェクトでは、WordPress のコード量がベンチマークよりもはるかに大きいため、明らかなパフォーマンスの向上はありません。JIT は VM のオーバーヘッドを削減しますが、CPU キャッシュ ヒットと余分なメモリの減少を引き起こします。 ByteCode が大きすぎるため、オーバーヘッドが発生し、最終的には改善されません。
プロジェクトの種類が異なると、CPU オーバーヘッド率も異なり、結果も異なります。実際のプロジェクトを使用しないパフォーマンス テストは、あまり代表的ではありません。

2. Zval の変更点

#実際、PHP のさまざまなタイプの変数の実際の記憶媒体は Zval であり、その許容範囲と耐性が特徴です。本質的には、C言語で実装された構造体(struct)です。 PHP を書く学生にとっては、大まかに配列のようなものとして理解できます。
PHP5 の Zval、メモリは 24 バイトを占有します (PPT からのスクリーンショット):

PHP7 の Zval、メモリは 16 バイトを占有します (PPT からのスクリーンショット):

Zval 24 バイトから 16 バイトに減少しました。なぜ減少したのでしょうか? ここでは、C に慣れていない学生が理解できるように、C 言語の基礎を少し追加する必要があります。 struct と Union (union) の間には若干の違いがあり、Struct の各メンバー変数は独立したメモリ空間を占有しますが、union のメンバー変数はメモリ空間を共有します (つまり、メンバー変数の 1 つが変更されると、パブリックスペースは変更後、他のメンバー変数の記録はありません)。したがって、メンバー変数がより多くなったように見えますが、実際に占有されるメモリ領域は減少しています。

さらに、大幅に変更された機能が他にもあり、一部の単純な型では参照が使用されなくなりました。

Zval 構造図 (PPT より):

Zval 図中の

Zval は 2 つの 64 ビット (1 バイト = 8 ビット、ビットは「ビット」) で構成されます。 long または bealoon で、長さが 64 ビットを超えない場合、value に直接格納され、後続の参照はありません。変数の型が 64 ビットを超える配列、オブジェクト、文字列などの場合、格納される値は実際の記憶構造アドレスを指すポインタになります。


単純な変数型の場合、Zval ストレージは非常にシンプルかつ効率的になります。

参照を必要としない型: NULL、Boolean、Long、Double

参照を必要とする型: String、Array、Object、Resource、Reference######3. 内部型 zend_string## #

Zend_string は実際に文字列を格納する構造体で、実際の内容は val (char、文字型) に格納され、val は長さ 1 の char 配列 (メンバ変数の占有に便利) です。

構造体の最後のメンバー変数は、char* の代わりに char 配列を使用します。ここでは、CPU のキャッシュ ミスを減らすことができる小さな最適化トリックを示します。
char 配列を使用する場合、malloc が上記の構造体のメモリに適用されるとき、同じ領域に適用されます。通常、長さは実際の char ストレージ領域の sizeof(_zend_string) です。ただし、char* を使用する場合、この場所に格納されるのはポインタのみであり、実際の格納場所は別の独立したメモリ領域にあります。

char[1] と char* を使用したメモリ割り当ての比較:

論理実装の観点から見ると、実際には 2 つに大きな違いはなく、その効果は非常に大きくなります。似ている。実際、これらのメモリ ブロックが CPU にロードされると、見た目は大きく異なります。前者は連続して割り当てられた同じメモリであるため、通常、CPU がそれを読み取るときに一緒に取得できます (同じレベルのキャッシュにあるため)。後者は、2 つのメモリからのデータが含まれているため、CPU が最初のメモリを読み取るときに、2 番目のメモリのデータが同じレベルのキャッシュにない可能性が非常に高いため、CPU は L2 (2 次キャッシュ) の下を検索する必要があります。 Even to 目的の 2 番目のメモリデータがメモリ領域に見つかりました。これにより CPU キャッシュ ミスが発生し、両者の時間の差は最大 100 倍になる可能性があります。

さらに、文字列をコピーするとき、参照割り当てを使用すると、zend_string はメモリのコピーを回避できます。

6. PHP 配列 (HashTable および Zend Array) の変更点

PHP プログラムを作成するプロセスで最も頻繁に使用される型は配列であり、PHP5 配列は HashTable を使用して実装されます。ざっくりまとめると、二重リンクリストをサポートする HashTable で、配列キーを介して要素にアクセスするハッシュ マッピングをサポートするだけでなく、foreach を介して二重リンク リストにアクセスすることで配列要素を横断することもできます。
PHP5 の HashTable (PPT からのスクリーンショット):

この図は、さまざまなポインターが飛び回っており、非常に複雑に見えます。キー値を通じて要素のコンテンツにアクセスする場合、場合によっては 3 つのポインターが必要になりますジャンプして適切なコンテンツを見つけます。最も重要な点は、これらの配列要素の格納が異なるメモリ領域に分散されていることです。同様に、CPU が読み取りを行う場合、同じレベルのキャッシュにない可能性が高いため、CPU は下位レベルのキャッシュ、さらにはメモリ領域を検索する必要があり、これにより CPU キャッシュ ヒットが減少します。消費量が増加する時間。

PHP7 の Zend 配列 (PPT からのスクリーンショット):

新しいバージョンの配列構造は非常にシンプルで目を引きます。最大の特徴は、配列要素全体とハッシュマッピングテーブルがすべて接続され、同じメモリ上に割り当てられていることです。単純型の整数配列を走査している場合、配列要素 (バケット) 自体が同じメモリ内に継続的に割り当てられ、配列要素の zval が整数要素を内部的に格納するため、効率が非常に速くなります。ポインタ外部リンクでもあり、すべてのデータは現在のメモリ領域に保存されます。もちろん、最も重要なことは、CPU キャッシュ ミス (CPU キャッシュ ヒット率の低下) を回避できることです。

Zend 配列の変更:
(1) 配列の値のデフォルトは zval です。
(2) HashTable のサイズは 72 バイトから 56 バイトに減少し、22% 削減されました。
(3) バケットのサイズは 72 バイトから 32 バイトに減少し、50% 削減されました。
(4) 配列要素のバケットのメモリ空間はまとめて割り当てられます。
(5) 配列要素(Bucket.key)のキーはzend_stringを指します。
(6) 配列要素の値はバケットに埋め込まれます。
(7) CPU キャッシュミスを削減します。

7. 関数呼び出し規約

PHP7 では関数呼び出しの仕組みが改良され、パラメーターの転送処理を最適化することで一部の命令が削減され、実行効率が向上しました。

PHP5 の関数呼び出しメカニズム (PPT からのスクリーンショット):

図では、vm スタック内の命令 send_val と recv パラメーターは同じです。PHP7 では、これら 2 つの項目の繰り返しが削減されます。関数呼び出しメカニズムの根本的な最適化を実現します。

PHP7 の関数呼び出しメカニズム (PPT からのスクリーンショット):

8. コンパイラにマクロ定義とインライン関数 (インライン) を通じて作業の一部を事前に完了させます

C言語のマクロ定義は前処理段階(コンパイル段階)で実行され、作業の一部が事前に完了しており、プログラム実行時にメモリを確保する必要がなく、機能を実現できます。関数呼び出しのプレッシャーのない、 -like 関数 スタックのスタックとポップのオーバーヘッドは比較的高くなります。インライン関数も同様で、前処理の段階でプログラム中の関数を関数本体に置き換えますが、ここで実際に実行するプログラムを実行する際には関数呼び出しのオーバーヘッドは発生しません。

PHP7 はこの分野で多くの最適化を行い、実行フェーズで実行する必要がある多くの作業をコンパイルフェーズに組み込みました。例えば、パラメータの型判定(Parameters Parsing)はすべて固定の文字定数であるため、コンパイル段階で完了することができ、その後の実行効率が向上します。

たとえば、渡されるパラメータの型の扱い方を、左の記述方法から右のマクロの記述方法に最適化します。

3. 概要

Niao Ge の PPT は、PHP5.6 で WordPress を 100 回実行すると 70 億の CPU 命令実行が生成されるのに対し、PHP7 ではわずか 2.5 回の比較データをリリースしました。これは衝撃的なデータです。

ニアオ兄弟のシェアを通して、私にとって最も深い視点は、細部に注意を払い、多くの小さな最適化を行い、少しずつ継続的に蓄積し、少量ずつ積み上げ、最終的には驚くべき結果に収束するということです。 9人で1日で山を築くのは不可能、おそらくこれが理由だと思います。

PHP7 が飛躍的なパフォーマンスの向上を達成したことは疑いの余地がありません。これらの結果を PHP の Web システムに適用できれば、おそらくより少ないマシンでより多くのリクエスト量をサポートできるようになるでしょう。 PHP7 の正式版のリリースには、尽きない期待が満ちています。

推奨チュートリアル: 「php ビデオ チュートリアル

以上がPHP7 の革新性とパフォーマンスの最適化を学ぶの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcsdn.netで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。