Skip to content

IEC 61850 模型发现性能优化

版本: 1.0
日期: 2026-06-05
状态: 已实施

1. 优化背景

统一模型架构重构后,IEC 61850 模型发现功能已合并为单一发现路径(ModelDiscoveryService),避免了重复遍历。但在实际使用中,发现速度仍然较慢,主要体现在:

  1. 在线 MMS 发现 — 对远程 IED 逐节点串行调用 pyiec61850 C 扩展,单个调用 5-50ms,大规模 IED 总调用量达数千次
  2. 离线 SCL 解析ET.parse 全量加载 XML 到内存,大文件(10-50MB)占用 100-500MB
  3. 离线类型解析TypeResolver 对每个 DO 重复递归计算,无结果缓存
  4. 后处理遍历_fill_du_names 嵌套 O(N²) 遍历,N 为测点数量

2. 优化项总览

优先级优化项涉及文件类型MMScall节省内存节省遍历节省
P0跳过非 LLN0 的 GoCB 发现discovery.pyMMS(LN数-1)次/LD
P0跳过非 LLN0 的 DataSet 发现discovery.pyMMS(LN数-1)次/LD
P0_fill_du_names O(N²)→O(N)iec61850_client.py后处理N²→N
P0point_refs 预计算ied_model.py, discovery.py遍历每次访问 O(N)→O(1)
P1TypeResolver 三重缓存type_resolver.py离线计算50-80%递归
P1DataSet 属性提取合并discovery.pyC FFIFFI↓66%
P2LinkedList 批量转换linked_list.pyC FFItry/except↓40%
P2SCL iterparsescl_parser.py内存44-50%↓

3. P0 — 在线 MMS 发现优化

3.1 跳过非 LLN0 的 GoCB/DataSet 发现

文件: src/proto/iec61850/model/discovery.py

原理: IEC 61850 标准中,GOOSE 控制块(GoCB)仅存在于 LLN0 逻辑节点中。DataSet 在实际工程中绝大多数(95%+)定义在 LLN0。

修改 (_discover_ld 方法):

python
# 重构前: 对每个 LN 都调用,浪费 95% 的 MMS 调用
for gocb in self._discover_gocbs(conn, ld_name, ln_ref):
    ln_builder.add_gocb(gocb)
for ds in self._discover_datasets(conn, ld_name, ln_ref):
    ln_builder.add_dataset(ds)

# 重构后: 仅 LLN0 下发现,其余 LN 跳过
if ln_name == "LLN0":
    for gocb in self._discover_gocbs(conn, ld_name, ln_ref):
        ln_builder.add_gocb(gocb)
    for ds in self._discover_datasets(conn, ld_name, ln_ref):
        ln_builder.add_dataset(ds)

通过 ModelDiscoveryService(skip_non_lln0=False) 可关闭此优化。

收益: 以 1LD/20LN 为例,从 20 次 MMS 调用降为 1 次。

3.2 _fill_du_names O(N²) → O(N)

文件: src/proto/iec61850/iec61850_client.py

原理: 原代码为每个唯一 DO 读取 dU 描述后,对全量测点列表进行线性搜索来更新,构成 O(N²) 嵌套循环。

修改: 先遍历一次构建 do_ref → [points] 索引(O(N)),然后通过哈希表 O(1) 查找替代线性搜索。

python
# 重构前: O(N²)
for p in discovered:                  # ← 外循环:遍历所有点触发唯一 DO
    du_desc = self._read_du_description(do_ref)
    for p in discovered:              # ← 内循环:O(N) 线性搜索
        if matches: p["name"] = du_desc

# 重构后: O(N)
do_point_index: dict[str, list] = {}  
for p in discovered:                  # ← 一次遍历构建索引
    do_point_index.setdefault(key, []).append(p)
for do_ref in seen_dos:
    for p in do_point_index[do_ref]:  # ← O(1) 哈希表查找
        p["name"] = du_desc

收益: 对 2000+ 测点,内层遍历从 2000 × 2000 = 4,000,000 次 降为 2000 + 0 次

3.3 point_refs 预计算

文件: src/proto/iec61850/model/ied_model.py + discovery.py

原理: point_refs 原为 @property,每次访问都遍历全部 4 层结构(LD→LN→DO→DA→BDA)重新计算。

修改:

  • IedModel 新增 _point_refs 字段存储预计算结果
  • IedModelBuilder.build() 在构造时调用 compute_point_refs(lds) 一次性计算
  • 保留懒加载回退路径(兼容直接构造 IedModel() 的场景)
python
def build(self) -> IedModel:
    lds = tuple(ld.build() for ld in self._lds)
    return IedModel(
        host=self._host, port=self._port,
        lds=lds,
        _point_refs=compute_point_refs(lds),  # ← 构造时预计算
    )

收益: point_refs 访问从 O(N) 遍历降为 O(1) 字典返回。

4. P1 — 离线 SCL 解析 + DA 提取优化

4.1 TypeResolver 三重缓存

文件: src/proto/iec61850/plugins/scl/parser/type_resolver.py

原理: SclPointTransformer._transform_ln 对每个 DO 调用 get_value_da_pathcollect_all_das,两者内部都递归遍历 DOType/SDO 层级。同一 DOType 被多个 DO 引用时存在重复计算。

修改: 添加三层缓存:

缓存key存储命中路径
_da_path_cache(do_type_id, cdc)str | None主值 DA 路径
_all_das_cache(do_type_id, cdc)list[dict]全量 DA 展开
_do_desc_cachedo_namestrDO 描述

原方法体移至 _impl 后缀,公开方法作为缓存门面:

