迭代 120-优化性能 1

背景

development 分支由 master 去掉元编程等技术后重构而来,但整体性能下降约 20%。本迭代基于 profile 日志对比,定位主要回退点并给出可落地的优化建议(先写建议,后续再逐个落地实现并复测)。

对比结论(整体)

  • master 总耗时: 553.1201s(logs/performance_profile_master_20260116_214606.log)

  • development 总耗时: 663.5783s(logs/performance_profile_development_20260116_214820.log)

  • 回退幅度: +110.4582s(约 +19.97%)

主要回退点(按影响排序)

1) runonce 路径整体变慢:Cerebro._runonce 明显回退

  • 表现:

    • master: cerebro.py:_runonce 累计 228.14s

    • development: cerebro.py:_runonce 累计 302.84s

    • 差值: +74.70s

  • 关键关联热点:

    • development 中 lineiterator.py:_once 累计 50.43s(master 中同类逻辑累计约 11.85s)

    • development 中若干指标的 once() 实现耗时显著(尤其是布林带)

  • 初步原因判断:

    • LineIterator._once 的实现与 master 相比引入了更多 Python 层逻辑(循环、hasattr/getattr 分支、重复 oncebinding 传播等),导致 runonce 阶段子调用开销放大。

2) broker 通知链路变慢:Cerebro._brokernotify / BackBroker.next 回退

  • 表现:

    • master: cerebro.py:_brokernotify 累计 24.05s

    • development: cerebro.py:_brokernotify 累计 57.44s

    • 差值: +33.40s

  • 关键关联热点:

    • master: bbroker.py:next 累计 21.39s

    • development: bbroker.py:next 累计 53.24s

    • development: bbroker.py:_get_value 自身耗时 8.10s(master 同类逻辑约 3.43s)

  • 初步原因判断:

    • BackBroker 在热点路径里多次 get_param(),以及 _get_value() 循环内重复取参。

    • development 中引入的 Python 层 __getattribute__ 覆盖(见下一条)会放大 broker 上的所有属性访问成本。

3) __getattribute__ 覆盖带来大量 Python 层属性访问开销(高频、可直接优化)

  • BackBroker.getattribute:

    • development: bbroker.py:__getattribute__ 自身耗时 11.21s(36.92M calls)

    • master: 无此热点(未覆盖 __getattribute__

  • CommInfoBase.getattribute:

    • development: comminfo.py:__getattribute__ 自身耗时 5.80s(11.73M calls)

    • master: 无此热点(未覆盖 __getattribute__

  • 初步原因判断:

    • 覆盖 __getattribute__ 会让对象的“所有属性访问”从 C-level 变成 Python-level 调用,哪怕只针对一个字段做特殊处理,也会把整类对象的属性读取开销整体抬升。

4) 内置函数调用量显著上升(属于“症状”,多由上面几条触发)

  • isinstance:

    • master: 45.38M calls / 7.49s

    • development: 104.29M calls / 13.78s

  • hasattr:

    • master: 4.46M calls / 0.94s

    • development: 38.47M calls / 8.32s

  • math.isnan:

    • development: 53.10M calls / 4.75s(master 未出现为热点)

  • 初步原因判断:

    • 大量 hasattr/getattr/isinstance/math.isnan 通常来自“为了兼容/防御式编程”在热循环里做了过多检查;需要把这些检查挪到初始化阶段缓存结果,或用更轻量的判定方式。

改进建议(可落地、按优先级)

建议 A(最高优先级):移除/替换 __getattribute__ 覆盖,只对目标字段做“局部拦截”

  • 目标: 直接吃掉 development 中 10s+ 级别的纯开销,并减少连带的 getattr/hasattr/isinstance 放大效应。

  • 落地方向:

    • BackBroker.cash:

      • 不使用 BackBroker.__getattribute__

      • 改为“仅对 cash 做拦截”的 descriptor/property(例如自定义 cash descriptor:__get__ 优先返回 _cash__set__ 同步更新 _cash 与参数系统)。

    • CommInfoBase.stocklike:

      • 不使用 CommInfoBase.__getattribute__

      • 同样用 property/descriptor,或在 _post_init_setup() 后把最终 _stocklike 回写进参数系统,保证 self.stocklike 直接命中 param_manager 的快路径。

建议 B(高优先级):对齐/精简 LineIterator._once(runonce 热点),避免重复 oncebinding/多余分支

  • 目标: 缩小 LineIterator._once 带来的子调用累积开销(当前 development: 50.43s vs master: ~11.85s)。

  • 落地方向:

    • 尽量恢复 master 的结构:

      • 一次性 forward(避免在 _once 内做过多动态 end 计算)

      • 指标一次性 _once()

      • home/reset 的顺序与 master 保持一致

      • oncebinding 传播尽量“在必要的层级做一次”(避免每个 indicator 再嵌套多轮 hasattr/遍历)

    • 避免在热路径里 hasattr(lines_obj, '__iter__') / hasattr(line, 'oncebinding') 这种重复检查:

      • 可以在对象构建阶段缓存“是否需要 oncebinding”

      • 或直接按约定类型调用,让异常暴露(EAFP)并在外层处理

建议 C(高优先级):broker 热循环内减少 get_param()/字典查找次数(局部缓存)

  • 目标: 降低 BackBroker.next/_get_value 中的参数系统访问成本。

  • 落地方向:

    • _get_value() 开头缓存:

      • shortcash = self.get_param('shortcash')

      • positions = self.positions

      • getcommissioninfo = self.getcommissioninfo

    • next() 开头缓存:

      • checksubmit = self.get_param('checksubmit')

    • 复用局部变量,减少重复属性查找(Python 解释器层面收益很可观)。

建议 D(中优先级):减少 math.isnan/isinstance 在热路径里的调用

  • 目标: 直接降低 math.isnan 53M 次调用,以及 isinstance 的额外增量。

  • 落地方向:

    • 对数值序列,优先用 x != x 判断 NaN(NaN 的特性)替代 isinstance(x, float) and math.isnan(x)

    • 将“是否 datetime line / indicator line”的判断在初始化时缓存(目前部分模块已有缓存,但仍有大量零散判断)。

建议 E(中优先级):优化布林带等指标的 once() 算法复杂度

  • 表现: development 中 indicators/bollinger.py:once 自身耗时 12.98s(12 calls)。

  • 落地方向:

    • once() 内的双重循环(N*period)改为滑动窗口:维护 rolling sumsum_sq,每步 O(1) 更新。

    • 或回退到 master 的“组合指标实现(SMA + StdDev)”,但需确认与当前去元编程后的绑定/传播机制兼容。

建议 F(可选/评估项):参数系统的 history/callback 默认关闭

  • 目标: 降低参数系统在生产 backtest 中的额外能力开销。

  • 落地方向:

    • 允许在 ParameterizedBase/ParameterManager 初始化时传入 enable_history=False, enable_callbacks=False(默认关闭,测试或 debug 时再开启)。

验证方案(你后续修改代码后执行)

  • 安装验证:

    • pip install -U .

  • 格式化/质量:

    • bash scripts/optimize_code.sh

  • 单测:

    • 运行现有测试用例(pytest / run_tests.sh),确保全部通过

  • 性能复测:

    • python scripts/profile_performance.py --processes 12

    • 生成新的 profile log 后,与以下基准对比:

      • master: logs/performance_profile_master_20260116_214606.log

      • 当前 development: logs/performance_profile_development_20260116_214820.log