こんにちは。今期は「はたらく細胞」が面白いと思います。 YamaYuski です。
今までなんとな〜く存在は知っていましたが、詳しく調べていなかった HHVM と Hack 言語 について、軽く調べてみました。
HHVM は、 PHP 7 の登場によって大きくそのあり方が変わっていました。 Hack 言語は相変わらず悪くないね、といったのが所感。
現状の PHP 7
HHVM と Hack 言語を説明する前に、現状の PHP の挙動を振り返ってみます。
弊社は PHP をメインのサーバサイド言語として利用しています。
その PHP は、 Zend Engine というインタプリタ(ソースコードを読んですぐ実行する)エンジンを使って動いています。
このインタプリタは、下記の順序でコードを実行します。
- ソースコードが書かれたテキストファイルを読み込む
- ソースコードをパースして opcode と呼ばれるアセンブリ言語っぽいものに変換する
- opcode を順番に実行する
この opcode は下記のような形です(3v4l.org を使った例)。
<php // ソースコード foreach (['a', 'b', 'c'] as $val) { echo 'Hello ' . $val . PHP_EOL; }
// opcode 出力 line #* E I O op fetch ext return operands ------------------------------------------------------------------------------------- 3 0 E > > FE_RESET_R $1 <array>, ->6 1 > > FE_FETCH_R $1, !0, ->6 4 2 > CONCAT ~2 'Hello+', !0 3 CONCAT ~3 ~2, '%0A' 4 ECHO ~3 5 > JMP ->1 6 > FE_FREE $1 5 7 > RETURN 1
この方法だと、「ソースコードをファイルから読み込む(IO処理)」「opcodeに変換する(CPU処理)」という前処理を、全てのHTTPリクエストの最初に行う必要があります。考えただけでも「これ遅くね?」という話です。インタプリタなので当然ではあります(実際はもっと高速化する手法がエンジンの中で動いています)。
現在は Opcache エクステンションを用いることで、一度変換した opcode をメモリ上に載せておき、2回目以降の処理ではメモリにキャッシュされた opcode を取り出して実行するため、高速化されています。
JIT
しかし、この opcode は「CPU が直接理解出来る機械語」ではない「中間言語」なので、実行時はさらにCPUやOSなどの実行環境に応じて振る舞いを変えながら実行する必要があります。これが「コンパイルする言語よりも遅い」と呼ばれる所以の一つです。
Java や C# などもコンパイルによって中間言語を生成しますが、これらの言語は「中間言語を実行直前に機械語に変換し、それを実行」しています。これを JIT 、実行時コンパイラと呼びます。「必要になった瞬間にさらにコンパイルをかけて実行」するので Just-In-Time コンパイラなのです。
C++ や golang などは最初のコンパイルで一気に機械語まで変換します。これを JIT と対比して AOT 、事前コンパイラと呼んでいます。
PHP は JIT も AOT も基本的に行わないので、これらの言語より実行速度が遅いとされるわけですね。
といっても、 PHP 7 や Opcache のおかげで、昔(PHP5以前など)より遥かに速くランタイム実行出来るようになってきています。
弊社は PHP 7 や Opcache の導入以前、ソースコードレベルやミドルウェアレベルの、高度で緻密なチューニングを行うことで、ゲーム案件の高負荷に耐えうる PHP サーバを提供してきました。現在はそこまで複雑なチューニングをしなくても高負荷をさばけるようになっているので、嬉しい限りです。
HHVM
さて、前置きが長くなったのですが、ここで HHVM の話に入ります。
これは、 Hip Hop Virtual Machine の略称で、 PHP や後述する Hack 言語のソースコードを x64 CPU 向けに JIT コンパイルするための Virtual Machine です。
つまり、 Zend Engine で逐一実行する代わりに、 JIT コンパイルして実行するということですね。
HHVM は Facebook が開発しています。
当初 Facebook はサーバ資源の節約や速度向上のため、PHP から C++ へのトランスパイラ(HipHop for PHP)を作っていました。その後より一層の性能向上や、運用上の幾つかの問題を解決するため、JIT コンパイルを採用した HHVM を作ったようです。
以前は PHP 5 との互換性も含めていたため、純粋に PHP 5 のソースコードを HHVM 上で動かすことで高速化の恩恵を受けることが出来ました。
PHP 7 になってからは、 HHVM を PHP プロジェクトで利用するのは推奨されなくなりました。 PHP 7 自体に十分な最適化がなされているという判断でしょうか。
ではこれは何に使うのかというと、公式では下記を推しています。
- Hack言語 の実行環境としてのサポート
- C++ 実装の共有が楽(HNI)
- 高速なビルトインサーバの Proxygen
- php-fpm と同等の FastCGI サポート
- Repo Authoritative という AOT 的サポート
- これは phar の機械語変換版みたいなものですね
Facebook は、弊社とは比べ物にならないくらい全体負荷の高いワールドワイドなサービスです。
開発初期(2004年)は PHP 4.3〜5.0 あたりで行われていたとみられますが、この頃の PHP は Opcache もランタイム最適化もほとんどなかったため、かなり実行速度が遅い言語だったのではないかと思います。
そこで、既存のコードを活かしつつ実行速度を飛躍的に向上させるために 2013〜2014 年に HHVM 及び Hack 言語を開発しリリースしました。
現在も週に100コミット以上追加されています(このリポジトリの中に Hack 言語も含まれています)。
Hack 言語
Hack 言語は、 javascript に対する TypeScript のように、 PHP に静的型付け機能を追加し、 HHVM に最適化してランタイム実行速度と開発速度を大きく向上させるために生まれました。
言語構造はほぼ PHP と変わらないため、移行は非常に簡単です。この点も TypeScript に似ていますね。
開始するタグが <?php
ではなく <?hh
であるとか、HTMLの中に埋め込む <body><?php ...
のような形が出来ないだとかの非互換性はもちろん存在します。
しかし、 PHP にはない様々な機能が追加されています。
Hack 1. 非同期
PHP はインタプリタなので、ソースコードを上から順番に実行するのが基本です(クロージャを使った場合は実行タイミングが異なることはあります)。
しかし、何か重い処理を行った場合(SQLの実行、大きいファイルの操作等)、次の処理をブロックしてしまいます。
これは PHP(≒Zend Engine) や拡張ライブラリ等が基本的にシングルスレッドで同期的に処理を行うという前提で作られているためです。
一応スレッドセーフな PHP のランタイム(ZTS版)や非同期処理を行うライブラリも存在しますが、あまりメジャーではありません。
PHP は Apache の mod_php や php-fpm をフロントエンドとして使って、スレッドレベルではなくプロセスレベルでスケーリングすることで、大量の同時リクエストでも処理出来るように構成されているので、この形でも問題なく高負荷に対応することが出来るのでそうなっています。結構独特な仕組みですね。
対して Hack はネイティブで async/await
の非同期機能をサポートしています。
これは JavaScript 等様々な言語でも実装されている非同期の仕組みです(ちなみに JavaScript も基本シングルスレッドで動いています)。これを用いることで、重い処理を行っている間も別の処理を続けることが出来ます。
<?hh namespace Hack\UserDocumentation\Async\Intro\Examples\Limtations; async function do_cpu_work(): Awaitable<void> { print("Start CPU work\n"); $a = 0; $b = 1; $list = [$a, $b]; for ($i = 0; $i < 1000; ++$i) { $c = $a + $b; $list[] = $c; $a = $b; $b = $c; } print("End CPU work\n"); } async function do_sleep(): Awaitable<void> { print("Start sleep\n"); \sleep(1); print("End sleep\n"); } async function main(): Awaitable<void> { print("Start of main()\n"); await \HH\Asio\v([ do_cpu_work(), do_sleep(), ]); print("End of main()\n"); } \HH\Asio\join(main());
このような形で PHP 的コードで非同期処理を実現しています。
Hack 2. コレクション
PHP で非常に悩ましいものとして 配列・連想配列 と呼ばれるものがあります。
これは型を array
としか定義出来ず、中に [0, 1, 3, 5]
といった通常の純粋配列でも ['a' => 'b', 'c' => ['d' => 'e']]
といった深さを持つ連想配列でもなんでも入れることが出来ます。
型を定義出来ないため、何が入っているのかわからなくなることが頻繁におきてしまいます。つらい。
それを解決するために、 Hack では細かい型を定義出来るようになっています。
- 純粋配列を表す
Vector
string
かint
のキーとそれに伴うバリューを表すMap
- ユニークで順序を持った値を表す
Set
- キーとバリューの組み合わせを表す
Pair
また、それぞれは Immutable(変更不可能) な型として定義することも可能です。
これらの型は PHP の array
を拡張定義したものなので、中身はただの配列です。しかし、型を厳密に定義出来るようになるので、「この配列がどんな構造なのかわからん」という状態を減らすことが出来ます。
他に、 enum
による複数値の定義や、 shape
という配列型の独自定義を行うことも可能となっています。
Hack 3. その他の便利機能
それ以外の機能も色々とあります。
- React っぽく HTML をコード内に埋め込める XHP
- 厳密な型チェックを事前に行う Typechecker
using
ブロックで C# っぽく廃棄処理を確実に行える Disposables- 型付き言語でおなじみ Generics
- プリミティブ型にわかりやすい別名をつけれる Type Aliases
- クロージャ(無名関数)を簡単にかける Lambdas
- ジェネリックではないけど型を分けたい時などに使える Type Constants
- アノテーションのようにメタ情報を追加出来る Attributes
callable
に型を付けれる Callables
このように、 HHVM によって実行時処理を大きく高速化し、 Hack 言語でより厳密に型を定義&チェックして開発効率を大きく高速化させることが可能なのが PHP と比べた特徴になります。
これらの Hack 言語の特徴は、 PHP 7 の型宣言などの機能にも反映されている部分があります。
現在の PHP 7 は以前と比べ非常に高速化されているので、 HHVM を使って JIT コンパイルするメリットはかなり薄くなってきました。
しかし、 Hack 言語の配列の厳密型宣言はかなり嬉しい機能ですね。長年 PHPer が悩まされてきた部分を大きく改善出来るのではないかと思います。
PHP も HHVM/Hack も、どちらも一長一短あります。
どちらも頻繁に議論や更新が今でも盛んに行われているので、「どっちが良くてどっちが悪い」という判断は下せない状況です。
PHP の方がコミュニティサポートがより強いので、そのとっつきやすさはメリットとして強くあるかなと思います。
また、 HHVM/Hack は OSS とはいえ Facebook が中心となって開発しているので、もし(あまり可能性はないと思いますが) Facebook が倒産することを考えると、若干のリスクが存在するのかなと感じます。これはどの企業関連のOSSにも共通する問題ですね。
今まで HHVM/Hack のことは知っていましたが、これを機に詳しく知ることが出来たので個人的にはこのまとめが出来ただけで満足です。
弊社では、 PHP のゆるふわ感が好きなエンジニアを募集しています。暮らしやすい札幌で PHP や VR, JavaScript, Unity などのお仕事を探している方は是非採用情報へどうぞ!