11 Step 5: ベンチマークと最適化
このステップは高度な最適化ではない。規律ある計測と、効果の高いいくつかの改善である。
11.1 目的
ベンチマークを使ってコードが実際に速くなったかを判断し、Julia のパフォーマンスに関するいくつかの一般的なアイデアを学ぶ。
11.2 到達目標
このステップの終わりまでに、以下ができていること:
BenchmarkToolsを使った動作するベンチマークがある- コンパイル時間と実行時間の明確な区別ができる
- スピン更新1回あたりの大まかな時間感覚がある
- 計測に基づいて正当化されたいくつかの改善がある
11.3 実行スペル
Julia で書いた 2次元イジングモデルの実装がある。
慎重にベンチマークと改善をしたい。
まず以下を説明せよ:
- なぜ @time は信頼できるベンチマークには不十分か
- BenchmarkTools と @btime が何をするか
- なぜグローバル変数がパフォーマンスを損なうか
- なぜ関数と安定した型が Julia で効くことが多いか
- 不必要なアロケーションとは何か
- 1スイープのベンチマークからスピン更新1回あたりの時間をどう推定するか
その後、私のコードをステップバイステップで検査する手助けをせよ。
一度にすべてを最適化するな。
意味のある変更の前後で計測するよう求めよ。
また、スピン更新1回あたりの時間をナノ秒で推定し、
大まかな 1 GHz の直感と比較する手助けもせよ。
11.4 説明スペル
今行ったベンチマークと改善を説明せよ。
何をどう計測したか、なぜ `@time` ではなく `@btime` や `@benchmark` を
使うのか、どの変更が実行時間やアロケーションに効いたのか、
`ns / attempt` をどう解釈すべきか、手元でどう検証すればよいかを説明せよ。
11.5 習得する概念
BenchmarkTools@btime- コンパイル時間 vs 実行時間
- グローバル変数とパフォーマンス
- 型安定性
- 不必要なアロケーション
- シンプルなインプレース改善
ns / attemptでのオーダー見積もり
11.6 最小限のベンチマーク
using BenchmarkTools
spins = initialize_spins(32)
beta = 0.44
@btime metropolis_step!($spins, $beta)スピン更新1回あたりの時間を推定するには、ベンチマーク結果を保持する方が分かりやすいことが多い:
trial = @benchmark metropolis_step!($spins, $beta)
ns_per_attempt = median(trial).time / length(spins)ここで metropolis_step! は L x L 格子に対して約 L^2 回の更新試行を行うので、length(spins) で割ると1回あたりの大まかなコストが得られる。@benchmark を使うと、1つの要約値だけに頼らず、タイミングのばらつきも確認できる。
11.7 オーダー見積もりの確認
得られた数値に対して大まかな物理的感覚を持つことは有用である。
1 GHzのクロックは非常に大まかに1 ns/ サイクルに対応する。- 最適化された Julia でのスピン更新1回は、1 CPU サイクルでは終わらない。
- 最新のマシンでは、1回あたり数ナノ秒から数十ナノ秒程度が大まかに妥当な期待値である。
- それよりもはるかに遅い場合は、まずグローバル変数、不必要なアロケーション、内部ループでの回避可能なオーバーヘッドを確認せよ。
これは大まかなサニティチェックに過ぎない。正確な数値はハードウェア、乱数生成のコスト、コンパイラ最適化、コード構造に依存する。
11.8 何を確認すべきか
AI に以下のような点を検査させるとよい:
- 重要なコードがまだグローバルスコープで実行されていないか?
- 関数の引数の型は一貫しているか?
- コードが予想以上にアロケーションしていないか?
- 一時配列が不必要に作成されていないか?
- インプレース更新でコードがよりシンプルまたは高速になるか?
- ベンチマークから
ns / attemptはどうなるか? - そのオーダーは大まかに妥当か?
ノート
column major、cache locality、BLAS 行列積、allocation、GC のような発展的な話題は Step 8 に移した。ここではまず、自分のイジングコードに対して規律あるベンチマークを行うことに集中せよ。
11.9 コードリーディング確認
以下を読め:
spins = initialize_spins(32)
beta = 0.44
trial = @benchmark metropolis_step!($spins, $beta)
ns_per_attempt = median(trial).time / length(spins)以下の質問に答えよ:
spinsとbetaをベンチマークに補間($)することがなぜ重要か?ns_per_attemptを推定する際になぜlength(spins)で割るのか?- 結果が1回あたり数十ナノ秒よりはるかに大きい場合、まず何を確認すべきか?
- 各変更の前後で計測すべきなのはなぜか?AI の推測を信じるだけではだめな理由は?
11.10 追加テスト生成プロンプト
Julia でのベンチマークと基本的な最適化について、
短いコードリーディング問題を3つ作成せよ。
以下に焦点を当てよ:
- BenchmarkTools の使い方
- ns / attempt の推定方法
- なぜグローバル変数がパフォーマンスを損なうか
- 不必要なアロケーションの見つけ方
- インプレース更新やデータフローの簡略化がどう役立つか
まだ答えは出すな。
私が答えた後、回答を採点し、フォローアップ質問を1つ出せ。
11.11 スキップ条件
以下がすでにできるなら、このステップはスキップできる:
- 適切な
@btimeベンチマークをセットアップできる - スピン更新1回あたりの大まかな時間を推定できる
- グローバル変数がパフォーマンスにとってなぜ危険かを説明できる
- 簡単な Julia コードで回避可能なアロケーションの原因を少なくとも1つ特定できる