perf(compile): TPC 编译提速——并行 worker 满核 + vast 解析器热点优化
cProfile 分析(206s 全量重编译 TPV):~163s 为 ProcessPool 等待(16 核机器仅用 8 worker),vast.parse 35.9s(1231 次)为最大 Python 计算热点。 - Phase2Translator:4 处 n_workers 上限 8 → os.cpu_count()(16 核满负荷),并行翻译/llc 编译利用率翻倍 - vast/Tokenizer._PosInfo:isascii 快速路径 + 行索引增量查找(~300 万次调用,免每次 bisect+encode) - vast/Tokenizer._EmitOp:运算符按首字符分组,58 万次调用从 42 项全表 startswith 降到 2-3 项 - vast/Tokenizer._ScanName:内联 _IsIdentPart(70 万次循环免函数调用) - Projectrans/Utils:find_reachable 用 deque + 后缀索引替代 pop(0)/全表扫描;topological_sort 用反向邻接表 + heapq 替代每轮 sort+全图扫描
This commit is contained in:
1 parent
5d93692baa
commit
58ce673f01
4 files changed
+101
-44
No files matched your search
@@ -179,9 +179,10 @@ class Phase1Generator:
|
||||
|
||||
needed: set[str] = set()
|
||||
dep_graph: dict[str, set[str]] = {}
|
||||
queue: list[str] = list(imported_from_src)
|
||||
from collections import deque
|
||||
queue: deque[str] = deque(imported_from_src)
|
||||
while queue:
|
||||
mod_name: str = queue.pop(0)
|
||||
mod_name: str = queue.popleft()
|
||||
if mod_name in needed:
|
||||
continue
|
||||
if mod_name in include_file_map:
|
||||
|
||||
@@ -417,7 +417,7 @@ class Phase2Translator:
|
||||
_vlog().info(f"\n需要翻译 {len(need_translate)} 个文件")
|
||||
t_start = time.time()
|
||||
|
||||
n_workers = min(8, len(need_translate))
|
||||
n_workers = min(os.cpu_count() or 4, len(need_translate))
|
||||
|
||||
if n_workers > 1 and len(need_translate) > 1:
|
||||
_vlog().info(f" 并行翻译 (workers={n_workers})")
|
||||
@@ -1643,7 +1643,7 @@ class Phase2Translator:
|
||||
need_compile.append(ll_path)
|
||||
|
||||
if need_compile:
|
||||
n_workers = min(os.cpu_count() or 4, len(need_compile), 8)
|
||||
n_workers = min(os.cpu_count() or 4, len(need_compile))
|
||||
_vlog().info(f" 并行编译 {len(need_compile)} 个文件 (workers={n_workers})")
|
||||
errors = []
|
||||
with ThreadPoolExecutor(max_workers=n_workers) as executor:
|
||||
@@ -2167,7 +2167,7 @@ class Phase2Translator:
|
||||
shared_pickle_path = ''
|
||||
|
||||
if shared_pickle_path and os.path.exists(shared_pickle_path):
|
||||
n_workers = min(8, len(need_translate))
|
||||
n_workers = min(os.cpu_count() or 4, len(need_translate))
|
||||
errors = []
|
||||
# 波次并行翻译:按依赖关系分批提交,确保被依赖的文件先翻译完成。
|
||||
# 根因:_TryLoadStructFromStub 扫描 output 目录加载跨模块 struct 定义,
|
||||
@@ -2392,7 +2392,7 @@ class Phase2Translator:
|
||||
return (task, result)
|
||||
|
||||
if len(compile_tasks) > 1:
|
||||
n_workers = min(8, len(compile_tasks))
|
||||
n_workers = min(os.cpu_count() or 4, len(compile_tasks))
|
||||
with ThreadPoolExecutor(max_workers=n_workers) as executor:
|
||||
futures = {executor.submit(_compile_one_include, task): task for task in compile_tasks}
|
||||
done_count = 0
|
||||
|
||||
+40
-26
@@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
from collections import deque
|
||||
from typing import Deque
|
||||
import vast as ast
|
||||
|
||||
from lib.core.VLogger import get_logger as _vlog
|
||||
@@ -150,33 +152,37 @@ def find_reachable_files_from_entries(src_root: str, entry_files: list[str]) ->
|
||||
if top_module not in all_files:
|
||||
all_files[top_module] = filepath
|
||||
|
||||
# 后缀索引:pkg.sub.mod → 注册 "pkg.sub.mod" / "sub.mod" / "mod",
|
||||
# 使 from-import 的短依赖名(dep)O(1) 定位目标文件,替代原
|
||||
# for key,val in all_files.items() 全表线性扫描(O(n²),Phase1 全量
|
||||
# 重编译可达文件多时显著拖慢)。
|
||||
_suffix_index: dict[str, str] = {}
|
||||
for _key, _val in all_files.items():
|
||||
_parts: list[str] = _key.split('.')
|
||||
for _i in range(len(_parts)):
|
||||
_suf: str = '.'.join(_parts[_i:])
|
||||
if _suf not in _suffix_index:
|
||||
_suffix_index[_suf] = _val
|
||||
|
||||
reachable: set[str] = set()
|
||||
queue: list[str] = list(entry_files)
|
||||
# BFS 队列用 deque:pop(0) 是 O(n) 出队(每次移动整表),
|
||||
# popleft 为 O(1),可达文件数百时累计节省显著。
|
||||
queue_dq: Deque[str] = deque(entry_files)
|
||||
|
||||
# BFS trace: record each file processed and unresolved deps
|
||||
_bfs_trace: list[str] = []
|
||||
_unresolved_deps: list[tuple[str, str]] = [] # (current_file, dep)
|
||||
|
||||
while queue:
|
||||
current: str = queue.pop(0)
|
||||
while queue_dq:
|
||||
current: str = queue_dq.popleft()
|
||||
if current in reachable:
|
||||
continue
|
||||
reachable.add(current)
|
||||
deps: set[str] = get_file_dependencies(current, src_root)
|
||||
_bfs_trace.append(f" {os.path.basename(current)} deps={len(deps)}")
|
||||
for dep in deps:
|
||||
if dep in all_files and all_files[dep] not in reachable:
|
||||
queue.append(all_files[dep])
|
||||
queue_dq.append(all_files[dep])
|
||||
else:
|
||||
_matched = False
|
||||
for key, val in all_files.items():
|
||||
if key.endswith('.' + dep) or key == dep:
|
||||
if val not in reachable:
|
||||
queue.append(val)
|
||||
_matched = True
|
||||
break
|
||||
if not _matched:
|
||||
_unresolved_deps.append((os.path.basename(current), dep))
|
||||
_target: str | None = _suffix_index.get(dep)
|
||||
if _target is not None and _target not in reachable:
|
||||
queue_dq.append(_target)
|
||||
|
||||
return reachable
|
||||
|
||||
@@ -204,6 +210,14 @@ def topological_sort_files(py_files: list[str], src_root: str) -> list[str]:
|
||||
if dep_file != filepath: # 避免自依赖
|
||||
graph[filepath].add(dep_file)
|
||||
|
||||
# 反向邻接表:依赖当前文件的文件列表。Kahn 出队时只需遍历 reverse[cur],
|
||||
# 替代原"每出队一个节点就全图扫描找依赖者"的 O(V²)(文件多时显著拖慢)。
|
||||
reverse: dict[str, list[str]] = {f: [] for f in py_files}
|
||||
for f, deps in graph.items():
|
||||
for dep in deps:
|
||||
if dep in reverse:
|
||||
reverse[dep].append(f)
|
||||
|
||||
# 拓扑排序 (Kahn's algorithm)
|
||||
in_degree: dict[str, int] = {f: 0 for f in py_files}
|
||||
for f, deps in graph.items():
|
||||
@@ -211,20 +225,20 @@ def topological_sort_files(py_files: list[str], src_root: str) -> list[str]:
|
||||
if dep in in_degree:
|
||||
in_degree[f] += 1
|
||||
|
||||
# 小根堆替代"每轮 queue.sort() + pop(0)"(O(n² log n)):heappop 取最小
|
||||
# 保证同级别文件按字母序处理(确定性),单次 O(log n)。
|
||||
import heapq
|
||||
queue: list[str] = [f for f, degree in in_degree.items() if degree == 0]
|
||||
heapq.heapify(queue)
|
||||
sorted_files: list[str] = []
|
||||
|
||||
while queue:
|
||||
# 按字母顺序处理同级别的文件,确保确定性
|
||||
queue.sort()
|
||||
current: str = queue.pop(0)
|
||||
current: str = heapq.heappop(queue)
|
||||
sorted_files.append(current)
|
||||
|
||||
for f, deps in graph.items():
|
||||
if current in deps:
|
||||
in_degree[f] -= 1
|
||||
if in_degree[f] == 0:
|
||||
queue.append(f)
|
||||
for f in reverse[current]:
|
||||
in_degree[f] -= 1
|
||||
if in_degree[f] == 0:
|
||||
heapq.heappush(queue, f)
|
||||
|
||||
# 如果有环,添加剩余文件
|
||||
if len(sorted_files) < len(py_files):
|
||||
|
||||
+54
-12
@@ -42,6 +42,17 @@ _OPERATORS: tuple[str, ...] = (
|
||||
'@', '&', '|', '^', '~', '<', '>', '=', '!',
|
||||
)
|
||||
|
||||
# 按首字符分组的运算符表(组内保持最长优先顺序)。_EmitOp 每 token 只遍历
|
||||
# 同首字符的少数候选(平均 2-3 个),替代原 42 项全表 startswith 扫描
|
||||
# (cProfile 热点:58 万次调用 tottime 3.6s,首字符分组后显著下降)。
|
||||
_OP_BY_FIRST: dict[str, tuple[str, ...]] = {}
|
||||
for _op in _OPERATORS:
|
||||
_first: str = _op[0]
|
||||
if _first in _OP_BY_FIRST:
|
||||
_OP_BY_FIRST[_first] = _OP_BY_FIRST[_first] + (_op,)
|
||||
else:
|
||||
_OP_BY_FIRST[_first] = (_op,)
|
||||
|
||||
# 行首空白 + 关键字 + 空白 + 标识符 → 软关键字函数定义(Parser 层处理)
|
||||
SOFT_KEYWORDS: frozenset[str] = frozenset({'fn', 'fun', 'func', 'function', 'extern'})
|
||||
|
||||
@@ -120,15 +131,36 @@ class Tokenizer:
|
||||
self.Comments: list[Token] = []
|
||||
# 行起始字符索引表(索引 0 对应第 1 行),用于行列换算
|
||||
self._LineStarts: list[int] = [0]
|
||||
# _PosInfo 增量缓存:token 顺序创建时 pos 单调递增,免 bisect
|
||||
self._LastLineIdx: int = 0
|
||||
self._LastPos: int = -1
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 位置换算
|
||||
# ------------------------------------------------------------------
|
||||
def _PosInfo(self, pos: int) -> tuple[int, int]:
|
||||
"""将字符索引换算为 (1-based 行号, UTF-8 字节列偏移)。"""
|
||||
idx: int = bisect.bisect_right(self._LineStarts, pos) - 1
|
||||
"""将字符索引换算为 (1-based 行号, UTF-8 字节列偏移)。
|
||||
|
||||
优化(cProfile 热点:~300 万次调用):token 顺序创建时 pos 单调
|
||||
递增,用上次行索引增量顺延替代 bisect(O(log n)→摊销 O(1));
|
||||
列偏移对 ASCII 行用 len() 直接量取,避免每次 encode 分配字节串。
|
||||
"""
|
||||
ls: list[int] = self._LineStarts
|
||||
if pos >= self._LastPos and self._LastLineIdx + 1 < len(ls):
|
||||
idx: int = self._LastLineIdx
|
||||
nls: int = len(ls)
|
||||
while idx + 1 < nls and ls[idx + 1] <= pos:
|
||||
idx += 1
|
||||
else:
|
||||
idx = bisect.bisect_right(ls, pos) - 1
|
||||
self._LastLineIdx = idx
|
||||
self._LastPos = pos
|
||||
line: int = idx + 1
|
||||
col_bytes: int = len(self.Source[self._LineStarts[idx]:pos].encode('utf-8'))
|
||||
seg: str = self.Source[ls[idx]:pos]
|
||||
if seg.isascii():
|
||||
col_bytes: int = len(seg)
|
||||
else:
|
||||
col_bytes = len(seg.encode('utf-8'))
|
||||
return line, col_bytes
|
||||
|
||||
def _Make(self, tok_type: str, start: int, end: int) -> Token:
|
||||
@@ -283,22 +315,32 @@ class Tokenizer:
|
||||
def _EmitOp(self) -> None:
|
||||
"""运算符最长匹配(含 ... 与增强赋值运算符)。"""
|
||||
start: int = self.Pos
|
||||
for op in _OPERATORS:
|
||||
if self.Source.startswith(op, self.Pos):
|
||||
self.Pos += len(op)
|
||||
self.Tokens.append(self._Make(OP, start, self.Pos))
|
||||
self.AtLineStart = False
|
||||
return
|
||||
ops: tuple[str, ...] | None = _OP_BY_FIRST.get(self.Source[self.Pos])
|
||||
if ops is not None:
|
||||
for op in ops:
|
||||
if self.Source.startswith(op, self.Pos):
|
||||
self.Pos += len(op)
|
||||
self.Tokens.append(self._Make(OP, start, self.Pos))
|
||||
self.AtLineStart = False
|
||||
return
|
||||
# 未知字符:单字符 OP 兜底
|
||||
self.Pos += 1
|
||||
self.Tokens.append(self._Make(OP, start, self.Pos))
|
||||
self.AtLineStart = False
|
||||
|
||||
def _ScanName(self) -> None:
|
||||
"""扫描标识符。"""
|
||||
"""扫描标识符(内联 _IsIdentPart 避免 70 万次函数调用开销)。"""
|
||||
start: int = self.Pos
|
||||
while self.Pos < self.Length and _IsIdentPart(self.Source[self.Pos]):
|
||||
self.Pos += 1
|
||||
src: str = self.Source
|
||||
n: int = self.Length
|
||||
pos: int = self.Pos
|
||||
while pos < n:
|
||||
ch: str = src[pos]
|
||||
if ch == '_' or ch.isalnum() or ord(ch) > 127:
|
||||
pos += 1
|
||||
else:
|
||||
break
|
||||
self.Pos = pos
|
||||
self.Tokens.append(self._Make(NAME, start, self.Pos))
|
||||
self.AtLineStart = False
|
||||
|
||||
|
||||
Reference in new issue
Block a user