perf(vast): AST 节点类加 __slots__,去 __dict__ 提速降内存

AST 基类 __slots__=(),131 个动态节点类按 fields+attrs 生成 __slots__,
_Init 改 object.__setattr__;类默认值改为 _node_defaults 字典(原类属性
与 __slots__ 冲突)。解析提速 ~9%,实例内存 Name 由 48B+独立 dict 降为
80B 平铺,无 __dict__ 残留。Python 3.12 pickle 对 __slots__ 类开箱即用
(__reduce_ex__(2)),131 类 pickle roundtrip 全过,vast 差分 51/53 0 FAIL。
This commit is contained in:
TermiNexus committed 2026-08-17 23:27:43 +08:00
1 parent 50c0703afd
commit 9605af8ea1
2 files changed
+15 -8

No files matched your search

+13 -6
View File
@@ -11,7 +11,7 @@ AST 树,从而使编译器侧 ``import vast as ast`` 后原有代码零修改
构造语义与 CPython 一致:
- 位置参数按 ``_fields`` 顺序赋值,超出字段数抛 TypeError
- 关键字参数全部 setattr(包括未知字段,与 CPython 行为一致)
- 关键字参数按名赋值(实例为 __slots__ 紧凑布局,仅接受已声明字段)
- 未传入的字段不设置属性(访问时 AttributeError,与 CPython 一致)
"""
from __future__ import annotations
@@ -20,6 +20,10 @@ from __future__ import annotations
class AST:
"""所有 AST 节点的基类(与 ast.AST 同构)。"""
# 空 __slots__:保证子类定义 __slots__ 后实例不携带 __dict__
# (基类若有 __dict__,子类 __slots__ 将失效)
__slots__ = ()
_fields: tuple[str, ...] = ()
_attributes: tuple[str, ...] = ()
@@ -310,22 +314,25 @@ def _MakeNodeClass(name: str, fields: tuple[str, ...], attrs: tuple[str, ...]) -
f'{name} constructor takes at most {len(fields)} positional arguments'
)
for fname, val in zip(fields, args):
self.__dict__[fname] = val
object.__setattr__(self, fname, val)
for k, v in kwargs.items():
self.__dict__[k] = v
object.__setattr__(self, k, v)
base_name: str | None = _BASE_CLASS.get(name)
base_cls: type = globals()[base_name] if base_name else AST
# __slots__ = 位置字段 + 位置属性(实例的全部属性名)
slots: tuple[str, ...] = tuple(dict.fromkeys(fields + attrs))
ns: dict[str, object] = {
'__init__': _Init,
'__slots__': slots,
'_fields': fields,
'_attributes': attrs,
# 字段默认值不能直接作为类属性(与同名 __slots__ 冲突),
# 存入独立类属性供 dump 判断"值为 None 且默认值为 None"时跳过
'_node_defaults': dict(_CLASS_DEFAULTS.get(name, {})),
'__module__': __name__,
'__doc__': _DOCS.get(name, f'{name} AST 节点(与 ast.{name} 同构)。'),
}
# 类默认值(None 字段在 dump 时跳过)
for fname, fdefault in _CLASS_DEFAULTS.get(name, {}).items():
ns[fname] = fdefault
cls: type = type(name, (base_cls,), ns)
return cls
+2 -2
View File
@@ -81,7 +81,7 @@ def dump(node: AST, annotate_fields: bool = True, include_attributes: bool = Fal
except AttributeError:
keywords = True
continue
if value is None and getattr(cls, name, ...) is None:
if value is None and getattr(cls, '_node_defaults', {}).get(name, ...) is None:
keywords = True
continue
value, simple = _format(value, level)
@@ -96,7 +96,7 @@ def dump(node: AST, annotate_fields: bool = True, include_attributes: bool = Fal
value = getattr(node, name)
except AttributeError:
continue
if value is None and getattr(cls, name, ...) is None:
if value is None and getattr(cls, '_node_defaults', {}).get(name, ...) is None:
continue
value, simple = _format(value, level)
allsimple = allsimple and simple