こんにちは、hachi8833です。今回の翻訳記事は、Railsを含む一般的な開発・システム管理で使えると思います。
Railsのパフォーマンス改善については、先日開催されたぎんざRuby会議01でのk0kubunさんの発表スライドと合わせて読むとさらによいと思います。
概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Understanding your benchmarks and easy tips for fixing them
- 公開日: 2017/07/13
- 著者: Hongli Lai
- サイト: https://blog.phusion.nl/: Webサーバー「Passenger」で知られるオランダのPhusion社のブログです。
ベンチマークの詳しい理解と修正のコツ(翻訳)
開発者は本質的にスピードを愛するがゆえに、ベンチマークをも愛します。プログラミング言語のパフォーマンス、アプリサーバーのパフォーマンス、JavaScriptエンジンのパフォーマンスなどのベンチマークは、常に開発者の熱い注目を集めます。しかしながら、よいベンチマークを実行するためにはさまざまな注意が必要です。そのひとつが安定性です。ベンチマークを繰り返し実行すると、タイミングは少しずつ変動します。そして実に多くの人がこの点を見過ごしたまま、全アプリのシャットダウン後にベンチマークを数回ほど再実行して結果の平均を取るだけで済ませています。これで本当によいのでしょうか?
最近私は、第三者が確実に再現可能な信頼できるベンチマークの作成に関心を抱き、ベンチマークの安定性について調査を進めています。これによってベンチマークの結果を第三者が自分で検証できるようになり、たとえば私が開発したソフトウェアのユーザーに、ベンチマークの信頼性を検証してもらえるようになります。調査を進めるうちに、Victor StinnerというPythonのコア開発者のことを知りました。StinnerはPython 3のパフォーマンスを何年にも渡って熱心に改良し続けています。
最近StinnerはPyCon 2017の発表(発表内容はLinux Weekly Newsに非常によくまとめられています)でこの問題について触れています。Stinnerは、最適化が本当に有用かをテストする方法や、パフォーマンスでリグレッション発生の有無をテストする方法や、コミットによるパフォーマンスの変動を長期に渡ってトラックするために、CIツールに統合できる形式のテストを必要としていました。そしてStinnerはベンチマークの方法論を編み出すとともに、perfというベンチマーク用フレームワークも開発しました。
Stinnerの方法論はつぎのようにまとめられます。
- ベンチマークではウォームアップを行うこと
- ベンチマークの繰り返し回数は時間ベース(time budget)で決めること
- マルチプロセスで連続実行すること
- 大きな偏差を調査すること
- システムをカーネルレベル・ハードウェアレベルでチューニングすること
システムのチューニング作業は以下に分けられます。
- CPU親和性の維持
- カーネルスケジューラを専用のコアで実行する
- CPUクロックを一定にし、最小限のクロックスピードにする
- ベンチマーク専用のハードウェアを使う
そこでStinnerの方法論とチューニング戦略をもう少し詳しく見てみたいと思います。私はStinnerの方法論をまだ実践していませんが、別の機会に私も実際に上のような分析結果をレポートしたいと思います。
1. ベンチマークではウォームアップを行うこと
ベンチマークにウォームアップが必要であることは、常識として標準になりつつあります。
Stinnerは最初に回すベンチマークをウォームアップとして扱い、結果を破棄しています。これは、遅延読み込みデータをウォームアップ時に初期化し、必要なキャッシュ(CPUキャッシュ、I/Oキャッシュ、JITキャッシュなどすべて)を有効にしておくためです。ウォームアップ1回では足りない場合、必要に応じてperfツールでウォームアップの繰り返しを設定できます。
2. ベンチマークの繰り返し回数は時間ベース(time budget)で決めること
ベンチマーク実行回数の決定を軽く考えてはいけません。回数が少ないとベンチマークの信頼性が損なわれますし、回数が多いとベンチマークに時間がかかりすぎて開発者の意欲が削がれてしまいます。
Stinnerの方法論ではこのコンセプトを「time budget」(ストップウォッチ・ベンチマーク)と呼んでいます。繰り返し回数を最初に決めるのではなく、妥当なベンチマーク期間を最初に決めてその間の実行回数を計測し、その結果に基いてテストの繰り返し回数を決定します。そしてこの作業自体を何度も繰り返して(次のセクションを参照)、結果の平均をとります。
3. マルチプロセスで連続実行すること
Stinnerは、ベンチマークをシングルプロセスだけで行わずに、20の異なるプロセスで繰り返し実行します。各プロセスでは、前述のtime budgetで求めた繰り返し回数に基いてベンチマークを実行します。続いて、全プロセスでの実行結果の平均を取ります。理由は、プロセスにはある程度のランダムなステートが存在し、それがパフォーマンスに影響するためです。
たとえばASLR(アドレス空間配置のランダム化)は、ある種のセキュリティ攻撃の防止に常用される手法ですが、このランダム化はコードやデータの局所性に影響するため、ベンチマークのパフォーマンスが変動します。
他には、RubyやPythonで使われるハッシュテーブル関数シードの問題もあります。ハッシュテーブルの衝突を悪用した攻撃を緩和するために、RubyやPythonのハッシュテーブル関数は起動時にランダムに選択されたシードに依存しますが、これによってハッシュテーブルのパフォーマンスが変動します。
かといって、ASLRやハッシュテーブルのランダムシードといった要素を無効にするのはよい考えとはいえません。こうした機能を無効にするとシステムに潜在的な脆弱性が生じるが可能性があるばかりでなく、ベンチマーク結果が現実のパフォーマンスとかけはなれてしまいます。さらに、前述したランダムな要素は氷山の一角でしかなく、この他にも多くのランダムな要素があります。Stinnerは、ベンチマークをマルチプロセスで繰り返して平均を取ることで、こうしたパフォーマンス変動も計算に入れています。
4. 大きな偏差を調査すること
Stinnerは平均のほかに、標準偏差や、測定値が平均からどのぐらいかけ離れているかもチェックします。標準偏差が異常に高い場合や、最大偏差や最小偏差が平均からかけ離れている場合には、結果が有効でないとみなして偏差が発生した原因を調査します。Stinnerのperf
ツールは、標準偏差が平均の10%を超える場合や、平均からの最大偏差や最小偏差が少なくとも50%に達した場合に警告を出力してくれます。
5. CPU親和性を保つこと
OSは、1つのプロセスや1つのスレッドを任意のCPUコアに任意のタイミングでスケジューリングできます。プロセスやスレッドが別のCPUコアに移動するたびに、ベンチマークの安定性に影響が生じます。これは、移動先の新しいコアのL1キャッシュデータが元のコアのL1キャッシュと同じでないためです。プロセスやスレッドが元のコアに戻るまでの間に、元のコアのキャッシュは他の作業で汚されてしまいます。したがって、次の点が重要です。
- プロセスやスレッドが特定のCPUコアから移動しないようにすること。
- 1つのCPUコアが一度に1つのスレッドだけ(すなわちプロセスも1つだけ)を実行するようにすること。
これをCPU親和性(affinity)と呼びます(訳注: wikipedia-ja プロセッサ親和性)。
ベンチマークを実行するシステムには、十分な数のCPUコアが搭載されているべきです。ただしハイパースレッディングは「本物のCPUコア」には含まれません。2つのハイパースレッドコアでは同じ物理コアを(そして同じL1キャッシュも)共有しており、CPUのタイムスライスなどの値はどちらのハイパースレッドがRAMの空きを待っているかによって変わります。
Linuxには、CPU親和性を設定するツールが2つ用意されています。
taskset
コマンドは、特定のプロセスやスレッドをどのCPUコアにスケジューリングしてよいかをカーネルに伝えます。Xmodulo.comのGlenn Klockwood氏によるチュートリアルにも、taskset
をスレッドに対して使う方法のチュートリアルがあります。
コードの変更が可能な場合は、アプリケーションでpthread_setaffinity_np()
関数を呼んでCPU親和性を設定する方法もあります。
- カーネルの
isolcpus
コマンドラインオプションを使うと、特定のCPUコアをスケジューリングプロセスから除外できます(taskset
などでユーザーが指定した場合を除く)。利用方法についてはperf
のドキュメントをご覧ください。
6. カーネルスケジューラを専用のコアで実行すること
Linuxカーネルのスケジューラは、1つのCPUコアでタイムスライスを実行するために、最大1000回/秒の割り込みをかけます。各tickでの割り込みのたびにCPUコアでコンテキストスイッチが発生してCPUキャッシュが汚れる(カーネルは独自のコードデータ構造をメモリに読み込まなければならない)ため、これはよくありません。したがって、実行中のベンチマークで使っていないコアでカーネルのスケジューラ割り込みを処理するようにカーネルを設定することが推奨されます([1][2][3])。
この設定に使えるツールは、nohz_full
とrcu_nocbs
の2つです。オプションのドキュメントはカーネルのNO_HZ.txtにあります。
nohz_full
はスケジューラのモードをfull tickless
に設定し、rcu_nocbs
はカーネルスケジューラを特定のCPUコアでのみ実行します。LinuxのIntel P-state Power Savingドライバとの互換性の問題があるため、2つのオプションを併用する必要があります。これについてはStinnerのブログで指摘されており、Red HatのBugzillaにも報告されています。
7. CPUクロックを一定にし、最小限のクロックスピードにすること
現代のCPUのクロックスピード(周波数)は一定ではありません。必要に応じて最大パフォーマンスを発揮し、作業がほとんどない状態では電力を節約するために、CPUのクロックスピード周波数は頻繁に変更されます。このため、負荷および負荷のかかるタイミングに応じてシステムパフォーマンスが大きく変動することがあります。さらにCPU温度も大きく影響します。現代のCPUにはさまざまなオーバーヒート防止メカニズムが搭載されており、CPUが過熱すると引き下げます。
クロックスピードのスケーリングは、次のテクノロジーによって支配的な影響を受けています。
- Intel Turbo Boost(CPUターボボタンの現代版と言えるが、自動制御される)
- C-State(パワーマネジメント制御)
- P-State(実行クロック制御)
これらについては、Stinnerのブログ(その1、その2)に詳しい情報が記載されています。
これらの挙動はいずれもベンチマークの安定性を損ないます。そこで、Stinnerの方法論ではCPUクロックを可能な限り最小の値に設定したうえで、CPUクロックの自動スケーリング機構をすべて無効にします。一定の最小値に設定する方が、一定の最大値に設定するよりも熱の発生を防止できるからです。こうすることで、ベンチマークの生のパフォーマンスが問題にならなくなり、あるベンチマーク結果と別のベンチマーク結果の比較に信頼が置けるようになります。
CPUクロックスピードを最小値に設定するには、カーネルレベルで以下を実行します。
cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq | sudo tee /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq
ただしこの設定は再起動で失われてしまいますので、cpufrequtils
などのユーザー空間で動作するツールを使うこともできます。カーネル設定を直接変更するよりも、cpufrequtils
で設定する方が最も望ましいように思えます。Debianの場合、/etc/default/cpufrequtilsを編集してGOVERNOR=powersave
を設定します。Red Had Power Management Guideには、cpufrequtils
でgovernor
値を変更すると何が変わるかについての解説があります。
8. ベンチマーク専用のハードウェアを使うこと
ベンチマークを手元のノートパソコンで動かすと他のアプリと干渉する可能性があるため、ベンチマークの安定性という面でよくありません。アプリをすべて閉じたとしても、ノートパソコンを動かすための必須プログラムはベンチマークでは不要であるため、やはり不十分です。上述のカーネルレベルのチューニングやハードウェアレベルのチューニングは、ノートパソコンではうまく実行できないこともあります。たとえば、私のMacではLinuxが動きませんし、安定したベンチマークのためにLinuxを何とかして動かそうという気にもなれません。
もうひとつの問題は、今使っているノートパソコンの環境を完全には再現できないことです。ベンチマークを再現したいユーザーにとって、私のノートパソコンと完全に同一のハードウェアとソフトウェアを揃えるのは簡単なことではありません。
ノートパソコンが便利なのは、開発-テストを短いサイクルで回す限りにおいてです。ベンチマーク専用のハードウェアを購入・セットアップするとなるとコストも時間もかかります。かといってクラウドサーバーや仮想マシンでは安定したパフォーマンスが保証されないため、いずれもベンチマークには不向きです。同一のホストシステムで他のVMが動けばCPUパワーを持って行かれてしまいますし、ハイパーバイザーのコンテキストスイッチも発生します。クラウド環境の提供するCPU性能バースト機能(他のVMがアイドル時に自分のCPU割り当てを増やす)は、本番環境では素晴らしい機能ですがベンチマークの安定性にはよくありません。クラウドVMは仮想化されているため、ハードウェアレベルのチューニングの多くはクラウドサーバーには使えません。
9. 現実的なベンチマーク環境ホスティングプロバイダを求めて
ではどうすればよいのでしょうか。十分実現可能で使いやすく、コスト的にも見合う選択肢として、以下のサービスを見繕ってみました。
- Amazon EC2 Dedicated Hosts
- 定義済みの専用ハードウェアでEC2インスタンスを起動できます。料金は1時間単位です。インスタンスは仮想化されるため、ハードウェアへの直接アクセスは許可されていませんが、理想にかなり近い選択肢です。
メリット
- AWSの知名度が非常に高く、多くのユーザーが手軽に利用できる。
- 専用ホスト+インスタンスのスピンアップと設定は完全に自動化されている。
- 時間課金。
- CPUクロックの制御が公式サポートされている。
デメリット
- CPUが仮想化されるため、CPU親和性で特定の物理CPUコアを設定できない。これがどの程度大きな問題かはわかりませんが、今後調査が必要な部分でしょう。
- Hetzner
- 月額ベースの専用ハードウェアホスティングプロバイダであり、39ユーロ/月という低コストで専用サーバーを借りられます(ただし設定費用は最大で79ユーロ)。
メリット
- 時間課金ではないがそれでも安価。
- ハードウェアにフルアクセス可能。CPU親和性、CPU周波数スケーリングの無効化など何でも設定できる。
デメリット
- サーバーモデルに応じた設定費用が必要。
- 月単位の利用。これはこれでよいが、時間課金には見劣りする。
- Hetznerが提供するハードウェアは定期的に変更されるため、古いハードウェアモデルは利用できなくなる。第三者が完全に同一のハードウェアでベンチマークを再現したい場合には問題。
- 自動化は一部のみで、APIもAWSほど充実していない。
- RackSpace OnMetal
- 専用ハードウェアを時間課金ベースで提供します。
同社のコントロールパネルに問題があったために使う機会がこれまでなく、専用ハードウェアにフルアクセス可能か仮想化レイヤどまりなのかについては今のところ判断材料がありません。
- Scaleway Bare Metal Cloud
- 専用のx86_64(Intel Atom)サーバーやARMサーバーを提供します。
メリット
- 時間課金
- APIやCLIツールで自動化可能
- きわめて安価
デメリット
- 同社のARMハードウェアは奇妙な形でクラッシュするらしく、今のところ好印象を得ていません。
注意したい点
- 同社のx86_64サーバーがどのように仮想化されているかが未だに不明確で、物理コアにCPU親和性を設定できるかどうかを確認できずにいます。
- 同社の提供するx86_64はAtom CPUです。私はARMについては経験がありません。一見したところ、ARMにはCPU周波数のスケーリング機能が見当たりません(または、少なくとも設定できません)。どなたかご存知でしょうか。
- 同社のサービスの安定性についてはやや不安が残ります。本格的にproduction向けの負荷をかけるのはおすすめできませんが、ベンチマークの実行なら問題ありません。
結論
Stinnerのベンチマーク方法論は統計的手法をさほど採り入れておらず、もっぱらシステムチューニングに特化しています。Stinnerはベンチマーク結果の標準偏差をチェックし、偏差が必要以上に大きい場合には結果を破棄して、ベンチマークの安定化に必要なチューニング方法を模索します。Stinnerの方法の大半は、OSレベルやハードウェアレベルに着目しています。ASLRやランダムシードの影響を避けるためにベンチマークをマルチプロセスで実行し、カーネルのCPU設定やCPUのクロックスピードをチューニングします。
Stinnerの方法論の多くは、perfツールに込められています。Stinnerは、自身の方法論が結果の安定化と信頼性向上に非常に役立ったと述べています。その成果は新しいPythonベンチマークスイートや、ベンチマーク結果を長期間トラックできるCIシステムに結実しています。
私自身はまだStinnerの方法論を実践していませんが、この次のベンチマークはきっとこの方法論で進め、その成果をレポートしたいと思います。
ベンチマークに興味が湧いてきましたら、この記事がお役に立ったかどうか、共有したくなる洞察を得られたかどうか、ぜひ私までお知らせください。皆さまのご多幸とハッピーベンチマークを願っております。
関連資料(すべて英語)
- Pythonを高速化する — Linux Weekly News
- ベンチマークを安定化するには — presentation by Victor Stinner at FOSDEM 2017 Brussels
- 再現可能なベンチマークを実行する — by Victor Stinner
- ベンチマークに使うシステムのチューンアップ — by Victor Stinner
- ベンチマーク関連リンク集 — by Victor Stinner
- 安定なベンチマークを求めてpart1: システム編 — Victor Stinner's blog
- 安定なベンチマークを求めてpart3: 平均値編 — Victor Stinner's blog
- ストップウォッチベンチマークってありなの? — Stack Overflow
nohz_full=godmode
について — Jeremy Eder's blog- Linuxカーネルドキュメント: tickless schedulerモード
- ticklessカーネルの設定: isolcpus、nohz-full、rcu_nocbs — Stack Overflow
- CPU frequency scalingを止める — Stack Overflow
- CPUfrequtilsで使うgovernorの種類 — Red Hat Power Management Guide
- AWS EC2インスタンスのプロセッサステート制御 — Amazon EC2
関連記事
- RailsConf 2017のパフォーマンス関連の話題(1)BootsnapやPumaなど(翻訳)
- RailsConf 2017のパフォーマンス関連の話題(2)rack-freezeやsnip_snipなど(翻訳)
- RailsConf 2017のパフォーマンス関連の話題(3)「あなたのアプリサーバーの設定は間違っている」など(翻訳)
- Railsで検索を高速化するならこれで決まり!Sunspotで始めるSolr入門
- [Rails 4.0] 巨大なテーブルやserializeを使うときのActiveRecordオーバーヘッドを測定してみた