From 5167e298eff81fe637ae796d9e2d0f62fdc5941b Mon Sep 17 00:00:00 2001 From: company-desktop-computer Date: Wed, 28 Oct 2026 10:19:06 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E7=AE=A1=E7=BA=BF?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E6=A0=B7=E5=BC=8F=E7=BC=93=E5=AD=98=E7=9A=84?= =?UTF-8?q?=E6=95=88=E7=8E=87=E6=8F=90=E5=8D=87=E9=87=8F=E5=8C=96=E5=88=86?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增第 13 节,把「这次改动到底快了多少」量化下来: - 实测 Plumbing.db(106 张表)新建连接 + 首条查询 + 关闭 ≈ 0.98 ms, 其中 schema 解析占 ~0.74 ms;仅 open + close 只有 0.09 ms。说明代价 不在 sqlite3_open 本身,而在新连接的首次查询。 - 按此折算改动前的开库账:dwgInFields 单实体 16 次 open ≈12~16 ms, 绘制子实体 14 次,Jig 拖动每帧 ~20 次 ≈15~20 ms(TDbPipeLine 里的 setInfo2Ent 没有 HasKey 判断,是无条件必付)。 - 改动后开库次数从 16×N 变成常数 2~4,O(N) 降为 O(1);端到端估算 提升 20~60 倍,500 实体图纸加载 6~8 s → 0.1~0.4 s。 - 同时写明哪些路径受益、哪些不受益(新图纸已由 PR #87 刷过 XData, 本来就不查库),避免高估收益。 - 补当前残留瓶颈与复核方法:剩余代价主要是 getCurrentTrade 触发的 ReadModelXData 全量 XData 解析,以及图层/线型表每次打开;附计数 器验证法与三个场景的预期数字。 --- sqlite_query_cache.md | 104 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/sqlite_query_cache.md b/sqlite_query_cache.md index f951c26..3752741 100644 --- a/sqlite_query_cache.md +++ b/sqlite_query_cache.md @@ -2,7 +2,7 @@ > 结论:这两个类本身不直接调用 sqlite。初始化阶段(`dwgInFields`,即打开图纸加载实体时)的大量 sqlite 访问全部通过 `CoreTools` 的 4 个样式查询 getter 间接产生。 > -> 状态(2026-10-27):第 7 节方案已落地到 `CoreCommand/CoreTools.cpp`,R190 Debug 下 `AecRxBase` / `CoreCommand_BAL_REMOTE` / `CoreLibrary_BAL_REMOTE` / `CoreImplement` 四个工程编译通过。详见第 12 节。 +> 状态(2026-10-27):第 7 节方案已落地到 `CoreCommand/CoreTools.cpp`(PR #88 已合并),R190 Debug 下 `AecRxBase` / `CoreCommand_BAL_REMOTE` / `CoreLibrary_BAL_REMOTE` / `CoreImplement` 四个工程编译通过。详见第 12 节;量化收益见第 13 节。 --- @@ -282,3 +282,105 @@ cmake --build --preset R190 --config Debug --target CoreImplement - 在 CAD 中加载 `CoreCommand.arx` 后实测:管线中心/外边线的颜色、线型、线宽、图层与改前一致;老图纸(样式键缺失)加载正常;两张图切换不串图层/线型。 - 用 MSVC 性能分析对比 `sqlite3_open` / `sqlite3_exec` 调用次数。 - 若后续需要「改配置立即生效」,在系统样式配置保存处加 `InvalidatePipeSysStyleCache()`(清空 `PipeSysStyleCache()`)。 + +## 13. 效率提升量化分析(2026-10-27) + +### 13.1 先把「贵在哪」测出来 + +`sys\Plumbing.db` 实测:**4.27 MB / 106 张表 / 4170 页 × 1 KB**。 + +用同引擎(Python `sqlite3` 3.50.4)在**文件已在系统页缓存**的条件下跑 400 次取中位数(3 轮结果一致): + +| 操作 | 耗时 | +| --- | --- | +| `open` + `close`(不查询) | **0.09 ms** | +| `open` + 跑 1 条 `SELECT` + `close` | **0.98 ms** | +| 空库(1 张表)同流程 | 0.24 ms | +| 同一连接上再跑 1 条 `SELECT` | 0.055 ms | + +**结论:代价不在 `sqlite3_open` 本身(0.09 ms),而在「新连接上的第一次查询」——它要解析 106 张表的 schema,约 0.74 ms。** 而 `sqlite3_close` 会把页缓存和 schema 一起扔掉,所以下一次 `open` 又得从头解析。 + +> 仓库里编译的是 sqlite **3.7.16.2(2013 年,见 `inc/sqlite3.h`)**,schema 解析比 3.50 更慢,因此上表应视为**下限**。 + +### 13.2 改动前的开库账(精确计数) + +每个 getter 内部:自己 `InitADOServer` 开 1 次 + `getCurrentPipeSysTblName()` 再开 1 次 = **2 次 open + 2 条 SQL**。 + +| 调用场景 | getter 数 | open 次数 | 估算耗时 | +| --- | --- | --- | --- | +| `dwgInFields` 单实体(8 个键全缺) | 8 | **16** | **≈12–16 ms** | +| 首实体额外递归(`getLyByPipeSys` 建图层 → 再调 lt+color) | +4 | +8 | +8 ms | +| `setInfo2Ent` + `setInfo2Dc`(每个绘制子实体) | 7 | **14** | **≈11–14 ms** | +| `TDbPipeLine::setInfo2Ent`(Jig 创建,**无条件** 8 个) | 8 | **16** | **≈12–16 ms** | +| Jig 拖动一帧(`SetPipeLayer` + `setInfo2Ent`) | ~10 | **~20** | **≈15–20 ms/帧** | + +> 注:`TDbPipeLine`(`PipelineCmd/AecPipeLine.cpp:52`)里的 `setInfo2Ent` **没有 `HasKey` 判断**,无条件查 8 次 —— 所以「拖动画管」这条路是必付的,每帧 20 ms 意味着光这一项就卡死在 50 fps 以下。 + +### 13.3 改动后的账 + +| 阶段 | open 次数 | +| --- | --- | +| 进程内首次(某系统 × 中心线) | 2(表名 1 + 样式 1) | +| 同系统另一边线 | +2 | +| **之后每次 getter** | **0** | + +即:**开库次数从 `16 × N(实体数)` 变成常数 2~4**,从 O(N) 降为 O(1)。 + +### 13.4 提升倍数 + +| 场景 | 改前 | 改后 | 提升 | +| --- | --- | --- | --- | +| 单次 getter | 2 open ≈ 2 ms | 0 open ≈ 0.03–0.1 ms | **20–60×** | +| 单实体加载(8 getter) | 16 open ≈ 12–16 ms | ≈0.3–0.8 ms | **20–50×** | +| 500 实体图纸加载 | 8000 open ≈ **6–8 s** | 2–4 open + ≈0.1–0.4 s | **20–40×** | +| 绘制一个管件子实体 | 14 open ≈ 11–14 ms | ≈0.3–0.7 ms | **20–40×** | +| Jig 拖动一帧 | 20 open ≈ 15–20 ms | ≈0.3–1 ms | **20–60×** | + +**端到端总结:sqlite 那部分开销被消掉 ~100%(O(N)→O(1)),实际观感提升约 10–40 倍**(取决于每个实体还附带多少次 getter 调用)。若 `Plumbing.db` 在冷缓存或被杀软逐个扫描,单次 open 可达 5–10 ms,提升倍数还会更大。 + +### 13.5 哪些路径受益、哪些不受益 + +| 路径 | 是否受益 | 原因 | +| --- | --- | --- | +| Jig 创建 / 拖拽(`TDbPipeLine::setInfo2Ent`、`SetPipeLayer`) | ✅ **必然受益** | 无条件查库,每帧必付,体感最明显 | +| 老图纸加载(XData 缺样式键) | ✅ **受益最大** | `HasKey` 未命中 → 逐键查库 | +| 新版本保存过的图纸加载 | ⚠️ 无提升 | PR #87 已把样式刷进 XData,`HasKey` 全命中 → 本来就不查库 | +| 新图纸绘制(`setInfo2Ent` 命中 `getPipeSysInfoByEnt`) | ⚠️ 无提升 | 同上,样式已在实体 XData 里 | + +### 13.6 现在剩下的瓶颈(可再提 5–10 倍) + +`getCurrentPipeSysTblName()` 的缓存检查**在 `getCurrentTrade()` 之后**,所以每次 getter 调用仍要跑一次: + +``` +getCurrentTrade() + └─ KTModelLayoutXData::ReadModelXData(KT_PIPEINIT) AecRxBase/KTModelLayoutXData.cpp:52 + ├─ 打开 NamedObjectsDictionary + ├─ getAt("ACAD_LAYOUT") → 打开 + ├─ getAt("Model") → 打开 + └─ Resbuf2XDMap:把 Model 布局的全部 XData 转成 map +``` + +即 **3 次字典对象打开 + 一次全量 XData 解析**,每个 getter 一次 → 单实体 8 次。这是当前每个 getter 剩下的 0.03–0.1 ms 的主要来源。 + +下一步(按性价比排序): + +1. **`getCurrentTrade` 不走全量解析**:加一个「只取单个 XData 键」的轻量读法,或缓存 trade(在写入 `DOC_TRADE` 处失效)。 +2. **`getLyByPipeSys` 每次 `kForWrite` 打开图层表**(即使图层已存在,见第 2 节旧文),`getLtByPipeSys` 每次打开线型表 → 可加 `(数据库指针, sys, center) → AcDbObjectId` 缓存,随文档切换清空。 +3. 做完 1+2,getter 可降到 µs 级,管线重绘接近「零成本」。 + +### 13.7 复核方法 + +在 `QueryPipeSysStyle` 里临时加计数(或换成 MSVC 性能分析看 `sqlite3_prepare` 调用次数): + +```cpp +static int g_nQueryCount = 0; +++g_nQueryCount; +acutPrintf(_T("\n[PipeSysStyle] 实际查库次数 = %d"), g_nQueryCount); +``` + +| 验证场景 | 改前 | 改后 | +| --- | --- | --- | +| 打开一张 500+ 管件的老图纸(样式键缺失) | 应打几千 | 应 ≤ 4 | +| 拖动画一根管 + 弯头 | 每帧都涨 | 始终不涨 | +| 平移重绘整图 | 每帧涨几百 | 不涨 | +