背景HHVM は Facebook によって開発された高性能 PHP 仮想マシンで、公式のものよりも 9 倍高速であると主張されています。非常に興味があったので、時間をかけて簡単に学習し、この記事にまとめました。 2 つの質問に答えることができます:
あなたならどうしますか?HHVM の実装原則について説明する前に、自分の立場になって考えてみましょう。PHP で書かれた Web サイトでパフォーマンスの問題が発生したとします。分析の結果、リソースの大部分が PHP で消費されていることがわかりました。どのように最適化しますか。 PHPのパフォーマンスは? たとえば、いくつかの方法があります:
オプション 1 は、10 年前、Netscape の例を挙げて、特に Facebook のような複雑なビジネス ロジックを備えた製品については、PHP コードが多すぎると警告しました。行数は 2,000 万と言われています ([PHP on the Metal with HHVM] から引用) 変更のコストはおそらく仮想マシンを作成するよりも大きく、数千人のチームにとってはゼロから学習することは受け入れられません。 オプション 2 は最も安全なソリューションであり、段階的に移行できます。実際、Facebook もこの点で熱心に取り組んでおり、初期の頃から Facebook 内で主に使用されているもう 1 つの言語は C++ です。これは、Thrift コードで確認できます。他の言語での実装は非常に粗雑で、運用環境では使用できないためです。 現在、Facebook では PHP:C++ が 9:1 から 7:3 に増加したと言われており、Andrei Alexandrescu の存在も相まって Facebook では C++ の人気が高まっていますが、これでは問題の一部しか解決できません。結局のところ、C++ の開発コストは PHP よりもはるかに高く、頻繁に変更される場所での使用には適しておらず、RPC 呼び出しが多すぎるとパフォーマンスに重大な影響を及ぼします。 オプション 3 は良さそうですが、実際に実装するのは困難です。一般的に、パフォーマンスのボトルネックはそれほど重大ではなく、主に PHP 拡張機能の開発コストが高くなります。通常、これは公開アプリケーションでのみ使用され、あまり変更されていない基本ライブラリに基づいているため、このソリューションでは多くの問題を解決できません。 最初の 3 つの解決策では問題をうまく解決できないことがわかり、Facebook は実際には PHP 自体の最適化を検討するしかありません。 PHP の高速化PHPを最適化したいのですが、どのように最適化すればよいでしょうか?私の意見では、いくつかの方法があります:
PHP 言語レベルの最適化は最もシンプルで実現可能です。もちろん Facebook もそれを考えており、パフォーマンスのボトルネックを特定するのに非常に役立つ XHProf のようなパフォーマンス分析ツールも開発しました。 ただし、XHProf は依然として Facebook の問題をうまく解決できていないため、引き続き検討を続けます。次はオプション 2 です。簡単に言うと、Zend の実行プロセスは、PHP をオペコードにコンパイルする部分と、オペコードを実行する部分の 2 つの部分に分けることができるため、最適化が行われます。 Zend それはこの 2 つの側面から考えることができます。 オペコードの最適化は一般的な方法であり、PHP の繰り返しの解析を回避でき、また、Zend Optimizer Plus などの静的なコンパイルの最適化も行うことができます。ただし、PHP 言語の動的な性質のため、この最適化方法は限定的で楽観的です。パフォーマンスは 20% しか向上しないと推定されています。もう 1 つの考慮事項は、レジスタベースのアプローチなど、オペコード アーキテクチャ自体を最適化することですが、このアプローチでは変更に多大な労力が必要であり、パフォーマンスの向上は特に明らかではありません (おそらく 30%?)。高くありません。もう 1 つの方法は、オペコードの実行を最適化することです。まず、Zend のインタプリタ (インタプリタとも呼ばれます) がオペコードを読み取った後、さまざまなオペコードに応じてさまざまな関数を呼び出します (実際には一部はスイッチですが、説明は便宜上簡略化しています)、この関数内でさまざまな言語関連の操作を実行します (興味がある場合は、書籍「PHP コアの徹底理解」を参照してください)。そのため、複雑なカプセル化やZend での間接呼び出しの説明として、このデバイスではすでに非常にうまく機能しています。 Zend の実行パフォーマンスを向上させたい場合は、実際には関数呼び出しにはオーバーヘッドがあるため、その原理は C の inline キーワードに似ています。この方法では、実行時に関連する関数が展開され、それらが順番に実行されます (単なる例えであり、実際の実装は異なります)。また、CPU パイプライン予測の失敗によって引き起こされる無駄も回避します。 さらに、JavaScriptCore や LuaJIT などのアセンブリを使用してインタープリターを実装することもできます。具体的な詳細については、Mike の説明を読むことをお勧めします。 しかし、これら 2 つのメソッドは変更するにはコストがかかりすぎ、特に下位互換性を確保するためには、1 つを書き直すよりもさらに困難です。これは、後で PHP の特性について説明するときにわかると思います。 高性能の仮想マシンを開発するのは簡単なことではありません。JVM が現在のパフォーマンスに達するまでに 10 年以上かかりました。では、これらの高性能の仮想マシンを PHP のパフォーマンスの最適化に直接使用できるでしょうか。これが選択肢3の考え方です。 実際、このソリューションは Quercus や IBM の P8 などによって長い間試みられてきましたが、Quercus はほとんど誰も使用されておらず、P8 も廃止されました。 Facebookもこの手法を調査しており、信憑性の低い噂も流れたが、実際にはFacebookは2011年に断念している。 オプション 3 は見た目は良いですが、実際の効果は理想的ではないため、多くの専門家 (マイクなど) によると、VM は常に特定の言語に対して最適化されており、他の言語では動的言語などの実装時に多くのボトルネックが発生します。メソッド呼び出しは Dart のドキュメントで紹介されており、Quercus のパフォーマンスは Zend+APC とそれほど変わらないと言われている ([The HipHop Compiler for PHP より]) ので、あまり意味はありません。 しかし、最近の Grall プロジェクトはかなり良くできていて、大きな成果を上げている言語もありますが、私はまだ Grall を勉強する時間がありません。なのでここでは判断できません。 次のステップはオプション 4 で、これはまさに HPHPc (HHVM の前身) が行うことです。原理は、PHP コードを C++ に変換し、それをローカル ファイルにコンパイルすることです。これは (事前に) AOT と見なすことができます。 ) メソッドについて コード変換の技術的な詳細については、論文「The HipHop Compiler for PHP」を参照してください。以下は概要を理解するために使用できる論文のスクリーンショットです。 このアプローチの最大の利点は、(VM と比較して) 実装が簡単で、多くのコンパイル最適化を行うことができることです (オフラインなので、遅くても問題ありません)。たとえば、上記のようになります。インタプリタを埋め込むコストは小さくないため、HPHPc は単純にこれらの構文をサポートしていません。 ![]() を C コードに変換する例です。
php_hash_find (LOCAL_ST, "f", 5863275, &fgc_info.params);<div class="blockcode">php_call_function (&fgc_info) ) ;<div id="code_LjU"><ol>
<li>コードをコピー</li>
<li>
<li> <p> phc と言えば、2 年前に Facebook に phc のデモをしに行って、そこのエンジニアとコミュニケーションを取った結果、リリースされるやいなや人気が出た、と著者がブログで泣いたことがある。 4年間忙しかったが、今は未来が暗い。 。 。 </p>
<p>Roadsend は保守されなくなりました。PHP のような動的言語の場合、このアプローチには多くの制限があり、オンラインにする際のファイルのデプロイメントは実際に 1G に達してしまいました。 </p>
<p>PHP QB というプロジェクトもありますが、これは時間の都合上見ませんでした。おそらく似たようなものだと思います。 </p>
<p>残された道は 1 つだけです。それは、より高速な PHP 仮想マシンを作成して、この暗い道を最後までやり遂げることです。Facebook が仮想マシンを構築すると最初に聞いたとき、あなたはそう思ったかもしれません。はあまりにも無茶苦茶でしたが、よく分析してみると実はこれしか方法がないことが分かります。 </p>
<h2>より高速な仮想マシン</h2>
<p>HHVM のほうが速いのはなぜですか? JITのキーテクノロジーはさまざまなニュースで取り上げられていますが、実際のところ、JITは一振りするだけでパフォーマンスが向上する魔法の杖ではなく、JIT自体の運用にも時間がかかります。最も極端な例では、LuaJIT 2 のインタープリターは V8 の JIT よりも若干速いため、詳細な処理に絶対的なものはありません。 . HHVM の開発の歴史 それは継続的な最適化の歴史であり、HPHPc を少しずつ超えていることがわかります: </p>
<img alt="HHVM 是如何提升 PHP 性能的?" src="http://img.it-home.org/data/attachment/forum/2014pic/20140321154123_97.jpg" style="max-width:90%" style="max-width:90%">
<p>Android 4.4 の新しい仮想マシン ART は AOT ソリューション (覚えていますか? 前述の HPHPc がこれです) を使用しており、結果は JIT を使用した以前の Dalvik の 2 倍高速であるため、必ずしも JIT が高速であるとは限りません。 AOTよりも。 </p>
<p>したがって、このプロジェクトは非常に危険であり、強い心と忍耐力がなければ、Google は Python のパフォーマンスを向上させるために JIT を使用しようと考えていましたが、最終的には Python の使用は失敗しました。実際にはパフォーマンスの問題はありません (Google は以前、クロールを Python で記述していました [In The Plex を参照] が、それはすべて 1996 年のことです)。 </p>
<p> Google と比較して、Facebook は明らかにモチベーションと決意が優れています。PHP は Facebook がこのプロジェクトに投資した専門家を見てみましょう (未完了)。
</p>
<ul>Andrei Alexandrescu 氏、『Modern C++ Design』および『C++coding Standards』の著者、C++ 分野の誰もが認めるマスター<li>
</li>Keith Adams は VMware のコア アーキテクチャを担当していました。当時、VMware は Intel との技術協力を行うために彼を単身派遣しました。これは彼が VMM の分野でどれだけの知識を持っていたかを証明しました<li>。
</li>Microsoft で .NET 仮想マシンの開発に参加し、JIT を改善した Drew Paroski 氏<li>
</li>Jason Evans、Firefox のメモリ消費量を半分に削減する jemalloc を開発<li>
</li>Sara Golemon、『PHP の拡張と埋め込み』の著者、PHP カーネルの専門家、PHP マスターは全員この本を読んだと思いますが、おそらく彼女が実際には女性であることをご存知ないでしょう<li>
</li>
</ul>Lars Bak や Mike Pall のような仮想マシンの分野のトップの専門家はいませんが、これらの専門家が協力して仮想マシンを作成できれば、大きな問題にはならないでしょう。では、彼らはどのような課題に直面するでしょうか?次に、それらについて 1 つずつ説明します。 <p>
</p>仕様は何ですか? <h3>
</h3>独自の PHP 仮想マシンを作成するときに直面する最初の問題は、PHP には言語仕様がなく、多くのバージョン間で構文に互換性がないことです (5.2.1 や 5.2.3 などの小さなバージョン番号であっても)。 PHPの言語仕様はどうなるのでしょうか? IEEE の声明を見てみましょう: <p>
</p>
<blockquote>PHP グループは、PHP (言語) の仕様について最終決定権を持っていると主張しています。このグループの仕様は実装であり、散文仕様や合意された検証スイートはありません。
<p>
</p> したがって、唯一の方法は、Zend の実装を正直に検討することです。幸いなことに、これは HPHPc で一度実行されているため、HHVM はそれを直接使用できるため、この問題はそれほど大きくありません。 </blockquote>
<p>言語または拡張子? </p>
<h3> PHP 言語の実装は、仮想マシンの実装と同じくらい簡単ではありません。PHP 言語自体にもさまざまな拡張機能が含まれており、これらの拡張機能は、使用できるさまざまな機能を実装するために絶えず機能します。 PHP コードを分析すると、空白行のコメントを除いた C コードには 80 万行以上あることがわかります。そして、Zend エンジンの部分がいくつあると思いますか。行数は 100,000 行弱です。 </h3>
<p>これは開発者にとっては悪いことではありませんが、エンジンの実装者にとっては非常に悲劇的です。これを Java 仮想マシンを作成するには、バイトコード解釈と Java で構築されたいくつかの基本的な JNI 呼び出しのみを実装する必要があります。 -in ライブラリは Java で実装されるため、パフォーマンスの最適化を考慮しない場合、ワークロードの観点からは、JVM よりも PHP 仮想マシンを実装する方がはるかに困難です。たとえば、8,000 行の TypeScript を使用して JVM を実装した人もいます。 。 </p>
<p>この問題に対して、HHVM の解決策は非常にシンプルです。つまり、Facebook で使用されているものを実装するだけであり、HPHPc で以前に記述されたものを使用することもできるため、問題は大きくありません。 </p>
<h3>インタプリタの実装</h3>
<p>次のステップはインタプリタの実装です。PHP を解析した後、HHVM によって設計されたバイトコードが生成され、再利用のために <code>~/.hhvm.hhbc (SQLite ファイル) に保存されます。バイトコードを実行するときは、Zend と同様であり、別のバイトコードも配置されます。 into さまざまな関数で実装されます (このメソッドには仮想マシン内で特別な名前が付いています: サブルーチン スレッド)
インタープリタの本体は bytecode.cpp に実装されています。 <code class="c++"><div class="blockcode">
<div id="code_oM7"><ol>
<li>if (c2.m_type == KindOfInt64) return o(c1.m_data.num, c2.m_data.num);</li>
<li>if (c2.m_type == KindOfDouble) return o(c1.m_data.num, c2.m_data.dbl);</li>
</ol></div>
<em onclick="copycode($('code_oM7'));">复制代码</em>
</div> 正是因为有了 Interpreter,HHVM 在对于 PHP 语法的支持上比 HPHPc 有明显改进,理论上做到完全兼容官方 PHP,但仅这么做在性能并不会比 Zend 好多少,由于无法确定变量类型,所以需要加上类似上面的条件判断语句,但这样的代码不利于现代 CPU 的执行优化,另一个问题是数据都是 boxed 的,每次读取都需要通过类似 if (c2.m_type == KindOfInt64) return o(c1.m_data.num, c2.m_data.num); if (c2.m_type == KindOfDouble) return o(c1.m_data.num, c2.m_data.dbl); コードをコピー
m_data.num のようなものを通過する必要があることです。 および m_data.dbl メソッドを使用して間接的に取得します。 2008 年に LLVM を実験した人もいますが、その結果は元のものより 21 倍遅かったです。 。 。 2010 年に、日本 IBM 研究所は、JVM 仮想マシン コードに基づいて P9 を開発しました。そのパフォーマンスは、公式の PHP の 2.5 ~ 9.5 倍です。「PHP 用に改良されたジャストインタイム コンパイラーの評価」を参照してください。
<div class="blockcode"> 2011 年に、Andrei Homescu は RPython に基づいて開発し、論文 HappyJIT: PHP 用のトレース JIT コンパイラーを書きましたが、テスト結果はまちまちで理想的ではありませんでした。 <div id="code_JSG">
<ol>
<li>それでは、JIT とは一体何でしょうか? JIT を実装するにはどうすればよいですか? </li>
<li>
</li>
<li>動的言語には、基本的に eval メソッドがあり、実行のために文字列を渡すことができます。JIT も同様のことを行いますが、文字列ではなく、異なるプラットフォーム上のマシンコードを結合して実行する必要がありますが、その方法は異なります。 Cで実装するには? Eli が書いたこの導入例を参照してください。記事のコードの一部を次に示します。
</li>
<li>
<li>
<li>
</ol>
</div>unsigned char code[] = {<em onclick="copycode($('code_JSG'));"> 0x48, 0x89, 0xf8, // mov %rdi, %rax</em> 0x48, 0x83, 0xc0, 0x04, // add $4, %rax</div> 0xc3 // ret🎜} ;🎜memcpy(m, code, sizeof(code));🎜🎜🎜コードをコピー🎜🎜 ただし、マシンコードを手作業で記述する場合は間違いが起こりやすいため、Mozilla の Nanojit や LuaJIT の DynASM などの補助ライブラリを使用するのが最善ですが、HHVM ではこれらを使用せず、x64 のみをサポートするライブラリを実装します (さらに、VIXL を使用して ARM 64 ビットを生成し、mprotect を通じてコードを実行可能にしようとしています。 しかし、なぜ JIT コードのほうが速いのでしょうか?実際、C++ で書かれたコードは最終的にはマシン コードにコンパイルされますが、同じコードが手動でマシン コードに変換された場合、GCC によって生成されたものとの違いは何でしょうか。先ほど CPU の実装原理に基づいた最適化手法について説明しましたが、JIT におけるより重要な最適化は、タイプに基づいて特定の命令を生成することにより、命令と条件判断の数を大幅に削減することです。TraceMonkey の次の図は、これを示しています。非常に直感的な比較です。後で HHVM の具体的な例を見てみましょう: ![]() HHVM は最初にインターピーターを通じて実行されますが、その後いつ JIT を使用するのでしょうか?一般的な JIT トリガー条件は 2 つあります:
2 つの方法のどちらが優れているかについては、Lambada に関する投稿があり、さまざまな専門家、特に Mike Pall (LuaJIT 著者)、Andreas Gal (Mozilla VP)、Brendan Eich (Mozilla CTO) からの議論を集めています。私自身の意見もたくさんありますし、皆さんにも見ていただくことをお勧めしますので、ここではひけらかしません。 それらの違いはコンパイル範囲だけではなく、ローカル変数の処理など多くの詳細も異なりますが、ここでは説明しません しかし、HHVM はこれら 2 つのメソッドを使用せず、タイプに応じて分割されたトレースレットと呼ばれる独自のメソッドを作成しました。 ![]() $k<p> が整数または文字列である 2 つの異なる状況を処理するために使用されているため、下の部分は戻り値であるようです。 Tracelet の解析と解体方法の詳細については、時間がありませんでしたので、Translator.cpp の <code>Translator::analyze<code>$k 为整数或字符串两种不同情况的,下面的部分是返回值,所以看起来它主要是根据类型的变化情况来划分 JIT 区域的,具体是如何分析和拆解 Tracelet 的细节可以查看 Translator.cpp 中的 Translator::analyze メソッドを参照してください。まだ見てくださいので、ここでは説明しません。
もちろん、高パフォーマンスの JIT を実現するには、さまざまな試みと最適化が必要です。たとえば、最初は、HHVM の新しく追加されたトレースレットが前に配置されます。つまり、上の図の A と C の位置が入れ替わります。後は後ろに入れてみた結果、事前に反応型を打ちやすいことがテストで判明したので14%向上しました JIT の実行プロセスは、まず HHBC を SSA (hhbc-translator.cpp) に変換し、次に SSA を最適化 (コピー伝播など) し、それをローカル マシン コードに再生成します。たとえば、X64 では、トランスレーターによって実装されます。 -x64.cppの。 次の PHP 関数など、HHVM によって最終的に生成されるマシンコードがどのようなものかを確認するために、簡単な例を使用してみましょう。
<div class="blockcode">
<div id="code_B9S">
<ol>
<li><?php<li>function a($b){<li> echo $b + 2;<li>}</ol></div><em onclick="copycode($('code_B9S'));">复制代码</em></div> function a($b){ そして、HPHP::print_int 関数の実装は次のようになります: <div class="blockcode"><div id="code_K6f"><ol>
<code class="c++ language-c++" data-lang="c++"><div class="blockcode">
<div id="code_K6f"><ol>
<li>void print_int(int64_t i) {</li>
<li> char buf[256];</li>
<li> snprintf(buf, 256, "%" PRId64, i);</li>
<li> echo(buf);</li>
<li> TRACE(1, "t-x64 output(int): %" PRId64 "n", i);</li>
<li>}</li>
</ol></div>
<em onclick="copycode($('code_K6f'));">复制代码</em>
</div> 可以看到 HHVM 编译出来的代码直接使用了 snprintf(buf, 256, "%" PRId64, i); echo(buf);<div class="blockcode">
<div id="code_K70"><ol><li>-v Eval.JitWarmupRequests=0</li></ol></div>
<em onclick="copycode($('code_K70'));">复制代码</em>
</div> TRACE(1, "t-x64 Output(int): %" PRId64 "n", i);} コードをコピーHHVM によってコンパイルされたコードは、インタープリターを回避して、 <div class="blockcode">
<div id="code_biL"><ol>
<li><?hh<li>class Point2 {<li> public float $x, $y;<li> function __construct(float $x, float $y) {<li> $this->x = $x;</li>
<li> $this->y = $y;</li>
<li> }</li>
<li>}</li>
<li>//来自:https://raw.github.com/strangeloop/StrangeLoop2013/master/slides/sessions/Adams-TakingPHPSeriously.pdf</li>
</ol></div>
<em onclick="copycode($('code_biL'));">复制代码</em>
</div> 注意到 <div class="blockcode"><div id="code_K70">
<ol><p>-v Eval.JitWarmupRequests=0</p></ol> <em onclick="copycode($('code_K70'));">コードをコピー</em>します<h2></h2> <p>そのため、パフォーマンスをテストするときは、1 回か 2 回実行しただけでは効果が見られないことに注意する必要があります。 。 </p>
<p>型導出は非常に面倒なので、プログラマにわかりやすく書かせるようにすべきです</p>
<p> JIT の鍵は型を推測することです。そのため、特定の変数の型が時間の経過とともに変化すると、最適化が難しくなります。そのため、HHVM エンジニアは、PHP 構文に工夫を凝らし、型サポートを追加することを検討し始め、新しい型を立ち上げました。言語 - Hack (Tucao) この名前は SEO にはあまり良くありません)、次のようになります: </p>
<code class="php language-php" data-lang="php"><div class="blockcode"><div id="code_biL">
<ol></ol>
<ul><?hh<li>class Point2 {</li> public float $ x, $y;<li> function __construct(float $x, float $y) {</li> $this->x = $x;<li> $this->y = $y;</li> }</ul>}<p>//From: https ://raw.github.com/strangeloop/StrangeLoop2013/master/slides/sessions/Adams-TakingPHPSeriously.pdf</p>
<em onclick="copycode($('code_biL'));">コードをコピー em><p></p> <h2> <code>float
キーワードに気づきましたか?静的型を使用すると、HHVM のパフォーマンスをより最適化できますが、PHP 構文と互換性がなく、HHVM のみを使用できることも意味します。
| 引用