PGOでRustで書かれた広告サーバを早くする
これはRust Advent Calendar 2022の15日目の記事です。
TL;DR
- Mapboxの広告サーバにPGOを適用したらスループットが最大8%向上した
- cargo-pgoすごい便利
- BOLTは試したみたけど失敗した
PGOとは?
PGO(Profile Guided Optimization)は、プログラムを実行してプロファイラが収集した情報を基にインライン化、コードレイアウト、レジスタ割り当てを行う最適化手法です。
英語版WikipediaにしかPGOのページ[1]がなかったので意訳してみます。
つまり、PGOはコンパイラがコンパイル・リンクを終えた後のバイナリを動かしてプロファイリングし、最適化、再ビルドによって性能UPしたバイナリを作ることができます。Mozillaは自社のclangバイナリに対してPGOを使ったところ、Firefoxのコンパイル速度が9%とまで向上することに成功しました[2] また、RustのコンパイラチームはPGOを使ってrustc
のコンパイル性能を最大5%向上させることに成功しました[3]
一般的によくデザインされチューニングされたソフトウェアは、かなり大きなボトルネック解消やアーキテクチャレベルの変更をしないと9%も性能向上することは難しいです。コードの変更による工数やバグのリスクなしに9%も性能向上できればかなり嬉しいのではないでしょうか?
RustにおけるPGO
Rust BookにはProfile-guided Optimizationのページが用意されており、Rustで作ったバイナリに対して割と簡単にPGOを適用できるようになっています。
日本語で書かれている情報としては、rhysd
さんがRustでProfile-Guided Optimizationやってみたという記事を書いてくれています。記事中ではRustで実装したWasmインタプリタの性能が0~10%向上したとのことで、解説もとても分かりやすいので参考にしてみてください。
今回はcargo-pgoを使ってPGOしてみたいと思います。cargo-pgo
はPGOのためコンパイラオプションやプログラム実行をコマンドラインだけでできるようになっており大変便利です。また、後述するBOLTにも対応しています。2022年12月現在でRustでPGOを試したい場合はcargo-pgo
を使いましょう。
Mapboxの広告サーバ
Mapboxはデジタル地図上に広告を配信するプラットフォームを開発しています[4][5] 広告を配信するサーバは速度と安定性が求められることから、Rustで書かれています。今回はこの広告サーバをPGOを使ってどのくらい早くなるか試してみます。(↓をクリックするとMapboxの広告プラットフォームのサイトへ移動します)
広告バックエンドのアーキテクチャはざっくりいうとこんな感じになっています。
Mapboxの広告サーバは以下の特徴があります。
- 広告データやオークション用のデータは全てプロセス上のオンメモリにロードしている
- そのため、Loggingやクライアントとのやりとりを除いてバックエンドの担う処理の多くはCPU Boundになる
- クライアントからの地図1タイルリクエスト毎にRealtimeオークションを実施し、タイルに表示するピンを決定する。オークションはそこそこ重たい処理
- 最終的な結果はGeoJSONでクライアントに返す。GeoJSONはProtobuf等と比べて(De)Serializeは遅い
計測環境
- AWS EC2 x2
- Amazon Linux 2 / Linux Kernel: 5.10
- Linux Kernel: 5.10
- 4 vCPU / 16GB RAM
- rustc 1.65
同じVPC内に二つのEC2インスタンスを立て、Host①に広告サーバを起動し、Host②でHTTP load testerを実行しリクエストを送りスループットを計測します。
Setup
Rustのツールチェインのインストール
rustc、cargo-pgoとllvm-tools-preview
をインストールする
sudo yum udpate -y
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.bashrc
rustup component add llvm-tools-preview
cargo install cargo-pgo
計測用のツールのインストール
heyというHTTPロードテスターを使います。
sudo yum install -y htop
wget https://hey-release.s3.us-east-2.amazonaws.com/hey_linux_amd64
mv hey_linux_amd64 $HOME/.local/bin/hey
chmod +x $HOME/.local/bin/hey
計測サンプル
以下の3つのケースを計測してみます
-
Release
: PGOなしのReleaseビルドバイナリ
cargo build --release
-
PGO(Test)
:cargo-pgo
でユニットテストを実行してPGOビルドをする
cargo pgo test
cargo pgo optimize
-
PGO(Bench)
: 広告サーバを起動し、hey
で負荷をかけた後にcargo-pgo
でPGOビルドをする
cargo pgo run
# heyで負荷をかける
cargo pgo optimize
計測結果
hey
で広告サーバに10万回リクエストを20セット送りスループット(RPS)を計測しました。
単位はRPS(Request per Second)
avg | min | max | |
---|---|---|---|
Release |
11,528.13 | 10,289.82 | 12,406.1 |
PGO(Test) |
11,121.73 (-3.53%) | 10,331.65 (0.41%) 👍 | 11,387.63 (-8.21%) |
PGO(Bench) |
12,435.22 (7.87%) 👍 | 10,324.45 (0.34%) 👍 | 13,004.10 (4.82%) 👍 |
結果はなんとユニットテストでPGOした場合逆にスループットが悪くなってました 😱 Wikipediaに計測データが悪いと逆に遅くなることがあると書いてあったのはこういうことでしょうか。ちなみに広告サーバのテストコードはテストの正常系・異常系を網羅するために書いているで、APIのベンチマークは含まれていません。
hey
で負荷をかけてPGOビルドしたケースでは約8%も性能向上することができました*🎉
とても良い結果が得られたので、DockerイメージのビルドパイプラインにPGOビルドを含めたいと思います。ただ、今回はスタンドアロンで広告サーバを起動しheyで負荷をかけたのですが、Dockerビルド上でプロセスを複数立ち上げたりは正直あまりしたくないのでcargo benchを使ったPGOビルドで今回と同様な結果を出せるか継続して調査していきたいと思います。
番外編: BOLTを試してみる
cargo-pgo
はBOLTも対応しているので試してみました。BOLTとはMeta社が開発して後にllvmにマージされたバイナリ最適化ツールで、PGOよりもさらにアグレッシブな最適化をできるようです。PGOとBOLT両方で最適化を適用することもできるようです。
BOLTのDependencyのインストール
BOLTはLinuxで動くバイナリで提供されていない?っぽいのでソースからビルドする必要があります。BOLTを使わない場合はここはスキップしてもOK。
sudo yum udpate -y
sudo yum install gcc git vim htop ninja-build gcc-c++
cmakeが古いのでcmakeをソースからビルドする
wget https://github.com/Kitware/CMake/releases/download/v3.25.1/cmake-3.25.1.tar.gz
./bootstrap --prefix=$HOME/.local
tar zxvf cmake-3.25.1.tar.gz
cd cmake-3.25.1
./bootstrap --prefix=$HOME/.local
make -j2
make install
llvmをソースからビルドする (ビルドに数時間かかります)
git clone https://github.com/llvm/llvm-project
cd llvm-project
git checkout llvmorg-14.0.5
cmake -S llvm -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=$HOME/.local \
-DLLVM_ENABLE_PROJECTS="clang;lld;compiler-rt;bolt"
cd build
ninja-build install
で、cargo-pgo
でInstrumentedバイナリを作って起動したところ、curlでリクエストを送ると広告サーバはJSONのパース部分でpanicしてしまいました😅 リクエストの代わりにテストコードを実行したり、llvmを最新のHEADを取ってきて試してもpanicしてしまったのでこれ以上出来ませんでした。誰かBOLTで上手くいった人がいたらぜひご報告お願いします。
Discussion