Python 3.14 JIT对单态属性访问的优化本质是绕过PyObject_GetAttr,直接生成类似C结构体成员访问的机器指令;其依赖类型守卫与布局守卫确保安全性,仅在纯__slots__类、无子类覆盖且无自定义属性协议时稳定生效。

Python 3.14 的 JIT 编译器对“单态”属性访问的优化,本质是绕过整个 Python 动态查找机制,直译为接近 C 结构体成员访问的机器指令。这不是“加速解释”,而是彻底不走解释路径。
单态属性访问为何能跳过 PyObject_GetAttr
当 JIT 观测到某函数连续多次以相同类型对象(如全是 Point 实例)访问同一属性(如 .x),它就认定该访问是单态的。此时 JIT 不再生成调用 PyObject_GetAttr 的字节码,而是直接在编译阶段固化三件事:
- 对象地址作为基址寄存器(如 x86-64 下的
rdi) - 属性在内存中的固定偏移量(如
24字节,由Point_Type.tp_basicsize和字段布局决定) - 目标数据类型对应的加载指令(如
movsd xmm0, [rdi + 24]加载 double)
最终生成的汇编和 ((Point*)obj)->x 几乎等价——没有哈希查找、没有字典遍历、没有类型分支判断。
cmp+jne 类型守卫与布局守卫的实际作用
JIT 并不盲目信任单态假设,而是在生成的机器码入口插入两条轻量级守卫指令:
-
cmp比较obj->ob_type是否等于已观测的&Point_Type;不等则jne跳转回解释器路径(deoptimization) - 额外检查
obj->ob_type->tp_dictoffset == 0且Point_Type.tp_basicsize == 40,确保没被子类或__slots__破坏内存布局
这两条指令加起来不到 10 个 CPU 周期。只要守卫持续通过,JIT 就不会重新编译;一旦失败(比如传入一个 SubPoint 实例),当前机器码立即失效,下次调用降级为多态处理。
为什么 __slots__ 和继承会破坏单态性
单态性的维持高度依赖实例内存结构的确定性:
- 普通类实例带
__dict__,属性查找必须走哈希表,偏移不可知 -
__slots__可以消除__dict__,但若子类又加了新 slot,tp_basicsize或tp_dictoffset就会变,触发布局守卫失败 - 即使子类没新增字段,只要重写了
__getattribute__或定义了描述符,JIT 就无法安全跳过动态查找链
所以,真正稳定的单态访问只出现在:纯 __slots__ 类 + 无子类覆盖 + 无自定义属性协议的组合里。任何一处松动,都会让 JIT 回退到更保守的多态路径。
最容易被忽略的是布局守卫的隐式依赖——它不只看类型对象指针是否一致,还硬编码了该类型的内存尺寸和字段排布。这意味着哪怕你只改了一个 __slots__ 元组的顺序,JIT 也可能在运行时突然降级,而错误日志里只显示 “layout guard failed”,不会告诉你具体哪块偏移变了。

















