From 47d7b96cad92d034b64eca8a826b6ff37608b6f9 Mon Sep 17 00:00:00 2001 From: company-desktop-computer Date: Wed, 28 Oct 2026 23:32:22 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E7=BB=99=E5=9F=BA=E7=A1=80=E4=BD=93?= =?UTF-8?q?=E6=9E=84=E9=80=A0=E5=8A=A0=E8=AE=A1=E6=97=B6=EF=BC=8C=E5=AE=9A?= =?UTF-8?q?=E4=BD=8D=E5=88=B0=20DrawPipe/DrawCone/DrawLoft=20=E5=85=B1?= =?UTF-8?q?=E5=8D=A0=2033%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 继续追"剩余效率问题"。上一轮已确认 Parasolid 的 93.4% 发生在我们的视口绘制里, 本轮机位下一层:给 GiDrawUtility 的基础体构造函数加计时。 新增 inc/PerfCounterLite.h:header-only 的轻量计数器。 AecRxBase.arx 不链接 CoreLibrary(静态库只被 CoreCommand 链接),拿不到 SectionPerf 的符号,所以单独做一份;只在 Debug + AEC_PERF_COUNTER=1 时生效,Release 零开销。 统计写 %TEMP%\aec_prim_perf.txt(独立文件,避免提前写出 aec_section_perf.txt 让自动化脚本误判"开图完成")。 接入 GiDrawUtility:DrawPipe 三个重载、DrawLoft 两个重载、DrawCone、DrawCylinder。 实测(big1.dwg,93 s 开图 / 81 s CPU): DrawPipe(region) 7201 次 13.2 s 平均 1837us 最长 130ms DrawLoft(reg) 883 次 5.27 s 平均 5969us DrawLoft(curve) 883 次 5.90 s 平均 6679us DrawCone 811 次 4.64 s 平均 5717us DrawPipe(pts)/Cylinder 2411 次 4.07 s 平均 1688us 去重后约 27 s,占 81 s CPU 的 33%。 结论(详见 perf-primitive-cost.md): - 7201 次管道构建 vs 约 2260 个管类实体 = 3.2 次/实体,护栏/爬梯/设备细节也在造管道 - 单次成本差一个量级:圆柱 1.7ms 最便宜(已是优化过的快路径),圆锥 5.7ms、放样 6.0~6.7ms 最贵 - 长尾明显:DrawPipe 最长 130ms(平均的 70 倍),存在个别大件 - 都在"首次构建"路径上,没有重复重建,所以只能少建 / 建得更便宜 / 跨实体复用同参数几何 验证:R230 Debug 编译通过,tools/run_acad_perf.ps1 自动开图取数; 截面缓存与回写仍为正常/0(未受本次插桩影响)。 --- .../Code2026/AecRxBase/GiDrawUtility.cpp | 8 + SourceCode/Code2026/inc/PerfCounterLite.h | 147 ++++++++++++++++++ perf-primitive-cost.md | 64 ++++++++ 3 files changed, 219 insertions(+) create mode 100644 SourceCode/Code2026/inc/PerfCounterLite.h create mode 100644 perf-primitive-cost.md diff --git a/SourceCode/Code2026/AecRxBase/GiDrawUtility.cpp b/SourceCode/Code2026/AecRxBase/GiDrawUtility.cpp index 931cb3e..cf7a042 100644 --- a/SourceCode/Code2026/AecRxBase/GiDrawUtility.cpp +++ b/SourceCode/Code2026/AecRxBase/GiDrawUtility.cpp @@ -1,5 +1,6 @@ #include "StdAfx.h" #include "GiDrawutility.h" +#include "PerfCounterLite.h" #include "EarThwormDef.h" namespace @@ -262,6 +263,7 @@ AECDLLIMPEXP AcDbEntity* GiDrawUtility::DrawLoft(const AcDbRegion *pTop , const AcDbRegion *pBottom , const AcDbCurve *pLine) { + perf_lite::Scope perfScope("GDU::DrawLoft(reg)"); AcDbRegion *_pTop = (AcDbRegion*)pTop->clone(); AcDbRegion *_pBottom = (AcDbRegion*)pBottom->clone(); @@ -297,6 +299,7 @@ AECDLLIMPEXP AcDbEntity* GiDrawUtility::DrawLoft(const AcDbCurve *pTop , const AcDbCurve *pBottom , const AcDbCurve *pLine) { + perf_lite::Scope perfScope("GDU::DrawLoft(curve)"); if (!pTop || !pBottom || !pLine) return NULL; AcDbCurve *_pTop = (AcDbCurve*)pTop->clone(); @@ -385,6 +388,7 @@ AECDLLIMPEXP AcDbEntity* GiDrawUtility::DrawCone(const AcGePoint3d &ptPos , const double &H , const double &xOffset) { + perf_lite::Scope perfScope("GDU::DrawCone"); AcGePoint3d ptBottom(AcGePoint3d::kOrigin); AcGePoint3d ptTop(ptBottom + xOffset * AcGeVector3d::kXAxis); ptTop.z = H; @@ -684,6 +688,7 @@ AECDLLIMPEXP AcDbEntity* GiDrawUtility::DrawCylinder(const AcGePoint3d &ptFr , const AcGePoint3d &ptTo , const double &dRadius) { + perf_lite::Scope perfScope("GDU::DrawCylinder"); return DrawPipe(ptFr, ptTo, dRadius, 0.); } @@ -782,6 +787,7 @@ AcDbEntity* GiDrawUtility::DrawPipe(const AcGePoint3d& ptFrom , const double &dThick , const int &nSegs /*= 20*/) { + perf_lite::Scope perfScope("GDU::DrawPipe(pts)"); // 零长度路径无法扫掠 if (EQUAL_ZERO(ptFrom.distanceTo(ptTo))) return NULL; @@ -844,6 +850,7 @@ AcDbEntity* GiDrawUtility::DrawPipe(const AcDbCurve* pCurve AcDbEntity* GiDrawUtility::DrawPipe(const AcDbCurve *pCurve, double dR) { + perf_lite::Scope perfScope("GDU::DrawPipe(curve,dR)"); if (!pCurve) return NULL; if (DOWNEQUAL_ZERO(dR)) return NULL; @@ -940,6 +947,7 @@ AcDbEntity *GiDrawUtility::DrawPipe( , const AcDbRegion *pRegion , const AcGePoint3d &ptCenInRgn) { + perf_lite::Scope perfScope("GDU::DrawPipe(region)"); if (!pRegion || !pCurve) return NULL; AcGePoint3d ptStart(AcGePoint3d::kOrigin); diff --git a/SourceCode/Code2026/inc/PerfCounterLite.h b/SourceCode/Code2026/inc/PerfCounterLite.h new file mode 100644 index 0000000..64ed1cf --- /dev/null +++ b/SourceCode/Code2026/inc/PerfCounterLite.h @@ -0,0 +1,147 @@ +// PerfCounterLite.h —— 给 AecRxBase 这类"不链接 CoreLibrary"的模块用的轻量计数器 +// +// 为什么不复用 SectionPerf:CoreLibrary 是静态库且只被 CoreCommand 链接, +// AecRxBase.arx 里拿不到它的符号;所以这里做成 header-only。 +// +// 行为: +// - 仅在 Debug(NDEBUG 未定义)且 AEC_PERF_COUNTER=1 时开启,Release 零开销 +// - 每个 tag 统计:调用次数、累计耗时、平均/最长 +// - 每 200 次调用重写一次 %TEMP%\aec_prim_perf.txt(快照) +// —— 故意不写 aec_section_perf.txt:那个文件是自动化脚本判断"开图完成"的信号, +// 提前写会让脚本误判并提前关闭 CAD + +#ifndef PERF_COUNTER_LITE_H +#define PERF_COUNTER_LITE_H + +#include +#include +#include +#include +#include +#include + +namespace perf_lite +{ + struct Entry + { + Entry() : n(0), us(0), maxus(0) {} + long long n; + long long us; + long long maxus; + }; + + inline bool Enabled() + { +#ifndef NDEBUG + static int s_nOn = -1; + if (s_nOn < 0) + { + const char* psz = ::getenv("AEC_PERF_COUNTER"); + s_nOn = (psz != NULL && psz[0] != '\0' && psz[0] != '0') ? 1 : 0; + } + return s_nOn == 1; +#else + return false; +#endif + } + + inline std::map& Stats() + { + static std::map s_map; + return s_map; + } + + inline long long QpcNow() + { + LARGE_INTEGER li; + ::QueryPerformanceCounter(&li); + return (long long)li.QuadPart; + } + + inline long long QpcFreq() + { + static long long s_nFreq = 0; + if (s_nFreq == 0) + { + LARGE_INTEGER li; + if (!::QueryPerformanceFrequency(&li) || li.QuadPart == 0) + s_nFreq = 1; + else + s_nFreq = (long long)li.QuadPart; + } + return s_nFreq; + } + + inline void Dump() + { + char szTmp[MAX_PATH + 1]; + ::memset(szTmp, 0, sizeof(szTmp)); + if (::GetTempPathA(MAX_PATH, szTmp) == 0) + return; + std::string sPath(szTmp); + sPath += "aec_prim_perf.txt"; + + FILE* fp = NULL; + if (fopen_s(&fp, sPath.c_str(), "w") != 0 || fp == NULL) + return; + + fprintf(fp, "---- primitive builders (per tag) ----\n"); + fprintf(fp, " count total_ms avg_us max_us tag\n"); + const std::map& m = Stats(); + for (std::map::const_iterator it = m.begin(); it != m.end(); ++it) + { + const Entry& e = it->second; + const double dAvg = e.n ? ((double)e.us / (double)e.n) : 0.0; + fprintf(fp, " %-9lld %10.1f %10.1f %10lld %s\n", + e.n, (double)e.us / 1000.0, dAvg, e.maxus, it->first.c_str()); + } + fclose(fp); + } + + inline void Add(const char* pszTag, long long nStart) + { + const long long nUs = (QpcNow() - nStart) * 1000000LL / QpcFreq(); + Entry& e = Stats()[std::string(pszTag != NULL ? pszTag : "?")]; + ++e.n; + e.us += nUs; + if (nUs > e.maxus) + e.maxus = nUs; + + // 每 200 次落一次快照,开图结束后文件里就是全量统计 + static long long s_nTotal = 0; + if (++s_nTotal % 200 == 0) + Dump(); + } + + // RAII:函数入口构造即可 + class Scope + { + public: + explicit Scope(const char* pszTag) : m_pszTag(pszTag), m_nStart(0) + { + if (Enabled()) + m_nStart = QpcNow(); + } + ~Scope() + { + if (m_nStart != 0) + Add(m_pszTag, m_nStart); + } + + private: + Scope(const Scope&); + Scope& operator=(const Scope&); + + const char* m_pszTag; + long long m_nStart; + }; + + // 在卸载时再补一次,确保最后的零头也落盘 + inline void Flush() + { + if (Enabled()) + Dump(); + } +} + +#endif // PERF_COUNTER_LITE_H diff --git a/perf-primitive-cost.md b/perf-primitive-cost.md new file mode 100644 index 0000000..74c4ab6 --- /dev/null +++ b/perf-primitive-cost.md @@ -0,0 +1,64 @@ +# 开图期基础体构造成本实测 + +> 承接 `open-drawing-perf-summary.md`(PR #107)的结论:剩余时间 93.4 % 的 Parasolid +> 工作都发生在我们的视口绘制里。本文再往下一层,量化**具体是哪些基础体构造函数在花钱**。 +> 工具:`inc/PerfCounterLite.h`,接入点在 `GiDrawUtility`。 +> 基准:`E:\桌面\big1.dwg`,AutoCAD 2020 / R230 Debug,开图 93 s(81 s CPU)。 + +## 1. 为什么要另做一套计数器 + +`GiDrawUtility` 在 **AecRxBase.arx**,而 `SectionPerfCounter` 在 CoreLibrary(静态库) +里、只被 CoreCommand 链接,AecRxBase 拿不到那些符号。所以新增 +`inc/PerfCounterLite.h`:header-only、Debug + `AEC_PERF_COUNTER=1` 才生效、 +按 tag 统计「次数 / 累计 / 平均 / 最长」。 + +输出写到 `%TEMP%\aec_prim_perf.txt`(每 200 次重写一次快照)。 +**故意不写 `aec_section_perf.txt`**:那个文件是自动化脚本判断"开图完成"的信号, +提前写会让脚本误判并提前关闭 CAD(这个坑之前在抓调用栈时踩过一次)。 + +## 2. 实测数据 + +| 构造函数 | 调用次数 | 总耗时 | 平均 | 最长 | +| --- | ---: | ---: | ---: | ---: | +| `DrawPipe(region)` 截面沿曲线扫掠 | **7201** | **13.2 s** | 1837 µs | **130 ms** | +| `DrawLoft(reg)` 放样(内含 curve 版) | 883 | 5.27 s | 5969 µs | 25 ms | +| `DrawLoft(curve)` | 883 | 5.90 s | 6679 µs | 26 ms | +| `DrawCone` 圆锥/圆台 | 811 | 4.64 s | **5717 µs** | 12 ms | +| `DrawPipe(pts)` → `DrawCylinder`(嵌套调用) | 2411 | 4.07 s | 1688 µs | 5 ms | + +`DrawLoft(reg)` 与 `DrawLoft(curve)`、`DrawPipe(pts)` 与 `DrawCylinder` 是嵌套关系, +去重后合计约 **27 s ≈ 81 s CPU 的 33 %**,与 CPU 采样里 +`DrawPipe 14.5 % + DrawLoft 6.6 % + …` 的量级吻合。 + +## 3. 四个结论 + +1. **调用量远超实体数**:7201 次管道构建,而这张图的管类实体约 2260 个 → **3.2 次/实体**。 + 说明除了管段本身,护栏、爬梯(`AecDbClimbStair::DrawLadderH3d`)、设备细节 + 也都在造管道。 +2. **单次成本差一个量级**:圆柱 1.7 ms、圆锥 5.7 ms、放样 6.0~6.7 ms。 + 最便宜的形式(点 + 半径 → `DrawCylinder`)已经是优化过的;**贵的是圆锥/放样**, + 它们通常用于变径、异形过渡件 —— 要么减少数量,要么用参数化近似替代。 +3. **长尾明显**:`DrawPipe(region)` 最长单次 **130 ms**,是平均值的 70 倍。 + 说明存在个别"大件"(长路径或复杂截面)。这类可以单独做分段或降级表达。 +4. **没有重复重建**:这些调用都在"首次构建"路径上 + (`AcDbX3dSolid`/`AecDbX3dPart` 各建一次,前一轮已用计数器验证)。 + 所以不能再靠"命中缓存"来省,剩下只有三条路: + - **少建**:LOD、按视图剔除、合并小构件; + - **建得更便宜**:直管已有圆柱快路径,圆锥/放样考虑参数化近似; + - **跨实体复用同参数几何**:需要 (DN, 壁厚, 段长) 之类的签名——本次没做。 + +## 4. 复现方式 + +```bat +set AEC_PERF_COUNTER=1 +:: 打开图纸(或用 tools/run_acad_perf.ps1 自动跑) +type %TEMP%\aec_prim_perf.txt +``` + +注意: +- 计数器只在 **Debug** 生效(`NDEBUG` 未定义时),Release 零开销; +- 每 200 次调用才落一次盘,所以文件里的次数是 200 的整数倍附近, + 比值/平均值仍然可用; +- 新增文件必须 **UTF-8 with BOM**:本轮就踩过一次——`.h` 存成无 BOM 的 UTF-8, + MSVC 按 GBK 读,中文注释末尾的字节把换行吃掉,编译直接报 + `s_nTotal: undeclared identifier` 这种莫名其妙的错。 -- 2.54.0