跳转至

架构决策:求值引擎

背景

最初的实现用「递归闭包模式」来组合谓词逻辑。这种写法语法优雅、实现简单,但在规模化的profiling下,暴露出 Python 执行模型里几处结构性的缺陷。

本文记录迁移到迭代式 AST 引擎的理由。

Profiling:闭包陷阱

对深规则链(depth > 100)做 profiling,暴露了闭包方案的三个问题。

1. 栈帧开销

Python 解释器为每次函数调用都付出可观的开销(PUSH_FRAME / POP_FRAME)。

CPU profiling 显示,执行时间被栈管理主导,而非真正的逻辑求值。深度 2000 的逻辑链会建出深度 2000 的调用栈,立刻撞上 sys.getrecursionlimit() 的墙,并因 CPU 缓存未命中而非线性地劣化性能。

绝大部分时间都耗在等待闭包返回上。

展开 / 折叠:CPU 剖析

Codspeed Prof

下面是一座摩天楼

展开 / 折叠:CPU 剖析

CPU Flamegraph

谓词嵌套超出栈深度限制时也会触发 RecursionError,但这在生产环境中极不可能发生。

2. 内存碎片与 GC 不稳定

闭包是不透明的对象,持有对其执行上下文(cell)的引用。

基准测试显示,高深度下执行时间的标准差(StdDev)极大。

img.png

构建深链会因大量临时可调用对象而触发激进的 GC 周期,导致不可预测的延迟尖峰(P95 离群值)。

打开内存火焰图(HTML)

3. 零自省能力(「黑盒」)

编译后的闭包对运行时是不透明的。

一个组合好的谓词只是 <function <lambda> at 0x...>。当一条规则求值为 False 时,如果不借助侵入式的日志,既无法追踪是哪个节点失败,也无法检视中间状态。


方案:AST 模式

我们用一个显式的迭代式 AST 引擎替换了递归执行模型。

设计原则

  1. 规则定义为数据结构(节点),而非编译后的函数。
  2. 用单个循环处理整棵树。
  3. 规则结构是不可变的,执行状态是临时的。

收益

特性 闭包(旧) AST 引擎(现)
控制流 递归调用栈 while 循环 / 栈机
空间复杂度 栈帧 栈帧(仅堆上)
稳定性 高抖动(GC 颠簸) 确定 / 线性
可调试性 无(不透明) 完全可自省(可访问节点)

结论

转向基于 AST 的引擎,让我们能以线性的性能特征常数的栈占用支撑任意复杂的规则链,同时解锁了闭包模型下不可能实现的序列化、单步调试等能力。