迭代 123 - 性能优化实施方案(第四轮)

背景

迭代 119/120/121 的性能分析显示,整体耗时热点主要集中在:

  • Line 系统基础操作:linebuffer.forward/__getitem__/__setitem__lineseries.__setattr__

  • 日期转换:backtrader/utils/dateintern.py:num2datelinebuffer.py:datetime/date/time

迭代 122 已对 PandasData._load 做了 Numpy 后端优化,显著削减了 pandas indexer 热路径。

本轮(迭代 123)目标是在不修改测试用例、不改变行为的前提下,继续压缩“每根 bar 必经”的 Python 热循环开销,重点针对 datetime 转换链路LineBuffer 高频访问路径

资源与限制

  • 资源:python scripts/profile_performance.py(使用默认参数,默认 --processes 7)生成日志到 logs/

  • 基准:logs/performance_profile_master_*.log

  • 限制:

    • 不允许修改测试用例

    • 修改后需 pip install -U .

    • 需通过 bash scripts/optimize_code.sh 与完整测试

对比基准(最新)

  • master 基准:logs/performance_profile_master_20260117_093200.log563.25s

  • development 当前:logs/performance_profile_development_20260117_105735.log398.67s

  • 对比报告:logs/performance_compare_master_093200_vs_dev_105735.md

当前热点概览(development 105735)

以下为当前日志中仍然占用较高的热点(示例,按 tottime/cumtime 相关性整理):

  • backtrader/linebuffer.py:forward(高频扩容/推进)

  • backtrader/lineseries.py:__setattr__(高频赋值/挂接对象)

  • backtrader/linebuffer.py:__getitem__(高频取值)

  • backtrader/linebuffer.py:datetime + backtrader/utils/dateintern.py:num2date(高频 float->datetime 转换)

其中 num2date 的调用次数与耗时在当前分支仍然非常可观,是“策略/分析器/定时器”链路中容易被重复触发的纯 Python 计算。

问题定位:datetime 转换存在重复计算

现状:

  • Line 系统内部存储的是 float 格式的 datetime 数值(date2num 输出),这是合理且高效的存储形式

  • 但当策略/分析器调用 data.datetime.datetime()/date()/time() 时,会触发:

    • LineBuffer.datetime() -> num2date(float) -> 生成 datetime.datetime

  • 在同一根 bar 内,往往会被多次调用(例如多个 analyzer / observer / timer / 自定义逻辑重复访问),导致 同一 idx 的相同值被重复转换

因此本轮主线优化思路是:

  • 保持底层存储为 float 不变

  • 通过 “按 idx 缓存”float -> datetime 的重复计算压缩为“每根 bar 最多一次”

优化方案

Step 1:为 LineBuffer.datetime() 增加“按 idx 缓存”的快速路径(低风险,高收益)

目标:对最常见的调用形态提供 cache 命中:

  • ago == 0

  • tz is None(或等价于 self._tz

  • naive == True

实现要点:

  • 仅对 datetime line 启用缓存(或对所有 line 启用,但仅在 datetime() 方法内生效)

  • 缓存字段示例:

    • _dt_cache_idx:上次缓存命中的 self._idx

    • _dt_cache_val:上次缓存命中的 float 值(可选,用于防止 idx 未变但值被修改的极端情况)

    • _dt_cache_dt:上次缓存的 datetime.datetime

  • LineBuffer.datetime()

    • 先读取 value = self[ago]

    • 再判断是否满足 cache 快速路径条件,满足则尝试返回缓存

    • 未命中则调用 num2date,并写入缓存

验收点:

  • dateintern.py:num2datencalls/tottime 明显下降

  • linebuffer.py:datetimetottime 明显下降

  • 行为保持一致(尤其是 out-of-range 时抛 IndexError 的语义保持)

Step 2:让 LineBuffer.date()/time() 复用缓存,避免二次转换(中风险,中收益)

现状:

  • LineBuffer.date() 当前是 self.datetime(...).date()

  • LineBuffer.time() 当前是 self.datetime(...).time()

改进方向:

  • 对默认参数场景(ago==0, tz is None, naive True),直接复用 datetime() 的缓存结果

  • 可选:增加独立的 _date_cache_idx/_date_cache_val/_date_cache_date_time_cache_*,避免 datetime().date() 的对象创建开销

验收点:

  • linebuffer.py:date/time(若在 profile 中可见)调用成本下降

  • num2date 的调用次数进一步下降

Step 3(可选):进一步降低 LineBuffer.__getitem__ 的 Python 分支/属性访问开销(高风险,需谨慎)

观察:

  • __getitem__ 是极高频路径,当前实现中存在多次 getattr/hasattr、分支判断与异常路径

建议策略:

  • 只做“确定安全”的微优化:

    • getattr(self, "_is_data_feed_line", False) 改为 self.__dict__.get("_is_data_feed_line", False)(与 forward() 统一)

    • 对 datetime line 的特殊判断尽量复用已缓存的 _is_datetime_line(若存在)

  • 不改变 out-of-range 的抛错语义

该步骤建议在 Step1/2 确认收益后再考虑,避免引入边界行为回归。

验收标准(Definition of Done)

  • 安装:pip install -U . 成功

  • 格式化/质量:bash scripts/optimize_code.sh 通过

  • 测试:pytest tests -n 8 全通过(不允许改测试)

  • 性能复测(默认参数):

    • 运行 python scripts/profile_performance.py 生成新日志

    • 与优化前的 development 日志对比:Total Execution Time 不得回退(建议目标:至少 -2%)

    • dateintern.py:num2date / linebuffer.py:datetimencalls/tottime 明显下降

回滚方案

若出现以下情况之一:

  • 单测失败/行为变化(尤其是 datetime 边界、时区、IndexError 语义)

  • 性能收益不稳定或回退

回滚策略:

  • 保留 Step 1/2 的代码结构,但增加“快速路径开关”(例如模块级常量或环境变量)

  • 关闭缓存后,LineBuffer.datetime/date/time 回退到原始实时转换逻辑