perf(callgen): text.ll 存在性改属性查询 + 返回类型按模块一次性缓存

第 1 项:_dep_text_ll_exists 原以 fileio.File(MODE.R) 只读打开探测存在性(33,244 次调用,占总 create_file_utf8 46.6%,每次约 57us 的开句柄成本)。改用 GetFileAttributesA 并显式排除目录(对齐 IncludesScanner._PathExistsA 先例),无输出缓冲、零句柄。

第 2 项:_LookupMethodRetTyFromTextLL(29,974 次、约 380,505 tick/次、self 11.41e9 即 8.9%)原每次重开加重读加逐字节重扫整份 text.ll(实测约 2.47 tick/字节,均值 154KB)。新增 PermPool 缓存表(mangled 名 到 返回类型文本):某模块 text.ll 首次成功读入时登记其全部 define 行,同模块后续查询 O(1);命中后用调用方 pool 经 _ParseLLVMRetTyStr 重建类型,与原路径等价。

关键安全点:只存返回类型文本、绝不存会随每文件 reset_to 失效的 LLVMType*;同名只记首次(保持首个匹配优先);仅成功读到文件后才回写,未找到或文件暂不存在绝不缓存(Phase A 顺序生成依赖,负缓存会出错);缓存或分配失败一律回退原扫描。原扫描逻辑一字未改,保留为未命中路径。
This commit is contained in:
TermiNexus committed 2026-09-14 15:26:10 +08:00
1 parent ce612c8e57
commit b918321f77
1 file changed
+160 -5
+160 -5
View File
@@ -7,11 +7,13 @@ from stdint import *
import ast
import llvmlite
import memhub
import hashtable
import string
import stdio
import stdlib
import viperlib
import w32.fileio as fileio
import w32.win32file as win32file
import lib.core.VLogger as VLogger
import lib.Stub.StubMerger as StubMerger
import lib.core.CompilerFactory as CompilerFactory
@@ -92,6 +94,14 @@ class CallGenBase:
GlobalDefaults: GenTypes.FuncDefaultsEntry | t.CPtr
GlobalDefaultsCount: int
# --- 依赖模块 text.ll「方法返回类型文本」缓存(_LookupMethodRetTyFromTextLL 性能优化) ---
# key = text.ll 里 define 的完整 mangled 函数名("{sha1}.{后缀}")→ 返回类型文本。
# 表与键/值均 PermPool 分配(跨文件/Phase 存活);sha1 本身是内容哈希 ⇒ 天然
# 内容寻址,无需 size/mtime 校验。值只存**文本**(绝不存 LLVMType*——其分配在
# 每文件 reset_to 失效的 pool 中);命中后用调用方 pool 重建类型,与原路径等价。
# 惰性建表;建表/分配失败即不缓存,回退原扫描(正确性保证,非兜底)。| 不用全局裸表。
MethodRetTyMap: hashtable.HashTable | t.CPtr
def InitCallGen(self) -> int:
"""初始化调用 Mixin 字段(子类构造时内联调用,规避 NoVTable 继承符号不转发)"""
if self is None:
@@ -114,6 +124,7 @@ class CallGenBase:
self.GlobalFuncNameIdxBuilt = -1
self.GlobalDefaults = None
self.GlobalDefaultsCount = 0
self.MethodRetTyMap = None
return 0
# ============================================================
@@ -1062,6 +1073,127 @@ class CallGenBase:
# 无法解析
return None
# ============================================================
# _LookupCachedRetTyText - 查「依赖模块 text.ll 方法返回类型文本」持久缓存
#
# 缓存键 = text.ll 中 define 的完整 mangled 函数名
# ("{sha1}.{class}.{method}";bare 模式 "{sha1}.{method}")。sha1 本身是内容
# 哈希 ⇒ 天然内容寻址,无需 size/mtime 校验。命中返回返回类型文本(PermPool
# 持久),未命中/表未建返回 None(调用方回退原扫描)。
# ============================================================
def _LookupCachedRetTyText(self, class_sha1: str, class_name: str,
method_name: str, use_bare: int) -> str:
"""查返回类型文本缓存:命中返回文本,未命中返回 None"""
if self.Gen is None or class_sha1 is None or method_name is None:
return None
ht: hashtable.HashTable | t.CPtr = self.Gen.CallGenBase.MethodRetTyMap
if ht is None:
return None
sha1_len: sizet = string.strlen(class_sha1)
cls_len: sizet = 0
if class_name is not None:
cls_len = string.strlen(class_name)
meth_len: sizet = string.strlen(method_name)
# 长度公式与 _LookupMethodRetTyFromTextLL 构造 search_buf 时完全一致
key_len: sizet = sha1_len + meth_len + 2
if use_bare == 0:
key_len = sha1_len + cls_len + meth_len + 4
key_buf: bytes = stdlib.malloc(key_len)
if key_buf is None:
return None
if use_bare != 0:
viperlib.snprintf(key_buf, key_len, "%s.%s", class_sha1, method_name)
else:
viperlib.snprintf(key_buf, key_len, "%s.%s.%s",
class_sha1, class_name, method_name)
v: t.CPtr = ht.__getitem__(key_buf)
stdlib.free(key_buf)
if v is None:
return None
return (str | t.CPtr)(v)
# ============================================================
# _CacheModuleRetTyTexts - 把某模块 text.ll 全部 define 行的返回类型写入持久缓存
#
# 仅在 _LookupMethodRetTyFromTextLL 成功读到 text.ll 后调用(未读到文件绝不写
# 缓存:Phase A 按文件顺序翻译,依赖模块此遍可能尚未生成 text.ll,未命中必须
# 回退原扫描、负结果不可缓存)。键=define 完整 mangled 函数名(深拷贝进
# PermPool),值="define " 与 "@" 之间的返回类型文本(深拷贝进 PermPool)。
# 同名只记首次出现(与逐行扫描「首个命中即返回」一致);只登记以 "{sha1}."
# 开头的本模块 mangled 名(调用方查询键恒为此前缀,外部/内部符号永不命中)。
# ============================================================
def _CacheModuleRetTyTexts(self, class_sha1: str, text_buf: bytes,
text_len: sizet) -> None:
"""登记 text.ll 全部 define 行到持久缓存(建表/分配失败即不缓存,回退原扫描)"""
if self.Gen is None or class_sha1 is None or text_buf is None or text_len == 0:
return
perm: rowptr[memhub.MemBuddy] = CompilerFactory.factory.PermPool
if perm is None:
return
ht: hashtable.HashTable | t.CPtr = self.Gen.CallGenBase.MethodRetTyMap
if ht is None:
ht = hashtable.HashTable(perm)
if ht is None:
return
self.Gen.CallGenBase.MethodRetTyMap = ht
sha1_len: sizet = string.strlen(class_sha1)
pos: sizet = 0
while pos < text_len:
ls: sizet = pos
le: sizet = pos
while le < text_len:
if text_buf[le] == '\n':
break
le += 1
ll: sizet = le - ls
# define 行判定与 _LookupMethodRetTyFromTextLL 逐字一致
if ll >= 7 and text_buf[ls] == 'd' and string.strncmp(text_buf + ls, "define ", 7) == 0:
at_pos: sizet = ls + 7
while at_pos < ls + ll:
if text_buf[at_pos] == '@':
break
at_pos += 1
if at_pos < ls + ll:
name_start: sizet = at_pos + 1
quoted: int = 0
if name_start < ls + ll and text_buf[name_start] == '"':
name_start += 1
quoted = 1
# 定界:带引号名到 '"',裸名到 '('(与查找侧边界判定等价)
name_end: sizet = name_start
if quoted != 0:
while name_end < ls + ll:
if text_buf[name_end] == '"':
break
name_end += 1
else:
while name_end < ls + ll:
if text_buf[name_end] == '(':
break
name_end += 1
name_len: sizet = name_end - name_start
# 只登记本模块 mangled 名 "{sha1}.{后缀}"(查询键恒为此前缀)
if name_len > sha1_len + 1:
if string.strncmp(text_buf + name_start, class_sha1, sha1_len) == 0 \
and text_buf[name_start + sha1_len] == '.':
key_buf: bytes = stdlib.malloc(name_len + 1)
if key_buf is not None:
string.strncpy(key_buf, text_buf + name_start, name_len)
key_buf[name_len] = '\0'
# 同名首次为准(不被后续同名覆盖,与首个命中一致)
if ht.__contains__(key_buf) == 0:
rt_len: sizet = at_pos - (ls + 7)
kcopy: str = perm.alloc(name_len + 1)
tcopy: str = perm.alloc(rt_len + 1)
if kcopy is not None and tcopy is not None:
string.strncpy(kcopy, text_buf + name_start, name_len)
kcopy[name_len] = '\0'
string.strncpy(tcopy, text_buf + ls + 7, rt_len)
tcopy[rt_len] = '\0'
ht.set_str(kcopy, tcopy)
stdlib.free(key_buf)
pos = le + 1
# ============================================================
# _LookupMethodRetTyFromTextLL - 从依赖模块的 text.ll 查找方法返回类型
#
@@ -1103,6 +1235,15 @@ class CallGenBase:
if sha1_len < 2:
return None
# [按模块一次性缓存·命中] 该模块 text.ll 已被读过一次 ⇒ O(1) 取回函数返回
# 类型文本,用**调用方 pool** 重建类型直接返回(与原路径语义等价,不再开
# 文件/读盘/逐字节重扫)。未命中 → 走下方原实现;仅在成功读到 text.ll 后
# 才回写缓存(依赖模块此遍可能尚未生成,负结果绝不缓存)。
cached_text: str = self._LookupCachedRetTyText(
class_sha1, class_name, method_name, use_bare)
if cached_text is not None:
return self._ParseLLVMRetTyStr(pool, cached_text, 0, string.strlen(cached_text))
# 构造 text.ll 路径 (level=1): {temp_dir}/{sha1[0:2]}/{sha1}.text.ll
# 使用 subdir 缓冲区避免 %c 格式化问题(与 StubMerger._sliced_path 一致)
path_len: sizet = self.Gen.CallGenBase.TempDirLen + sha1_len + 14
@@ -1170,6 +1311,12 @@ class CallGenBase:
# 在 text.ll 中搜索 define 行包含 search_buf
buf_len: sizet = string.strlen(read_buf)
# [按模块一次性缓存·回写] 已成功读到本模块 text.ll:登记其全部 define 行
# (同模块后续查询 O(1) 命中,不再重开/重读/逐字节重扫)。此处不改动下方
# 原扫描逻辑(保留为未命中路径)。
self._CacheModuleRetTyTexts(class_sha1, read_buf, buf_len)
pos: sizet = 0
result_ty: llvmlite.LLVMType | t.CPtr = None
@@ -1250,11 +1397,19 @@ class CallGenBase:
viperlib.snprintf(path_buf, path_len, "%s/%s/%s.text.ll",
self.Gen.CallGenBase.TempDir, subdir_buf, sha1)
stdlib.free(subdir_buf)
tf: fileio.File | t.CPtr = fileio.File(path_buf, fileio.MODE.R)
exists: int = 0
if not tf.closed:
tf.close()
exists = 1
# [廉价存在性判定] 用 GetFileAttributesA(无输出缓冲、不创建文件句柄)替代
# 原「只读打开探测」:原实现只为看是否存在却真开句柄(走 create_file_utf8),
# 本函数 33,244 次调用占总 create_file_utf8 调用 46.6%。写法对齐同仓既有
# IncludesScanner._PathExistsA(该处亦以属性查询替代开文件句柄)。
attr: ULONG = win32file.GetFileAttributesA(path_buf)
exists: int = 1
# INVALID_FILE_ATTRIBUTES = 不存在/不可访问 → 0。
if attr == 0xFFFFFFFF:
exists = 0
# 原实现以只读 CreateFileA 打开:目录会被拒绝(未带 FILE_FLAG_BACKUP_SEMANTICS)
# → 0,故此处显式排除目录以保持「是可读常规文件」的等价判定。
if (attr & win32file.FILE_ATTRIBUTE_DIRECTORY) != 0:
exists = 0
stdlib.free(path_buf)
return exists