σμ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—连续天数—

先测量,再优化

一个跑四个小时的回测,或一个每秒被调用一百万次的定价函数,会让每个人都想提速,而几乎所有人都猜错时间花在哪里。看起来很重的循环可能根本不算什么,看似无害的一行可能就是整个运行时间。性能分析(profiling)就是在改动任何东西之前,测量程序把时间花在哪里。规则很简单:没测量过的行不要优化,改动之后再测一次,看看是否真的有帮助。

# 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

噪声、重复与最小值

单次计时不可靠:操作系统、其他程序、CPU 缓存以及 Python 自身的启动都会带来噪声,而且它们只会增加时间。所以要把函数计时多次并取最小值,也就是受干扰最小的那次运行;timeit 做的正是这件事。使用 time.perf_counter()(高分辨率单调时钟),不要用 time.time()。用真实规模的输入来测量,因为十行时很快的函数在一千万行时可能很慢;并先预热,避免一次性开销(导入、缓存)被算到被测代码头上。

# 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 会把最耗时的排在前面。每个函数的占比就是它的耗时除以总耗时。修复一个函数的收益受阿姆达尔定律限制:把运行时间中占比 f 的部分加速 s 倍,总体加速为 1 / ((1 - f) + f / s)。把 60% 的运行时间加速十倍,总体只快 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 给一次调用计时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 继续