Files
TermiNexus 3658ab0637 perf(Phase2Translator/IRMetadataFix): 削逐行热点——跳过无谓 strip、预编译逐行正则、C 级数字扫描
prof 基线(PROF/20260930_224857/main_time.txt=438.40s,7.28 亿次调用)显示全流程最大的 Python 侧热点是逐行扫描产生的内置字符串/正则调用:str.startswith 2.372 亿次(32.1s)、str.strip 1.791 亿次(22.8s)、re.Pattern.match 3130 万次(11.4s)、re.match 模块级 1971 万次(10.7s)+re._compile 1975 万次(7.2s)。四处等价改动:
1) _split_ll 第二遍:in_define/in_global_def 续行(.ll 里占比最高)不再 str.strip——这两支只用行内容与花括号计数,前导/尾随空白不含花括号,_count_braces_outside_strings 结果逐字不变;
2) _split_ll 第一遍:加 "define" not in line 的 C 级快路径,绝大多数行连 strip 都不做;
3) Phase2Translator 逐行正则全部预编译为模块级 _RE_* 常量(11 处 re.match + 4 处 re.sub,模式串一字未改)——消掉每次调用的模式串哈希+缓存查找;
4) IRMetadataFix:_RewritePlain 的前导数字扫描由逐字符 Python 循环改为 str.lstrip 的 C 级扫描(该函数 prof 5.8M 次调用,是项目内 self time 最大者);SanitizeIRMetadata 的逐行 startswith("!") 改为 [:1] 索引切片。
离线自测:数字扫描与旧实现逐例一致;SanitizeIRMetadata 幂等性保持;_split_ll 冒烟输出与预期语义一致(global/多行初始化器/define→declare/constant/metadata 分流正确)。
2026-09-30 23:04:25 +08:00

