σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft Academyロードマップ
Ctrl K
AO
+1.2k 人の学習者
ログイン参加する ↗
ロードマップ
AO
+1.2k 人の学習者
ログイン
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python ロードマップ›モジュール 10 · 高性能Pythonエンジニアリング›トピック 10.4·~50 分上級

Pythonコードのプロファイリング

ロードマップ上の位置モジュール 10/11 · トピック 4/13
10.4 Pythonコードのプロファイリング
—ラボスコア— / 100合計XP—連続日数—

まず測り、それから最適化する

4時間かかるバックテストや、毎秒100万回呼ばれる評価関数があれば、誰もが速くしたくなりますが、時間がどこで使われているかはほとんどの人が読み違えます。重そうに見えるループは実はたいしたことがなく、無害に見える1行が実行時間の大半を占めていることもあります。プロファイリングとは、何かを変える前に、プログラムが時間をどこで使っているかを測ることです。ルールは単純で、測っていない行は最適化せず、変更後はもう一度測って効果があったかを確かめます。

# The simplest measurement: read a clock before and after
import time

def slow_sum(n):
    total = 0
    for i in range(n):
        total += i * i
    return total

start = time.perf_counter()
result = slow_sum(200_000)
elapsed = time.perf_counter() - start
print(result)                                        # 2666646666700000
print(elapsed > 0)                                   # True  a real duration; the exact value changes every run

ノイズ、繰り返し、最小値

1回の計測は当てになりません。OS、他のプログラム、CPU キャッシュ、Python 自体の起動がノイズを加え、しかも加えるのは時間だけです。そこで関数を何度か計測し、最も邪魔されなかった実行である最小値を取ります。timeit はまさにこれを行います。time.time() ではなく time.perf_counter()(高分解能の単調時計)を使います。入力サイズは現実的なものにしましょう。10行で速い関数が1000万行では遅いことがあります。また先にウォームアップして、一度きりのコスト(import やキャッシュ)が計測対象のコードに乗らないようにします。

# Repeat and take the minimum: the least disturbed run
import time

def work():
    return sum(i * i for i in range(50_000))

def best_of(func, repeats):
    best = None
    for _ in range(repeats):
        start = time.perf_counter()
        func()
        elapsed = time.perf_counter() - start
        if best is None or elapsed < best:
            best = elapsed
    return best

runs = [best_of(work, 1) for _ in range(5)]
print(best_of(work, 5) <= max(runs))                 # True  the minimum is never above the worst single run
print(min(runs) > 0)                                 # True

時間の行き先:プロファイラとアムダールの法則

ストップウォッチは「どれだけか」を、プロファイラは「どこか」を教えます。標準ライブラリの cProfile はすべての関数呼び出しを記録し、関数ごとに呼び出し回数、関数自身で使った時間、呼び出し先を含む累積時間を報告します。python -m cProfile -s cumtime script.py は重いものから順に表示します。各関数の割合は、その時間を合計で割るだけです。1つを直したときの効果はアムダールの法則で制限されます。実行時間の割合 f を s 倍速くすると、全体では 1 / ((1 - f) + f / s) になります。実行の60%を10倍速くしても全体では2.2倍にすぎず、半分を無限に速くしても上限は2倍です。まず最大のホットスポットから攻めましょう。

# Shares of the total and the payoff of fixing the biggest one (the timings are made-up numbers)
timings = {"load_prices": 6.0, "price_options": 3.0, "write_report": 1.0}
total = sum(timings.values())
for name, seconds in sorted(timings.items(), key=lambda item: -item[1]):
    print(f"{name:14s} {seconds / total:5.1%}")      # load_prices 60.0%, price_options 30.0%, write_report 10.0%

def amdahl_gain(fraction, speedup):
    return 1 / ((1 - fraction) + fraction / speedup)

print(round(amdahl_gain(0.6, 10), 2))                # 2.17  ten times faster on 60% of the run
print(round(amdahl_gain(0.1, 1000), 2))              # 1.11  a huge win on the smallest part barely helps
# cProfile records every call; the function that sleeps shows up as the hotspot
import cProfile
import time

def slow_part():
    time.sleep(0.02)

def fast_part():
    return sum(range(10))

def run_all():
    slow_part()
    for _ in range(100):
        fast_part()

profiler = cProfile.Profile()
profiler.runcall(run_all)
entries = {str(entry.code): entry for entry in profiler.getstats()}
slow = next(entry for name, entry in entries.items() if "slow_part" in name)
fast = next(entry for name, entry in entries.items() if "fast_part" in name)
print(fast.callcount, slow.callcount)                # 100 1   calls are counted exactly
print(slow.totaltime > fast.totaltime)               # True    one slow call outweighs 100 fast ones
意味のある条件でプロファイルするプロファイラは監視対象のプログラムを遅くし、ごく小さな関数の値を歪めます。正確な秒数よりも全体像(どの関数が支配的か)を信頼し、改善は単純なタイマーで確認しましょう。おもちゃではなく実際のデータサイズでプロファイルします。変更のたびに結果が正しいままかを確認してください。速くて間違った答えが最も高くつきます。そして十分速くなったら止めましょう。もう重要でないホットスポットを削る時間は、戦略に使わなかった時間です。
  • 何かを変える前に測る。perf_counter で計り、繰り返して最小値を取り、現実的な入力サイズを使う
  • プロファイラ(cProfile)は時間の行き先を示す。関数ごとの呼び出し回数、自身の時間、累積時間。各割合はその時間を合計で割ったもの
  • アムダールの法則:割合 f を s 倍速くすると 1 / ((1 - f) + f / s)。最大のホットスポットから直し、変更のたびに測り直す

次はホットループの最適化です。プロファイラが指したコードを速くする変更を学びます。

ラボ:採点される内容

  • time_call で1回の呼び出しを計測する25点
  • best_of で複数回の最良値を取る25点
  • hotspots で時間の行き先を見つける25点
  • amdahl_gain で効果を予測する25点
エクスプローラー · lab-120-1
▾ lab-120-1
…
bash · ブラウザ内
learner@quantcraft:~/lab-120-1$
Pythonは未読み込みPythonLn 1, Col 1Spaces: 4UTF-8

このトピックをリセットしますか?

lab-120-1 の内容はすべてスターターファイルに戻り、ターミナルも消去されます。現在の解答は失われ、元に戻せません。

リセットされるもの
  • main.py → スターターコード
  • ターミナル履歴とテスト出力
  • 未提出の変更
保持されるもの
  • ラボの最高スコア
  • XPと連続日数
  • 講義の進捗とメモ

コードを実行するにはログインしてください。

レクチャーはすべて無料で読めます。コードの実行、テスト、ラボの提出にはアカウントの作成かログインが必要です。

  • ブラウザ内IDEとbashターミナル。インストール不要
  • 即時採点、XP、ロードマップでの現在地
アカウントを作成ログイン
または
GitHubで続ける