高いレベルから、Facebookなどはどうですか。ヒップホップ仮想マシンでPHPパフォーマンスを向上させるために使用しますか?
従来のzendエンジンを使用してコードを実行するのとどう違うのですか?これは、型がオプションで事前最適化手法を可能にするハックで定義されているためですか?
私の好奇心は、この記事を読んだ後で発生しました HHVMの採用 。
彼らは、TranslatorX64のトレースレットを新しいHipHop中間表現(hhir)と、実際には同じ名前hhirで参照されるhhirを生成するロジックが存在する新しい間接層に置き換えました。
ここでは、「同じタイプチェックで始まりますが、翻訳の本文は6命令であり、TranslatorX64の9を大幅に上回っています」と述べています。
http://hhvm.com/blog/2027/faster-and-cheaper-the-evolution-of-the-hhvm-jit
「私たちは、間接参照の新しいレイヤーを追加することでこの問題を解決しました。この新しいレイヤーはSSAフォームの中間表現であり、TranslatorX64のトレースレットのバイトコードと最終的に必要なx86マシンコードの間に配置されます。これは、強く型付けされ、 TranslatorX64から移植したい最適化の数、および将来の新しい最適化。hhir(HipHop Intermediate Representationの略)という名前のこの新しいIRは、2013年5月にhhvmのJITとしてTranslatorX64を完全に置き換えました。hhirは特にそれ自体、その名前を使用して、コードとやり取りするすべてのコードを参照することがよくあります。ソースコードを最近見た場合、TranslatorX64という名前のクラスがまだ存在し、重要な量のコードが含まれていることに気付くでしょう。ほとんどの場合、システムの設計方法の成果物であり、最終的にクリーンアップする予定です。TranslatorX64に残されているすべてのコードは、コードを出力するために必要な機構であり、翻訳をリンクします。個々のバイトコードを変換する方法を理解したコードは、TranslatorX64からなくなりました。
HhirがTranslatorX64に取って代わったとき、それは約5%高速なコードを生成しており、手動で検査したときの外観が大幅に向上していました。生産デビューを別のミニロックダウンで追跡し、その上でパフォーマンスがさらに10%向上しました。これらの改善の一部を実際に見るために、関数addPositiveとその変換の一部を見てみましょう。
function addPositive($arr) { $n = count($arr); $sum = 0; for ($i = 0; $i < $n; $i++) { $elem = $arr[$i]; if ($elem > 0) { $sum = $sum + $elem; } } return $sum; }
この関数は、多くのPHPコードのように見えます。これは配列をループし、各要素に対して何かを実行します。今のところ、5行目と6行目、およびそれらのバイトコードに注目しましょう。
$elem = $arr[$i]; if ($elem > 0) { // line 5 85: CGetM <L:0 EL:3> 98: SetL 4 100: PopC // line 6 101: Int 0 110: CGetL2 4 112: Gt 113: JmpZ 13 (126)
これらの2行は、配列から要素を読み込み、ローカル変数に格納し、そのローカルの値を0と比較し、結果に基づいて条件付きでどこかにジャンプします。バイトコードで何が行われているのかについて詳しく知りたい場合は、bytecode.specificationをざっと読むことができます。 JITは、現在もTranslatorX64時代も、このコードを2つのトレースレットに分割します。1つはCGetMのみで、もう1つは残りの命令で行われます(これが発生する理由の詳細な説明はここでは関係ありませんが、ほとんどの場合、コンパイル時に配列要素の型がどうなるかわからないためです)。 CGetMの変換は、結局、C++ヘルパー関数の呼び出しになり、それほど興味深いものではないので、2番目のトレースレットを見ていきます。このコミットはTranslatorX64の正式な廃止でした。その親を使用して、TranslatorX64がこのコードをどのように翻訳したかを見てみましょう。
cmpl $0xa, 0xc(%rbx) jnz 0x276004b2 cmpl $0xc, -0x44(%rbp) jnle 0x276004b2 101: SetL 4 103: PopC movq (%rbx), %rax movq -0x50(%rbp), %r13 104: Int 0 xor %ecx, %ecx 113: CGetL2 4 mov %rax, %rdx movl $0xa, -0x44(%rbp) movq %rax, -0x50(%rbp) add $0x10, %rbx cmp %rcx, %rdx 115: Gt 116: JmpZ 13 (129) jle 0x7608200
最初の4行はタイプチェックで、$ elemの値とスタックの一番上の値が予想されるタイプであることを確認しています。どちらかが失敗した場合は、トレースレットの再変換をトリガーするコードにジャンプし、新しいタイプを使用して、異なる方法で専門化されたマシンコードのチャンクを生成します。翻訳の要点が続き、コードには改善の余地がたくさんあります。ライン8にはデッドロードがあり、ライン12でのレジスタの移動は簡単に回避でき、ライン10と16の間で一定の伝播が発生する可能性があります。これらはすべて、TranslatorX64で使用されるバイトコード一度のアプローチの結果です。立派なコンパイラはこのようなコードを出力しませんが、それを回避するために必要な単純な最適化は、TranslatorX64モデルに適合しません。
次に、同じhhvmリビジョンで、hhirを使用して翻訳された同じtraceletを見てみましょう。
cmpl $0xa, 0xc(%rbx) jnz 0x276004bf cmpl $0xc, -0x44(%rbp) jnle 0x276004bf 101: SetL 4 movq (%rbx), %rcx movl $0xa, -0x44(%rbp) movq %rcx, -0x50(%rbp) 115: Gt 116: JmpZ 13 (129) add $0x10, %rbx cmp $0x0, %rcx jle 0x76081c0
同じタイプチェックで始まりますが、変換の本体は6つの命令であり、TranslatorX64の9よりもはるかに優れています。デッドロードやレジスターからレジスターへの移動がなく、Int 0バイトコードからの即時0が行12のcmpに伝搬されていることに注意してください。トレースレットとその変換の間に生成されたhhirは次のとおりです。
(00) DefLabel (02) t1:FramePtr = DefFP (03) t2:StkPtr = DefSP<6> t1:FramePtr (05) t3:StkPtr = GuardStk<Int,0> t2:StkPtr (06) GuardLoc<Uncounted,4> t1:FramePtr (11) t4:Int = LdStack<Int,0> t3:StkPtr (13) StLoc<4> t1:FramePtr, t4:Int (27) t10:StkPtr = SpillStack t3:StkPtr, 1 (35) SyncABIRegs t1:FramePtr, t10:StkPtr (36) ReqBindJmpLte<129,121> t4:Int, 0
バイトコード命令は、より小さく単純な操作に分解されています。 SetLの一部である6行目のLdStackなど、特定のバイトコードの動作に隠されている多くの操作は、hhirで明示的に表されます。値のフローを表すために物理レジスタの代わりに名前のない一時(t1、t2など)を使用することにより、各値の定義と使用を簡単に追跡できます。これにより、ロードの宛先が実際に使用されているかどうか、または命令への入力の1つが実際に3バイトコード前の定数値であるかどうかを簡単に確認できます。 hhirとは何か、それがどのように機能するかについてのより完全な説明については、ir.specificationを参照してください。
この例では、hhlatorがTranslatorX64に対して行った改善のほんの一部を示しました。 2013年5月にhhirを本番環境に導入し、TranslatorX64を廃止することは、大きな節目でしたが、それは始まりに過ぎませんでした。それ以来、TranslatorX64ではほぼ不可能であったより多くの最適化を実装し、hhvmをプロセスでほぼ2倍効率化しました。また、ARMプロセッサー上でhhvmを実行することは、再実装する必要のあるアーキテクチャー固有のコードの量を分離および削減することにより、私たちの取り組みにおいて非常に重要でした。ARM詳細についてはポート! "
つまり、ランダムメモリアクセスを最小限に抑え、メモリ内のコード間をジャンプして、CPUキャッシュを適切に操作します。
HHVMパフォーマンスステータス によると、ランダムなメモリアクセスを最小限に抑えるために、文字列と配列である最も頻繁に使用されるデータ型を最適化しました。アイデアは、一緒に使用されるデータ(配列内のアイテムなど)をメモリ内で可能な限り互いに近くに、理想的には線形に保つことです。これにより、データがCPU L2/L3キャッシュに収まる場合、RAMにある場合よりも桁違いに速く処理できます。
言及された別のテクニックは、コンパイルされたバージョンが可能な限り線形になるように(つまり、「ジャンプ」の量が最小になるように)コードで最も頻繁に使用されるパスをコンパイルし、メモリにデータをできるだけロードまたはロードしないことです。