- IT Tips
READ MORE
こんにちは、hachi8833です。今回の翻訳記事は、Railsを含む一般的な開発・システム管理で使えると思います。
Railsのパフォーマンス改善については、先日開催されたぎんざRuby会議01でのk0kubunさんの発表スライドと合わせて読むとさらによいと思います。
原著者の許諾を得て翻訳・公開いたします。
開発者は本質的にスピードを愛するがゆえに、ベンチマークをも愛します。プログラミング言語のパフォーマンス、アプリサーバーのパフォーマンス、JavaScriptエンジンのパフォーマンスなどのベンチマークは、常に開発者の熱い注目を集めます。しかしながら、よいベンチマークを実行するためにはさまざまな注意が必要です。そのひとつが安定性です。ベンチマークを繰り返し実行すると、タイミングは少しずつ変動します。そして実に多くの人がこの点を見過ごしたまま、全アプリのシャットダウン後にベンチマークを数回ほど再実行して結果の平均を取るだけで済ませています。これで本当によいのでしょうか?
最近私は、第三者が確実に再現可能な信頼できるベンチマークの作成に関心を抱き、ベンチマークの安定性について調査を進めています。これによってベンチマークの結果を第三者が自分で検証できるようになり、たとえば私が開発したソフトウェアのユーザーに、ベンチマークの信頼性を検証してもらえるようになります。調査を進めるうちに、Victor StinnerというPythonのコア開発者のことを知りました。StinnerはPython 3のパフォーマンスを何年にも渡って熱心に改良し続けています。
最近StinnerはPyCon 2017の発表(発表内容はLinux Weekly Newsに非常によくまとめられています)でこの問題について触れています。Stinnerは、最適化が本当に有用かをテストする方法や、パフォーマンスでリグレッション発生の有無をテストする方法や、コミットによるパフォーマンスの変動を長期に渡ってトラックするために、CIツールに統合できる形式のテストを必要としていました。そしてStinnerはベンチマークの方法論を編み出すとともに、perfというベンチマーク用フレームワークも開発しました。
Stinnerの方法論はつぎのようにまとめられます。
システムのチューニング作業は以下に分けられます。
そこでStinnerの方法論とチューニング戦略をもう少し詳しく見てみたいと思います。私はStinnerの方法論をまだ実践していませんが、別の機会に私も実際に上のような分析結果をレポートしたいと思います。
ベンチマークにウォームアップが必要であることは、常識として標準になりつつあります。
Stinnerは最初に回すベンチマークをウォームアップとして扱い、結果を破棄しています。これは、遅延読み込みデータをウォームアップ時に初期化し、必要なキャッシュ(CPUキャッシュ、I/Oキャッシュ、JITキャッシュなどすべて)を有効にしておくためです。ウォームアップ1回では足りない場合、必要に応じてperfツールでウォームアップの繰り返しを設定できます。
ベンチマーク実行回数の決定を軽く考えてはいけません。回数が少ないとベンチマークの信頼性が損なわれますし、回数が多いとベンチマークに時間がかかりすぎて開発者の意欲が削がれてしまいます。
Stinnerの方法論ではこのコンセプトを「time budget」(ストップウォッチ・ベンチマーク)と呼んでいます。繰り返し回数を最初に決めるのではなく、妥当なベンチマーク期間を最初に決めてその間の実行回数を計測し、その結果に基いてテストの繰り返し回数を決定します。そしてこの作業自体を何度も繰り返して(次のセクションを参照)、結果の平均をとります。
Stinnerは、ベンチマークをシングルプロセスだけで行わずに、20の異なるプロセスで繰り返し実行します。各プロセスでは、前述のtime budgetで求めた繰り返し回数に基いてベンチマークを実行します。続いて、全プロセスでの実行結果の平均を取ります。理由は、プロセスにはある程度のランダムなステートが存在し、それがパフォーマンスに影響するためです。
たとえばASLR(アドレス空間配置のランダム化)は、ある種のセキュリティ攻撃の防止に常用される手法ですが、このランダム化はコードやデータの局所性に影響するため、ベンチマークのパフォーマンスが変動します。
他には、RubyやPythonで使われるハッシュテーブル関数シードの問題もあります。ハッシュテーブルの衝突を悪用した攻撃を緩和するために、RubyやPythonのハッシュテーブル関数は起動時にランダムに選択されたシードに依存しますが、これによってハッシュテーブルのパフォーマンスが変動します。
かといって、ASLRやハッシュテーブルのランダムシードといった要素を無効にするのはよい考えとはいえません。こうした機能を無効にするとシステムに潜在的な脆弱性が生じるが可能性があるばかりでなく、ベンチマーク結果が現実のパフォーマンスとかけはなれてしまいます。さらに、前述したランダムな要素は氷山の一角でしかなく、この他にも多くのランダムな要素があります。Stinnerは、ベンチマークをマルチプロセスで繰り返して平均を取ることで、こうしたパフォーマンス変動も計算に入れています。
Stinnerは平均のほかに、標準偏差や、測定値が平均からどのぐらいかけ離れているかもチェックします。標準偏差が異常に高い場合や、最大偏差や最小偏差が平均からかけ離れている場合には、結果が有効でないとみなして偏差が発生した原因を調査します。Stinnerのperf
ツールは、標準偏差が平均の10%を超える場合や、平均からの最大偏差や最小偏差が少なくとも50%に達した場合に警告を出力してくれます。
OSは、1つのプロセスや1つのスレッドを任意のCPUコアに任意のタイミングでスケジューリングできます。プロセスやスレッドが別のCPUコアに移動するたびに、ベンチマークの安定性に影響が生じます。これは、移動先の新しいコアのL1キャッシュデータが元のコアのL1キャッシュと同じでないためです。プロセスやスレッドが元のコアに戻るまでの間に、元のコアのキャッシュは他の作業で汚されてしまいます。したがって、次の点が重要です。
これを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
のドキュメントをご覧ください。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にも報告されています。
現代のCPUのクロックスピード(周波数)は一定ではありません。必要に応じて最大パフォーマンスを発揮し、作業がほとんどない状態では電力を節約するために、CPUのクロックスピード周波数は頻繁に変更されます。このため、負荷および負荷のかかるタイミングに応じてシステムパフォーマンスが大きく変動することがあります。さらにCPU温度も大きく影響します。現代のCPUにはさまざまなオーバーヒート防止メカニズムが搭載されており、CPUが過熱すると引き下げます。
クロックスピードのスケーリングは、次のテクノロジーによって支配的な影響を受けています。
これらについては、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
値を変更すると何が変わるかについての解説があります。
ベンチマークを手元のノートパソコンで動かすと他のアプリと干渉する可能性があるため、ベンチマークの安定性という面でよくありません。アプリをすべて閉じたとしても、ノートパソコンを動かすための必須プログラムはベンチマークでは不要であるため、やはり不十分です。上述のカーネルレベルのチューニングやハードウェアレベルのチューニングは、ノートパソコンではうまく実行できないこともあります。たとえば、私のMacではLinuxが動きませんし、安定したベンチマークのためにLinuxを何とかして動かそうという気にもなれません。
もうひとつの問題は、今使っているノートパソコンの環境を完全には再現できないことです。ベンチマークを再現したいユーザーにとって、私のノートパソコンと完全に同一のハードウェアとソフトウェアを揃えるのは簡単なことではありません。
ノートパソコンが便利なのは、開発-テストを短いサイクルで回す限りにおいてです。ベンチマーク専用のハードウェアを購入・セットアップするとなるとコストも時間もかかります。かといってクラウドサーバーや仮想マシンでは安定したパフォーマンスが保証されないため、いずれもベンチマークには不向きです。同一のホストシステムで他のVMが動けばCPUパワーを持って行かれてしまいますし、ハイパーバイザーのコンテキストスイッチも発生します。クラウド環境の提供するCPU性能バースト機能(他のVMがアイドル時に自分のCPU割り当てを増やす)は、本番環境では素晴らしい機能ですがベンチマークの安定性にはよくありません。クラウドVMは仮想化されているため、ハードウェアレベルのチューニングの多くはクラウドサーバーには使えません。
ではどうすればよいのでしょうか。十分実現可能で使いやすく、コスト的にも見合う選択肢として、以下のサービスを見繕ってみました。
同社のコントロールパネルに問題があったために使う機会がこれまでなく、専用ハードウェアにフルアクセス可能か仮想化レイヤどまりなのかについては今のところ判断材料がありません。
Stinnerのベンチマーク方法論は統計的手法をさほど採り入れておらず、もっぱらシステムチューニングに特化しています。Stinnerはベンチマーク結果の標準偏差をチェックし、偏差が必要以上に大きい場合には結果を破棄して、ベンチマークの安定化に必要なチューニング方法を模索します。Stinnerの方法の大半は、OSレベルやハードウェアレベルに着目しています。ASLRやランダムシードの影響を避けるためにベンチマークをマルチプロセスで実行し、カーネルのCPU設定やCPUのクロックスピードをチューニングします。
Stinnerの方法論の多くは、perfツールに込められています。Stinnerは、自身の方法論が結果の安定化と信頼性向上に非常に役立ったと述べています。その成果は新しいPythonベンチマークスイートや、ベンチマーク結果を長期間トラックできるCIシステムに結実しています。
私自身はまだStinnerの方法論を実践していませんが、この次のベンチマークはきっとこの方法論で進め、その成果をレポートしたいと思います。
ベンチマークに興味が湧いてきましたら、この記事がお役に立ったかどうか、共有したくなる洞察を得られたかどうか、ぜひ私までお知らせください。皆さまのご多幸とハッピーベンチマークを願っております。
nohz_full=godmode
について — Jeremy Eder’s blog