Site icon Embarcadero RAD Studio, Delphi, & C++Builder Blogs

コンパイラがLLVMの12年分の進化を飛び越えるとどうなるのか?

この記事は、 Antonio Zapater氏によるblog記事「What Happens When Your Compiler Jumps 12 Years of LLVM Evolution?」の抄訳です。

コンパイラのアップグレードの利点の1つは、アプリケーションコードを変更する必要がないことです。同じコードベースを再構築するだけで、新しいツールチェーンがどのようなパフォーマンスを発揮するかを確認できます。

Delphi 13.2は、Linux環境ではこの比較を特に興味深いものにしています。Delphi 13.1(およびそれ以前のバージョン)はLLVM 3.3に依存していましたが、Delphi 13.2はコンパイラの進化を10年以上も遡り、LLVM 20.1に移行しています。

社内テストで既に3つのWebBrokerデプロイメントがあったので、それぞれのデプロイメントを2つのコンパイラバージョンでビルドし、同じ負荷テストを実行したらどうなるだろうかと考えました。実行してみた結果、結果は明白でした。CPU負荷の高いワークロードでは、スループットが 254% から 390% 向上し、単一リクエストのレイテンシは 71%から78% 低下しました。

しかし、いつものように、これらのパーセンテージには重要な但し書きがあり、それは最初に実行したテストから始まります。

最初の結果は簡単すぎて退屈だった

テストプロジェクトで既に利用可能なシンプルなJSONエンドポイントから始めました。リクエストを受け取り、TJSONObjectを使用してダミーのレスポンスを構築し、それを返します。コンパイラが最適化するカスタムコードがほとんどなかったため、改善は控えめでしたが、それでもApacheのスループットは約12%向上し、スタンドアロンサーバーでは約9%向上しました。

リクエスト時間の大部分は、予想通り、フレームワークの内部処理、ソケット処理、ネットワークI/Oに費やされていました。とはいえ、このベースラインは有用でした。WebBrokerを再コンパイルしただけで、突然全体的に4倍速くなるわけではないという現実的な見通しを立てることができたからです。しかし、既に有望な兆候も示していました。コードを一行も変更することなく、既に測定可能なほど高速化されていたのです。

コンパイラに処理の余地を与える

新しいツールチェーンに実際に処理させるため、すべてのリクエストに小さなCPU負荷を追加しました。8,192個の整数からなる配列を作成し、合計を計算します。私のテストでは、この処理を400回繰り返しました。

一般的なバックエンドサービスでは、データベースが主なボトルネックとなることが多いのは事実です。しかし、多くのサービスは、データが到着した後もメモリ上で相当な処理を行っています。例えば:

文字列の割り当てや予測不能な分岐を多用するコードは、タイトなループとは異なる動作をしますが、数学的なコード生成やレジスタ割り当ては、最新のコンパイラが真価を発揮する場面です。

様々な選択肢を検討した結果、各リクエストに追加するメソッドは以下のようになりました。

function BusyWork(Passes: Integer): Int64;
var
  I, R: Integer;
  A: TArray<Integer>;
  Sum: Int64;
begin
  if Passes <= 0 then
    Exit(0);

  SetLength(A, 8192);
  Sum := 0;
  for R := 1 to Passes do
  begin
    for I := 0 to 8191 do
      A[I] := I * 17 + (R and 255);
      
    for I := 0 to 8191 do
      Sum := Sum + A[I];
  end;
  Result := Sum;
end;

そして差はさらに広がった ― RAD Studio 13.2 の新しい LLVM 最適化は驚異的だった!

この CPU ループが実装されたことで、13.1 と 13.2 のパフォーマンスの差は飛躍的に大きくなりました。LLVM 20 は、LLVM 3.3 と比較して、ループのベクトル化、レジスタ割り当て、不要なメモリ操作の削減において、格段に賢くなっています。