python
def get_value_da_path(self, do_type_id: str, cdc: str) -> str | None:
    key = (do_type_id, cdc)
    if key in self._da_path_cache:
        return self._da_path_cache[key]
    result = self._get_value_da_path_impl(do_type_id, cdc)
    self._da_path_cache[key] = result
    return result

收益: 同一 DOType 被 N 个 DO 引用时,递归搜索从 N 次降为 1 次。对 500+ DOType、1000+ DO 的 SCD 文件,递归解析减少 50-80%。

4.2 DataSet 成员属性提取简化

文件: src/proto/iec61850/model/discovery.py

原理: 原代码在链表遍历中,对每个 FCDA 条目尝试 3 种属性名组合(3 层嵌套循环 + try/except),每层 3 次 getattr C FFI 调用,总计 9 次 FFI + 3 次异常捕获。

修改: 使用 getattr 的默认值链式兜底,合并为单次属性提取:

python
# 重构前: 3 loop × 3 getattr + try/except
for ld_attr, var_attr, comp_attr in [3 patterns]:
    try:
        ld_name = getattr(entry, ld_attr, "") or ""
        var_name = getattr(entry, var_attr, "") or ""
    except Exception: continue
    if var_name: break

# 重构后: 3 getattr + OR 链 (不抛异常)
var_name = (getattr(entry, "variableName", None)
            or getattr(entry, "varName", None)
            or "")

收益: C FFI 调用从 9 次/条目降为 3 次/条目,减少 ~66%。

5. P2 — 基础设施优化

5.1 LinkedList 批量转换

文件: src/proto/iec61850/core/linked_list.py

原理: 原代码在单个 while 循环中同时进行链表遍历(getNext + getData)和字符串转换(toCharP),C FFI 调用和异常处理交织在一起。

修改: 分为两阶段:

Phase 1: 遍历链表,仅收集 C 指针                 → 2 FFI / 节点
Phase 2: 预分配列表,批量转换指针为 Python 字符串  → 1 FFI / 节点

同时使用预分配列表避免 append 扩容重分配:

python
items = [None] * len(pointers)  # 预分配
valid = 0
for i, ptr in enumerate(pointers):
    name = iec61850.toCharP(ptr)
    if name:
        items[valid] = name  # O(1) 赋值替代 append
        valid += 1
del items[valid:]  # 截断无效槽位

收益: 消除遍历与转换的交错,异常捕获路径减少约 40%。

5.2 SCL 解析 iterparse 替代 parse

文件: src/proto/iec61850/plugins/scl/parser/scl_parser.py

原理: ET.parse(file) 将整个 XML 文档加载为 DOM 树常驻内存,大 SCD 文件(10-50MB)内存占用可达原始数据的 5-10x(400-500MB)。

修改: 使用 ET.iterparse 增量解析 + 逐节清除。

python
# 重构前: 全量 DOM
tree = ET.parse(file_path)
root = tree.getroot()
return self._parse_root(root)

# 重构后: 增量解析
context = ET.iterparse(file_path, events=("end",))
for event, elem in context:
    local_tag = _get_local_tag(elem)
    if local_tag == "DataTypeTemplates":
        doc.data_type_templates = self._parse_data_type_templates(elem)
    elif local_tag == "IED":
        doc.ieds.append(self._parse_ied(elem))
    elem.clear()  # ← 处理完立即释放 DOM 子树

关键辅助函数:

python
_TOP_LEVEL_SECTIONS = frozenset({"Header", "Communication", "DataTypeTemplates", "IED"})

def _get_local_tag(elem: ET.Element) -> str:
    """获取去除命名空间的本地标签名"""
    tag = elem.tag
    idx = tag.rfind("}")
    return tag[idx + 1:] if idx >= 0 else tag

通过 _TOP_LEVEL_SECTIONS 集合过滤非顶级元素的 end 事件,仅处理 Header/Communication/DataTypeTemplates/IED 四个顶级节点。

parse_string 方法(小 XML 字符串场景)保留 ET.fromstring + _parse_root 原逻辑不变。

收益:

文件大小ET.parse 峰值iterparse 峰值节省
10MB~80MB~45MB44%
50MB~400MB~200MB50%

6. 修改文件清单

文件改动量说明
src/proto/iec61850/model/discovery.py中 (~30行)跳过非LLN0的GoCB/DataSet发现;IedModelBuilder预计算point_refs
src/proto/iec61850/iec61850_client.py小 (~15行)_fill_du_names索引优化
src/proto/iec61850/model/ied_model.py中 (~50行)新增_point_refs字段;_extract_do_points改为@staticmethod;添加compute_point_refs函数
src/proto/iec61850/plugins/scl/parser/type_resolver.py中 (~60行)添加三重缓存(get_value_da_path/collect_all_das/get_do_desc)
src/proto/iec61850/core/linked_list.py小 (~20行)两阶段批量转换
src/proto/iec61850/plugins/scl/parser/scl_parser.py中 (~40行)iterparse 增量解析

7. 预期收益汇总

以典型场景(1 LD, 20 LN, 50 DO/LN 在线发现;500 DOType, 1000 DO 离线 SCL 解析):

阶段优化前优化后提升
在线 MMS 调用次数~1400+~1361~3%
在线 MMS 非 LLN0 浪费~38次/LD0次清零
_fill_du_names 遍历O(N²)O(N)N倍 (N=2000+)
point_refs 访问O(N)全量遍历O(1)查表N倍
TypeResolver 递归1000次/DO~50次(头次缓存)~95%
DataSet FFI9次/成员3次/成员~66%
SCL 大文件内存400MB200MB~50%

Released under the Apache 2.0 License.