コンパイル・キューに対するTruffleアプローチ

バージョン21.2.0のTruffleは、コンパイル・キューイングへの新しいアプローチを備えています。このドキュメントでは、このアプローチの動機付けと概要を示します。

コンパイル・キューとは何ですか。

ゲスト・コードの実行中、各Truffleコール・ターゲットは、その実行回数およびそれらの実行中に発生したループ反復回数(つまり、ターゲットの「コール数およびループ数」)をカウントします。このカウンタが特定のしきい値に達すると、コール・ターゲットが「ホット」とみなされ、コンパイルがスケジュールされます。ゲスト・コードの実行に与える影響を最小限に抑えるために、ターゲットがコンパイルされるべきという概念はコンパイル・タスクとして具体的になり、コンパイルを待機するためにコンパイル・キューに配置されます。Truffleランタイムは、キューからタスクを取得し、指定されたコール・ターゲットをコンパイルする複数のコンパイラ・スレッド(--engine.CompilerThreads)を生成します。

Truffleのコンパイル・キューの初期実装は簡単なFIFOキューでした。このアプローチには、ゲスト・コード実行のウォームアップ特性に関する重要な制限があります。つまり、すべてのコール・ターゲットがコンパイルに等しく重要であるわけではありません。この目的は、実行時間が長くなるターゲットを識別し、それらを最初にコンパイルすることで、より優れたパフォーマンスに早期に導くことです。カウンタが特定のしきい値に達すると、コール・ターゲットはコンパイルのためにキューに入れられるため、FIFOキューはターゲットをそのしきい値に達した順にコンパイルし、実際には、実際の実行時間と関連しません。

次の玩具のJavaScriptの例を考えてみます:

function lowUsage() {
    for (i = 0; i < COMPILATION_THRESHOLD; i++) {
        // Do something
    }
}

function highUsage() {
    for (i = 0; i < 100 * COMPILATION_THRESHOLD; i++) {
        // Do something
    }
}

while(true) {
    lowUsage();
    highUsage();
}

lowUsagehighUsageの両方の関数は、初回の実行でも十分に高いコール数およびループ数のしきい値に達しますが、lowUsage関数が最初に到達します。この例ではhighUsage関数を最初にコンパイルするように示されていますが、より優れたパフォーマンスをより早く実現するために、FIFOキューを使用して、lowUsage関数を最初にコンパイルします。

コンパイル・キューのトラバース

Truffleの新しいコンパイル・キューは、「トラバース・コンパイル・キュー」と呼ばれ、より動的なアプローチでターゲットのコンパイル順序を選択します。コンパイラ・スレッドが次のコンパイル・タスクを要求するたびに、キュー内のすべてのエントリがトラバースされ、優先順位が最も高いエントリが選択されます。

タスクの優先度は複数の要因に基づいて決定されます。

まず、第1層コンパイル(つまり、第1層タスク)にスケジュールされたターゲットは、常に第2層タスクより高い優先度を持ちます。この論拠は、インタプリタでのコードの実行と、第1層コンパイル済コードでのコードの実行のパフォーマンスの差が、階層1と階層2のコンパイル済コードの違いよりもはるかに大きいという点です。つまり、これらのターゲットをより早くコンパイルすることで、よりメリットが得られます。また、第1層のコンパイルには通常時間がかかりません。したがって、1つのコンパイラ・スレッドは、1つの第2層コンパイルの完了と同時に複数の第1層コンパイルを完了できます。このアプローチは、特定のシナリオでパフォーマンスが低いことを示しており、今後のバージョンで改善される可能性があります。

同じ層の2つのタスクを比較する場合、最初にそれらのコンパイル履歴を検討し、より高いコンパイラ層で以前にコンパイルされたタスクを優先します。たとえば、コール・ターゲットが最初にコンパイルされ、なんらかの理由で無効化され、その後、第1層コンパイルのために再度キューに入れられると、コンパイルされたことがない他のすべての第1層ターゲットよりも優先されます。これは、以前にコンパイルされていた場合、それが明確に重要であり、無効化により必要以上にペナルティが課せられないようにするためです。

最後に、2つの前の条件で2つのタスクの優先度を差別化できない場合は、より高い「重み」を持つタスクに優先度を与えます。重みとは、ターゲットのコールとループの数および時間の関数です。これは、ターゲットのコール数およびループ数が過去1ミリ秒で増加した割合でのそのコールおよびループ数の積として定義されます。このメトリックは、ターゲットのコール数およびループ数を、そのコール・ターゲットの実行に費やされた時間のプロキシとして使用することで、そのコール・ターゲットの実行に費やされた合計時間を、その時間の最近の増加と均衡化することを目的としています。これにより、「ホット」だったが、現在は多く実行されていないターゲットと比較して、現在「非常にホット」のターゲットの優先度が上昇します。

パフォーマンス上の理由から、タスクの重みはキャッシュされ、1ミリ秒間再利用されます。キャッシュされた値が1ミリ秒より古い場合は、再計算されます。