Apache WebBrokerモジュール、nginxの背後にあるFastCGI、そしてIndyを使用したスタンドアロンWebBrokerサーバーで比較を行いました。以下の表は、両方のビルドがリクエストを100%正常に完了した同時実行レベルに特化しています。

Stackクリーンな同時実行数スループット増加量レイテンシ(同時実行数 1)
Apache ModuleConcurrency 200+390%-78%
FastCGI + nginxConcurrency 200+254%-77%
StandaloneConcurrency 25+297%-71%

Apacheは最もクリーンな結果を示しました。同時実行数1から200までのすべてのテストポイントにおいて、両バージョンともすべてのリクエストが成功し、バージョン13.2を使用した場合、平均でスループットが390%向上しました。FastCGIも大幅に改善し、初期段階でピークに達した後、設定されたワーカープロセスの上限付近で安定しました。

スタンドアロン版はリクエストごとにHTTPとIndyのオーバーヘッドが多く、単一リクエストでの改善率は若干小さくなりましたが、それでもレイテンシは71%低下し、同時実行数25でのスループットは297%向上しました。

WebBrokerの設定方法と使用したハードウェア構成

テスト環境は意図的に簡素なものにしました。

留意すべき点として、13.1 バイナリは RTL 37.1 上で動作していましたが、13.2 は RTL 37.2 上で動作していました。ランタイムライブラリはコンパイラと同時に更新されているため、これらの改善点のすべてを LLVM のみに起因するものと断定することはできません。改善点の一部は、Delphi RTL 自体内部で継続的に行われている最適化によるものです。

障害発生時の状況

高負荷の同時実行処理は、安定性にも顕著な違いをもたらしました。

この小規模な仮想マシン上でスタンドアロンサーバーの同時接続数を50以上に増やしたところ、どちらのビルドも最終的に処理能力を超え、5xxエラーを返すようになりました。しかし、その失敗のパターンは異なっていました。過負荷状態下では、Delphi 13.1はリクエストの20%から28%しか成功しませんでしたが、Delphi 13.2は83%から99%の成功率を維持しました。13.1のApacheは同時接続数が250前後で接続をいくつか切断し始めましたが、13.2はすべてのリクエストを処理しました。

注:失敗したリクエストはスループットにカウントされないため、上記の表には失敗した実行は含まれていません。

LLVMベンチマークを自分で試す方法

ご自身の環境でこれらのテストを実行したい場合は、プロジェクトファイルの簡略版をGitHubリポジトリに用意しています。

リポジトリには以下が含まれます。

PAServer を介して Delphi 13.1 と 13.2 を使用して、Linux64 向けにリリースモードで両方のプロジェクトをビルドしてください。コードは自動的にチェックサムを出力(または返します)するため、タイミングを比較する前に両方のビルドが同一の出力を生成していることを確認できます。

このテスト環境では、コードを一行も変更することなく、13.2 に移行することで、CPU 負荷の高いタスクのリクエストレイテンシが 70% 以上短縮されました。Linux 上で Delphi サービスを実行している場合、13.2 へのアップグレードは、おそらく今年最も迅速なパフォーマンス向上策となるでしょう。

最後に:rpmalloc について

RAD Studio Delphi 13.2では、LinuxおよびWindows on Arm向けに rpmalloc のオプションサポートが導入されたことを付け加えておきます。rpmallocは、マルチスレッド処理によるメモリ割り当てに特化したメモリマネージャです。

BusyWork メソッドはリクエストごとに配列メモリを割り当てるため、rpmalloc を使用すると負荷がかかった際にこれらの数値がさらに上昇する可能性があると直感的に考えています。ただし、rpmalloc はメモリ使用量が多く、メモリリークの報告機能も組み込まれていません。

今回のテストでは、明確な基準値を維持するためにデフォルトのメモリマネージャを使用しましたが、後日フォローアップテストを実施する予定です。

Exit mobile version