まず測り、それから最適化する
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点