トラバース・コンパイル・キューはバージョン21.2.0でデフォルトでオンになり、--engine.TraversingCompilationQueue=falseを使用して無効化できます。

動的コンパイルしきい値

コンパイル・キューのトラバースの問題の1つは、最新の重みを取得し、最も優先度の高いタスクを選択するためにキュー内のすべてのエントリをトラバースする必要があることです。キューのサイズが適切であるかぎり、パフォーマンスに大きな影響はありません。つまり、適当な時間内で最も優先度の高いタスクを常に選択するには、キューが無限に増大しないようにする必要があります。

これは、「動的コンパイルしきい値」と呼ばれるアプローチによって実現されます。簡単に言うと、動的コンパイルしきい値は、コンパイルしきい値(コンパイルするかどうかを決定する際に各コール・ターゲットのコール数およびループ数が比較される対象)が、キューの状態に応じて時間の経過とともに変化する可能性があることを意味します。キューが過負荷の場合、コンパイルしきい値を増やして、受信コンパイル・タスクの数を減らします。つまり、コンパイルをスケジュールするには、ターゲットが「よりホット」である必要があります。一方、キューが空に近い場合は、コンパイルしきい値を減らして、より多くのターゲットがコンパイルにスケジュールされるようにできます。つまり、コンパイル・スレッドがアイドル状態の恐れがあるため、「よりホットではない」ターゲットもコンパイルするようにします。

しきい値は、実際のところscale関数で決定される「スケール係数」による倍数であるため、このしきい値の変更を「スケーリング」と呼びます。スケール関数は、キュー内のタスクの数をコンパイラ・スレッドの数で割った値であるキューの「負荷」を入力として取得します。キュー内のタスクの生の数値データが、コンパイルの圧迫がどの程度存在するかについての適切なプロキシではないため、コンパイラ・スレッドの数を意図的に制御します。たとえば、平均コンパイル時間が100ミリ秒であり、キュー内に160タスクがあると仮定します。16スレッドで実行すると、約10 * 100ミリ秒(1秒)ですべてのタスクが終了します。一方、2つのコンパイラ・スレッドで実行すると、約80 * 100ミリ秒(8秒)かかります。

スケール関数は、--engine.DynamicCompilationThresholdsMinScale--engine.DynamicCompilationThresholdsMinNormalLoadおよびDynamicCompilationThresholdsMaxNormalLoadの3つのパラメータによって定義されます。

--engine.DynamicCompilationThresholdsMinScaleオプションは、しきい値のスケーリングの下限を定義します。デフォルト値は0.1であるため、コンパイルしきい値はそのデフォルト値の10%未満にスケーリングされません。これは、実際には、定義によるscale(0) = DynamicCompilationThresholdsMinScaleまたはデフォルト値のscale(0) = 0.1であることを意味します。

--engine.DynamicCompilationThresholdsMinNormalLoadオプションは、コンパイルしきい値がスケーリングされない最小負荷を定義します。つまり、キューの負荷がこの値を超えているかぎり、ランタイムはコンパイルしきい値をスケール・ダウンしません。これは、実際には、定義によるscale(DynamicCompilationThresholdsMinScale) = 1またはデフォルト値のscale(10) = 1であることを意味します

--engine.DynamicCompilationThresholdsMaxNormalLoadオプションは、コンパイルしきい値がスケーリングされない最大負荷を定義します。つまり、キューの負荷がこの値を下回っているかぎり、ランタイムはコンパイルしきい値をスケール・アップしません。これは、実際には、定義によるscale(DynamicCompilationThresholdsMaxScale) = 1またはデフォルト値のscale(90) = 1であることを意味します

これまでは、scale関数を3つのポイントに定義しました。これらのポイント間の値については、scale関数は、これら2つのポイントを結ぶ直線です。つまり、最小および最大の通常の負荷間のすべての値の場合、スケール関数は定義による1です。0から最小の通常負荷の間の値の場合、scale関数は最小スケールと1の間で直線的に増加します。この関数の勾配をsとして定義します。次に、関数ドメインの残りの部分(つまり、最大の通常の負荷より大きい値)について、scaleを、sがポイント(DynamicCompilationThresholdsMaxNormalLoad, 1)を通過する勾配のある線形関数として定義します。

次に示すのは、スケール関数のASCIIアート・プロットであり、その定義方法を表しています。

          ^ scale
          |
          |                                            /
          |                                           /
          |                                          /
          |                                         /
          |                                        /
          |                                       /
        1 |..... ________________________________/
          |     /.                               .
          |    / .                               .
          |   /  .                               .
          |  /   .                               .
          | /    .                               .
MinScale >|/     .                               .
          |      .                               .
          |_______________________________________________________> load
         0       ^                               ^
              MinNormalLoad                   MaxNormalLoad

動的しきい値は、トラバース・コンパイル・キューでのみ機能し、バージョン21.2.0ではデフォルトでオンになっています。--engine.DynamicCompilationThresholds=falseを使用して無効化できます。