迭代 123 - 性能优化实施方案(第四轮)¶
背景¶
迭代 119/120/121 的性能分析显示,整体耗时热点主要集中在:
Line 系统基础操作:
linebuffer.forward/__getitem__/__setitem__、lineseries.__setattr__日期转换:
backtrader/utils/dateintern.py:num2date与linebuffer.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.log(563.25s)development 当前:
logs/performance_profile_development_20260117_105735.log(398.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 == 0tz 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:num2date的ncalls/tottime明显下降linebuffer.py:datetime的tottime明显下降行为保持一致(尤其是 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:datetime的ncalls/tottime明显下降
回滚方案¶
若出现以下情况之一:
单测失败/行为变化(尤其是 datetime 边界、时区、IndexError 语义)
性能收益不稳定或回退
回滚策略:
保留 Step 1/2 的代码结构,但增加“快速路径开关”(例如模块级常量或环境变量)
关闭缓存后,
LineBuffer.datetime/date/time回退到原始实时转换逻辑