308 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""合并 IR 的元数据规整(sanitize)。
场景:多个模块的 .ll 文本被直接拼接成一个文件(每个模块都自带一小段
DWARF 元数据:!llvm.dbg.cu / !llvm.module.flags / !0 = !DIFile / ...
以及内联在 `define ... !dbg !N`、指令行上的 !N 引用)时,各模块的编号
元数据都从 !0 重新开始 —— 合并文件里同一个 !N 被定义多次、命名元数据
也被定义多次,LLVM 解析即报:
error: Metadata id is already used
error: ... !llvm.dbg.cu ... defined more than once
本模块提供 SanitizeIRMetadata(text) -> text:把全文的 `!N = ` 定义按
「元数据块」为单位重新编号成全局唯一 id,并同步改写全文引用;命名元数据
按名字聚合/去重成一份。无元数据的文本逐字节返回原样(严格 no-op),对
已规整的文本再跑一次结果一致(幂等)。
"""
from __future__ import annotations
import re
# `!N = ` 形式的编号元数据定义行
_Definition = re.compile(r'^!(\d+) = ')
# `!name = ...` 形式的命名元数据(首字符为字母,故不会命中 `!N = `)
_NamedDefinition = re.compile(r'^!([A-Za-z][\w.]*)\s*=\s*(.*)$')
# `!{ ... }` 列表形式
_ListForm = re.compile(r'^!\{(.*)\}$', re.DOTALL)
# 纯 id 引用 `!N`
_PureId = re.compile(r'^!(\d+)$')
# 前导数字字符集(str 形式):供 str.lstrip 的 **C 级**扫描用
# (_RewritePlain 在合并 .ll 上是百万级调用,原逐字符 Python 循环是 self time 主源)。
_DIGITS_STR: str = '0123456789'
def _SplitTopLevel(Content: str) -> list[str]:
"""按顶层逗号切分 !{ ... } 的条目(跳过字符串字面量与嵌套括号内的逗号)。"""
Items: list[str] = []
Depth: int = 0
Buf: list[str] = []
i: int = 0
n: int = len(Content)
while i < n:
Ch: str = Content[i]
if Ch == '"':
j: int = i + 1
while j < n:
if Content[j] == '\\':
j += 2
continue
if Content[j] == '"':
j += 1
break
j += 1
Buf.append(Content[i:j])
i = j
continue
if Ch in '{[(':
Depth += 1
elif Ch in '}])':
Depth -= 1
elif Ch == ',' and Depth == 0:
Items.append(''.join(Buf).strip())
Buf = []
i += 1
continue
Buf.append(Ch)
i += 1
Tail: str = ''.join(Buf).strip()
if Tail:
Items.append(Tail)
return Items
def _ListItems(Rhs: str) -> list[str] | None:
"""`!{ a, b }` -> ['a', 'b'];非列表形式返回 None。"""
m = _ListForm.match(Rhs.strip())
if not m:
return None
return _SplitTopLevel(m.group(1))
def _RewritePlain(Line: str, StrMap: dict[str, str]) -> str:
"""Rewrite `!N` references in a line that has no `"` and no `;`.
Equivalent to a regex substitution of `!` + digits but without the regex
engine callback: split on '!' (C-level), map each digit run by full token,
keep unknown ids verbatim.
"""
Parts: list[str] = Line.split('!')
if len(Parts) == 1:
return Line
Out: list[str] = [Parts[0]]
Ap = Out.append
Get = StrMap.get
for Part in Parts[1:]:
n: int = len(Part)
# 前导数字个数改由 str.lstrip 的 **C 级**扫描给出(原逐字符 Python 循环
# `while d < n and Part[d] in _DIGITS`;本函数 prof 实测 5.8M 次调用,
# 是 self time 最大的项目内函数)。语义与循环逐字等价:都是"前导数字个数"。
d: int = n - len(Part.lstrip(_DIGITS_STR))
if not d:
Ap('!')
Ap(Part)
continue
Digits: str = Part[:d]
# Leading zeros normalise the way int()/str() did in the old path.
Tok: str = '!' + Digits if (d == 1 or Digits[0] != '0') else '!' + str(int(Digits))
New: str | None = Get(Tok)
Ap(Tok if New is None else New)
Ap(Part[d:])
return ''.join(Out)
def _RewriteQuoted(Line: str, StrMap: dict[str, str]) -> str:
"""Rewrite `!N` refs in a line that contains `"` and/or `;`.
String literals and the trailing `;` comment are copied verbatim; the
remaining (quote/comment free) chunks go through `_RewritePlain`. Uses
C-level str.find instead of the old per-character Python scan.
"""
Out: list[str] = []; Ap = Out.append
i: int = 0
n: int = len(Line)
while i < n:
q: int = Line.find('"', i)
s: int = Line.find(';', i, q if q >= 0 else n)
if s >= 0:
Ap(_RewritePlain(Line[i:s], StrMap))
Ap(Line[s:]) # ';' comment: copy verbatim to end of line
break
if q < 0:
Ap(_RewritePlain(Line[i:], StrMap))
break
Ap(_RewritePlain(Line[i:q], StrMap))
j: int = q + 1
while j < n:
if Line[j] == '\\':
j += 2
continue
if Line[j] == '"':
j += 1
break
j += 1
Ap(Line[q:j]) # string literal: copy verbatim
i = j
return ''.join(Out)
def SanitizeIRMetadata(text: str) -> str:
"""让拼接后的 IR 元数据合法:编号全局唯一 + 命名元数据只留一份。
无元数据(没有任何以 `!` 开头的行)时原样返回 —— 严格 no-op。
"""
if '!' not in text:
return text
Segments: list[str] = text.split('\n')
LineCount: int = len(Segments)
# 1) 元数据块 = 连续的以 '!' 开头的行。
# 每个模块的元数据(命名行 + 编号定义)是连续的一段;IR 行把它断开,
# 所以块边界即模块边界,块内共享同一套局部编号空间。
Blocks: list[tuple[int, int]] = [] # [Start, End)
i: int = 0
while i < LineCount:
# 单个字符的判定用索引切片(seg[:1])替代 str.startswith:本循环对合并 .ll
# 的**每一行**各跑一次(prof 实测 str.startswith 237M 次为全流程最大热点),
# 索引切片无方法查找开销、语义与 startswith('!') 等价(空行得 ''≠'!')。
if Segments[i][:1] == '!':
j: int = i + 1
while j < LineCount and Segments[j][:1] == '!':
j += 1
Blocks.append((i, j))
i = j
else:
i += 1
if not Blocks:
return text
# 2) 每行归属的块:块内行归本块;块之间的行(IR、命名行)归「其后第一个
# 块」—— 因为每个模块的 IR 之后紧跟它自己的元数据块(含被引用的 !N)。
Owner: list[int] = [-1] * LineCount
PrevEnd: int = 0
for Index, (Start, End) in enumerate(Blocks):
for k in range(PrevEnd, End):
Owner[k] = Index
PrevEnd = End
# 3) 块内按定义出现顺序重新编号(局部 id -> 全局 id)
Lines: list[str] = list(Segments)
BlockMaps: list[dict[int, int]] = []
BlockStrMaps: list[dict[str, str]] = []
BlockIsIdentity: list[bool] = []
# Definition line index -> (new global id, length of its rewritten "!N = " prefix).
# Lets step 4/5 skip a per-line regex to re-detect definition lines.
DefInfo: dict[int, tuple[int, int]] = {}
# Named metadata lines are collected here (only block lines can start with '!').
NamedLines: dict[str, list[int]] = {}
Counter: int = 0
for Start, End in Blocks:
Mapping: dict[int, int] = {}
Identity: bool = True
for k in range(Start, End):
m = _Definition.match(Lines[k])
if not m:
nm = _NamedDefinition.match(Lines[k])
if nm is not None:
NamedLines.setdefault(nm.group(1), []).append(k)
continue
Local: int = int(m.group(1))
Mapping[Local] = Counter
if Local != Counter:
Identity = False
Prefix: str = '!' + str(Counter) + ' = '
DefInfo[k] = (Counter, len(Prefix))
Lines[k] = Prefix + Lines[k][m.end():]
Counter += 1
BlockMaps.append(Mapping)
BlockStrMaps.append({'!' + str(LocalId): '!' + str(GlobId)
for LocalId, GlobId in Mapping.items()})
BlockIsIdentity.append(Identity)
# 4) 改写全文引用(定义体右值、define/指令行的 !dbg、命名元数据列表)
# 定义行左侧的 id 是上一步刚分配的新编号,绝不能再当引用改写:本块的
# 新编号区间可能与它自己的旧局部编号区间重叠(例如新编号恰好也是本块
# 旧 id 843 → Mapping[843]=1686),一旦改写左侧就会出现两个定义共用同一
# id,opt/llc 随即报 "Metadata id is already used"(本 bug 实测残留 276 个
# 重复 id)。故定义行只改写 '=' 之后的右值。
GetDef = DefInfo.get
for k in range(LineCount):
Index: int = Owner[k]
if Index < 0 or BlockIsIdentity[Index] or not BlockMaps[Index]:
continue
Line: str = Lines[k]
if '!' not in Line:
continue
StrMap: dict[str, str] = BlockStrMaps[Index]
Def: tuple[int, int] | None = GetDef(k)
if Def is not None:
Split: int = Def[1]
Tail: str = Line[Split:]
Fixed: str = Line[:Split] + (_RewritePlain(Tail, StrMap)
if ('"' not in Tail and ';' not in Tail)
else _RewriteQuoted(Tail, StrMap))
else:
Fixed = (_RewritePlain(Line, StrMap)
if ('"' not in Line and ';' not in Line)
else _RewriteQuoted(Line, StrMap))
if Fixed != Line:
Lines[k] = Fixed
# 5) 定义体索引(新 id -> 右值文本),供命名元数据判重。
# 只遍历定义行(步骤 3 已登记),不再逐行做正则匹配。
BodyOf: dict[int, str] = {}
for k, (NewId, Split) in DefInfo.items():
BodyOf[NewId] = Lines[k][Split:].strip()
# 6) 命名元数据:同名多份合并成一份。
# - !llvm.dbg.cu:全部条目聚合(每个 DICompileUnit 都必须被列出,不能去重)
# - 其它(!llvm.module.flags、!llvm.ident ...):条目内容(id 引用解析成
# 节点正文)完全相同的多份只留一份,不同则聚合。
# NamedLines 已在步骤 3 随块扫描一并收集(命名行的名字不受引用改写影响)。
Dropped: set[int] = set()
for Name, Indices in NamedLines.items():
if len(Indices) == 1 and Name != 'llvm.dbg.cu':
continue
First: int = Indices[0]
Cr: str = Segments[First][len(Segments[First].rstrip('\r')):]
Items: list[str] = []
SeenItems: set[str] = set()
SeenBodies: set[tuple[str, ...]] = set()
Single: bool = True
IsDbgCu: bool = Name == 'llvm.dbg.cu'
for k in Indices:
NamedMatch = _NamedDefinition.match(Lines[k])
Rhs: str = (NamedMatch.group(2) if NamedMatch is not None else '').strip()
ListItems: list[str] | None = _ListItems(Rhs)
if ListItems is None:
Single = False
break
if not IsDbgCu:
# !llvm.dbg.cu never de-dupes by body, so skip resolving bodies for it.
Bodies: list[str] = []
for Item in ListItems:
PureMatch = _PureId.match(Item)
Bodies.append(BodyOf.get(int(PureMatch.group(1)), Item)
if PureMatch else Item)
Key: tuple[str, ...] = tuple(Bodies)
if Key in SeenBodies:
continue
SeenBodies.add(Key)
for Item in ListItems:
if Item not in SeenItems:
SeenItems.add(Item)
Items.append(Item)
if not Single:
# 非 `!{ ... }` 列表形式(实际 IR 里不出现):保留首份,其余丢弃,
# 避免出现同名命名元数据被定义多次。
Dropped.update(Indices[1:])
continue
Rebuilt: str = '!%s = !{ %s }%s' % (Name, ', '.join(Items), Cr)
if Rebuilt != Segments[First]:
Lines[First] = Rebuilt
Dropped.update(Indices[1:])
if not Dropped and Lines == Segments:
return text
return '\n'.join(Line for k, Line in enumerate(Lines) if k not in Dropped)