ヒップホップ仮想マシン(HHVM)は、理論的にどのようにPHPランタイムのパフォーマンスを向上させますか?


9

高いレベルから、Facebookなどはどうですか。Hip Hop仮想マシンでPHPのパフォーマンスを向上させるために使用しますか?

従来のzendエンジンを使用してコードを実行するのとどう違うのですか?これは、型がオプションで事前最適化手法を可能にするハックで定義されているためですか?

私の好奇心は、HHVMの採用というこの記事を読んだ後で生じました。

回答:


7

彼らは、TranslatorX64のトレースレットを新しいHipHop中間表現(hhir)と、実際には同じ名前hhirで参照されるhhirを生成するロジックが存在する新しい間接層に置き換えました。

ここでは、「同じタイプチェックから始まりますが、翻訳の本文は6命令であり、TranslatorX64の9命令よりもはるかに優れています」と述べています。

http://hhvm.com/blog/2027/faster-and-cheaper-the-evolution-of-the-hhvm-jit

これは主にシステムの設計方法の成果物であり、最終的にクリーンアップする予定です。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の公式の引退でした。

  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を参照してください。

この例では、TranslatorX64に対してhhirが行った改善のほんの一部を示しました。2013年5月にhhirを本番環境に導入し、TranslatorX64を廃止することは、大きな節目でしたが、それはほんの始まりに過ぎませんでした。それ以来、TranslatorX64ではほぼ不可能であった多くの最適化を実装し、hhvmをプロセスでほぼ2倍効率化しました。また、再実装する必要のあるアーキテクチャー固有のコードの量を分離して削減することにより、hhvmをARMプロセッサーで実行することも私たちの取り組みにおいて重要でした。詳細については、ARMポートへの投稿をお待ちしています!」


1

つまり、ランダムメモリアクセスを最小限に抑え、メモリ内のコード間をジャンプして、CPUキャッシュを適切に操作します。

HHVM Performance Statusによると、ランダムなメモリアクセスを最小限に抑えるために、文字列と配列である最も頻繁に使用されるデータ型を最適化しました。アイデアは、一緒に使用されるデータ(配列内のアイテムなど)をメモリ内で可能な限り互いに近くに、理想的には線形に保つことです。これにより、データがCPU L2 / L3キャッシュに収まる場合、RAMにある場合よりも桁違いに速く処理できます。

言及されている別の手法は、コンパイルされたバージョンが可能な限り線形(つまり、「ジャンプ」の量が最も少ない)であり、メモリへのデータのロード/メモリからのデータのロードをできるだけ少なくするような方法で、コードで最も頻繁に使用されるパスをコンパイルすることです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.