From f8d10d5a3b0ae02eee028accddd5a144b07c93e1 Mon Sep 17 00:00:00 2001 From: company-desktop-computer Date: Wed, 28 Oct 2026 20:41:05 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E5=A2=9E=E5=8A=A0=E6=88=AA=E9=9D=A2=20?= =?UTF-8?q?Region=20=E7=BC=93=E5=AD=98=EF=BC=8C=E5=BC=80=E5=9B=BE=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E8=80=97=E6=97=B6=E9=99=8D=2056%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一次开图要做 5641 次截面恢复(22.4 s),其中大量内容重复, 而每次恢复都是"写临时 SAT 文件 + 从盘 acisIn + 删文件",单次 2.7~5.5 ms, 还要过一次杀软。本提交加一级母体缓存,命中直接 clone 返回。 新增 CoreLibrary/SectionRegionCache.{h,cpp},三个通道按 key 的正确性强度分开: - kParam:纯参数 hash,用于 AecDbEwFitting::GetSection 的两个重载 (key = dD/dt/HLR 标志 或 W/H/dt/HLR 标志,同参数必同几何) - kAttr:类名 + spec XData(排除编号,并跳过 Id/点/向量/矩阵/二进制类型), 只给 AecDbEwPipe 用。AecDbEwPipeBase 是很多类的基类,其余派生类实例间的 区别就是截面,不满足"同数据必同截面",所以一律走内容 hash - kText:SAT 文本内容 hash,用于 RegionAcisInDwgInFields 的其它类 与 CoreTools::StringToRegion 的两个重载 约定:母体由缓存持有、永不外泄,Acquire/Put 都返回 clone,调用方拥有返回值; 母体不入库;每条通道 4096 条上限(AEC_SECTION_CACHE_MAX 可调),超出按 FIFO 淘汰; AEC_SECTION_CACHE=0 可整体关闭做 A/B;卸载时 ClearAll() 释放,避免 DLL 析构顺序问题。 关键发现:SAT 文本第 2 行带存盘时间戳 16 Autodesk AutoCAD 19 ASM 219.0.0.5600 NT 24 Thu Oct 22 10:19:20 2026 同一几何存盘时间不同,整份文本就不同(采样里两份 2596 字节的文本只差 10:19:20 与 10:19:16),这正是内容 hash 命中率只有 54% 的原因,与截面无关。 因此新增 HashSatText():算 hash 时跳过第 2 行,仅当该行含 "Autodesk" 时才跳过。 计数器也改用同一口径,避免两套统计互相矛盾。 实测(big1.dwg / AutoCAD 2020 / R230 Debug,AEC_SECTION_CACHE 关 vs 开): 开图墙钟 108~113 s -> 98 s 截面恢复总耗时 22.4 s -> 9.87 s(-56%) StringToRegion 14.05 s -> 7.73 s RegionAcisIn 8.38 s -> 2.14 s 临时文件 6773 次/9.34MB -> 2875 次/3.61MB 命中率 text 65.5% / attr 84.1% / param 88.8% clone 开销 命中 58us、未命中 73us(合计 0.37 s,可忽略) 顺带: - tools/run_acad_perf.ps1 增加 -CacheOff 开关,用于同一构建上的 A/B 对比 - SectionPerfCounter 增加 AppendReport(),让缓存统计也写进同一份报告文件 - 诊断口子 AEC_SAT_SAMPLE=n 可导出未命中的 SAT 原文,用于定位这类差异 验证:R230 Debug 编译通过并部署;用脚本自动开图各跑一次(缓存关/开), 数据已写入 pipe-section-region-performance-plan.md。 尚未做:R190 编译(本机缺 v100 工具集)、Release 构建的收益复测、 开图期间 1140 次 RegionAcisout 回写(与本缓存无关,需单独立项)。 --- SourceCode/Code2026/CoreCommand/CoreTools.cpp | 35 ++ SourceCode/Code2026/CoreCommand/EarthWorm.cpp | 3 + .../Code2026/CoreLibrary/AecDbEwFitting.cpp | 45 +++ .../Code2026/CoreLibrary/AecDbEwPipeBase.cpp | 26 ++ .../CoreLibrary/SectionPerfCounter.cpp | 17 +- .../CoreLibrary/SectionRegionCache.cpp | 344 ++++++++++++++++++ .../Code2026/CoreLibrary/cmakelists.txt | 1 + SourceCode/Code2026/inc/SectionPerfCounter.h | 3 + SourceCode/Code2026/inc/SectionRegionCache.h | 65 ++++ pipe-section-region-performance-plan.md | 52 +++ tools/run_acad_perf.ps1 | 5 +- 11 files changed, 594 insertions(+), 2 deletions(-) create mode 100644 SourceCode/Code2026/CoreLibrary/SectionRegionCache.cpp create mode 100644 SourceCode/Code2026/inc/SectionRegionCache.h diff --git a/SourceCode/Code2026/CoreCommand/CoreTools.cpp b/SourceCode/Code2026/CoreCommand/CoreTools.cpp index 91fb2a9..51782a9 100644 --- a/SourceCode/Code2026/CoreCommand/CoreTools.cpp +++ b/SourceCode/Code2026/CoreCommand/CoreTools.cpp @@ -21,6 +21,7 @@ #include "gpc.h" #include "ImpMD5.h" #include "SectionPerfCounter.h" +#include "SectionRegionCache.h" #include "breptools.h" #include "DimScaleZoneDlg.h" #include "simpleProperty.h" @@ -3523,6 +3524,11 @@ bool CoreTools::CurveToString(const AcDbCurve * pCurve, CString & sScr2) AcDbRegion* CoreTools::StringToRegion(const CString & sRegionScrp) { + // 截面缓存:SAT 文本内容 hash(字节相同即几何相同,对所有实体类都成立) + SectionRegionCache::Channel chCache = SectionRegionCache::kText; + unsigned long long nCacheKey = 0; + const bool bCacheOn = SectionRegionCache::Enabled(); + // Phase 0 计数:截面恢复耗时/次数/文本唯一性,见 pipe-section-region-performance-plan.md SectionPerf::Scope perfScope("CoreTools::StringToRegion"); @@ -3546,6 +3552,14 @@ AcDbRegion* CoreTools::StringToRegion(const CString & sRegionScrp) CString sFullPath = MakeTempSatPath(); if (sFullPath.IsEmpty()) return NULL; + if (bCacheOn) + { + nCacheKey = SectionRegionCache::HashSatText(sNewSrc.c_str(), sNewSrc.size()); + AcDbRegion* pHit = SectionRegionCache::Acquire(chCache, nCacheKey, sNewSrc.c_str(), sNewSrc.size()); + if (pHit != NULL) + return pHit; + } + SectionPerf::NoteRegionText(sNewSrc.c_str(), sNewSrc.size()); if (file.Open(sFullPath, CFile::modeCreate | CFile::modeWrite)) @@ -3586,11 +3600,20 @@ AcDbRegion* CoreTools::StringToRegion(const CString & sRegionScrp) DELETE_PTR(arBodyPtrs[i]); ::DeleteFile(sFullPath); + + if (bCacheOn) + return SectionRegionCache::Put(chCache, nCacheKey, pReg); + return pReg; } AcDbRegion *CoreTools::StringToRegion(Adesk::UInt64 nLen, char *pszReg) { + // 截面缓存(同 CString 重载) + SectionRegionCache::Channel chCache = SectionRegionCache::kText; + unsigned long long nCacheKey = 0; + const bool bCacheOn = SectionRegionCache::Enabled(); + SectionPerf::Scope perfScope("CoreTools::StringToRegion(buf)"); std::string sNewSrc; @@ -3610,6 +3633,14 @@ AcDbRegion *CoreTools::StringToRegion(Adesk::UInt64 nLen, char *pszReg) CString sFullPath = MakeTempSatPath(); if (sFullPath.IsEmpty()) return NULL; + if (bCacheOn) + { + nCacheKey = SectionRegionCache::HashSatText(sNewSrc.c_str(), sNewSrc.size()); + AcDbRegion* pHit = SectionRegionCache::Acquire(chCache, nCacheKey, sNewSrc.c_str(), sNewSrc.size()); + if (pHit != NULL) + return pHit; + } + SectionPerf::NoteRegionText(sNewSrc.c_str(), sNewSrc.size()); CFile file; @@ -3650,6 +3681,10 @@ AcDbRegion *CoreTools::StringToRegion(Adesk::UInt64 nLen, char *pszReg) DELETE_PTR(arBodyPtrs[i]); ::DeleteFile(sFullPath); + + if (bCacheOn) + return SectionRegionCache::Put(chCache, nCacheKey, pReg); + return pReg; } diff --git a/SourceCode/Code2026/CoreCommand/EarthWorm.cpp b/SourceCode/Code2026/CoreCommand/EarthWorm.cpp index c9c14f1..d22bc82 100644 --- a/SourceCode/Code2026/CoreCommand/EarthWorm.cpp +++ b/SourceCode/Code2026/CoreCommand/EarthWorm.cpp @@ -20,6 +20,7 @@ #include "AecDbDimScaleZone.h" #include "AecDbJoggedCamera.h" #include "SectionPerfCounter.h" +#include "SectionRegionCache.h" #include "breptools.h" #include "HlrCutOption.h" @@ -474,6 +475,7 @@ acrxEntryPoint(AcRx::AppMsgCode msg, void* pkt) // && g_lockState == BIT_SUCCESS) // 验证是否成功,都应该自行注销 UnloadApplication(); + SectionRegionCache::ClearAll(); //else // return AcRx::kRetError; #if ARX > 18 @@ -549,6 +551,7 @@ KTDocReactor *g_pDocReactor(NULL); void envierpf_dump() { SectionPerf::Dump(); + SectionRegionCache::Dump(); } void envierpf_reset() diff --git a/SourceCode/Code2026/CoreLibrary/AecDbEwFitting.cpp b/SourceCode/Code2026/CoreLibrary/AecDbEwFitting.cpp index 908bae9..e03b666 100644 --- a/SourceCode/Code2026/CoreLibrary/AecDbEwFitting.cpp +++ b/SourceCode/Code2026/CoreLibrary/AecDbEwFitting.cpp @@ -1,5 +1,7 @@ #include "stdafx.h" #include "AecDbEwFitting.h" +#include "SectionPerfCounter.h" +#include "SectionRegionCache.h" #include "dbgrip.h" #include "AecGripCback.h" #include "MFCDbDwgFiler.h" @@ -11986,6 +11988,24 @@ void AecDbEwFitting::DrawInfCap3d(AcGiDraw &dc) const //设置截面为管道外径和壁厚 AcDbRegion* AecDbEwFitting::GetSection(double dD,double dt) const { + // 截面缓存:GetSection 是 (dD, dt, HLR标志) 的纯函数,同参数必同几何, + // 且它在绘制期被反复调用(createFromCurves 单次约 3.6 ms)。 + SectionPerf::Scope perfScope("Fitting::GetSection(D,dt)"); + SectionRegionCache::Channel chCache = SectionRegionCache::kParam; + unsigned long long nCacheKey = 0; + const bool bCacheOn = SectionRegionCache::Enabled(); + if (bCacheOn) + { + const bool bHLR = isHLRCalc(this); + nCacheKey = SectionRegionCache::HashBytes(&dD, sizeof(dD)); + nCacheKey = SectionRegionCache::HashBytes(&dt, sizeof(dt)) ^ (nCacheKey << 1); + nCacheKey ^= bHLR ? 0x9E3779B97F4A7C15ULL : 0ULL; + + AcDbRegion* pHit = SectionRegionCache::Acquire(chCache, nCacheKey); + if (pHit != NULL) + return pHit; + } + assertReadEnabled(); AcDbVoidPtrArray arCurPtrs,arRegnPtrs; @@ -12025,11 +12045,33 @@ AcDbRegion* AecDbEwFitting::GetSection(double dD,double dt) const } FreeBuffer(0,arCurPtrs); } + + if (bCacheOn) + return SectionRegionCache::Put(chCache, nCacheKey, pRegion); + return pRegion; } AcDbRegion * AecDbEwFitting::GetSection(double W, double H, double dt) const { + // 截面缓存:矩形截面同理,key = (W, H, dt, HLR标志) + SectionPerf::Scope perfScope("Fitting::GetSection(W,H,dt)"); + SectionRegionCache::Channel chCache = SectionRegionCache::kParam; + unsigned long long nCacheKey = 0; + const bool bCacheOn = SectionRegionCache::Enabled(); + if (bCacheOn) + { + const bool bHLR = isHLRCalc(this); + nCacheKey = SectionRegionCache::HashBytes(&W, sizeof(W)); + nCacheKey = SectionRegionCache::HashBytes(&H, sizeof(H)) ^ (nCacheKey << 1); + nCacheKey = SectionRegionCache::HashBytes(&dt, sizeof(dt)) ^ (nCacheKey << 1); + nCacheKey ^= bHLR ? 0x9E3779B97F4A7C15ULL : 0ULL; + + AcDbRegion* pHit = SectionRegionCache::Acquire(chCache, nCacheKey); + if (pHit != NULL) + return pHit; + } + double x(W * .5), y(H * .5); AcDbVoidPtrArray arCurPtrs, arRegnPtrs; @@ -12069,6 +12111,9 @@ AcDbRegion * AecDbEwFitting::GetSection(double W, double H, double dt) const } FreeBuffer(0,arCurPtrs); + if (bCacheOn) + return SectionRegionCache::Put(chCache, nCacheKey, pRegion); + return pRegion; } diff --git a/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp b/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp index 8607c24..a5d7c67 100644 --- a/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp +++ b/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "encryption.h" #include "AecDbEwPipeBase.h" +#include "AecDbEwPipe.h" #include "KTCADUtilityEx.h" #include "MFCDbDwgFiler.h" #include "AecDb3dRoadAxisLine.h" @@ -19,6 +20,7 @@ #include "AcGiViewDraw.h" #include "AcDbX3dSolid.h" #include "SectionPerfCounter.h" +#include "SectionRegionCache.h" #ifndef EW_EARTHWORMINIT #define EW_EARTHWORMINIT _T("EW_EARTHWORMINIT") @@ -823,6 +825,26 @@ AcDbRegion* AecDbEwPipeBase::RegionAcisInDwgInFields(AcDbDwgFiler* pFiler) // 同一份 SAT 文本被恢复多少次:唯一 hash 数 / 模拟命中率 SectionPerf::NoteRegionText(pszTxt, (size_t)nLen); + // 截面缓存:管道用属性 key(同 spec 数据必同截面),其余类用 SAT 文本内容 hash。 + // 命中直接返回 clone,跳过"写临时文件 + acisIn + 删文件"整段。 + SectionRegionCache::Channel cacheChannel = SectionRegionCache::kText; + unsigned long long nCacheKey = 0; + bool bUseAttrKey = false; + if (SectionRegionCache::Enabled()) + { + bUseAttrKey = (isA() == AecDbEwPipe::desc()); + cacheChannel = bUseAttrKey ? SectionRegionCache::kAttr : SectionRegionCache::kText; + nCacheKey = bUseAttrKey + ? SectionRegionCache::BuildSpecAttrHash(m_mapSpecXData, isA()->name()) + : SectionRegionCache::HashSatText(pszTxt, (size_t)nLen); + + AcDbRegion* pHit = SectionRegionCache::Acquire(cacheChannel, nCacheKey, pszTxt, (size_t)nLen); + if (pHit != NULL) + { + delete[] pszTxt; + return pHit; + } + } // Phase 0:属性 key 命中率(只统计,不做缓存)。 // 管道/管件没有自定义截面,同一套 spec 数据必然得到同一截面, // 所以这里把 "类名 + 除编号外的全部 spec XData" 当作 key。 @@ -933,6 +955,10 @@ AcDbRegion* AecDbEwPipeBase::RegionAcisInDwgInFields(AcDbDwgFiler* pFiler) acutPrintf(_T("\nAecDbEwPipeBase: 截面 acisIn 未返回 region,len=%u。"),nLen); ::DeleteFile(sFullPath); + + if (SectionRegionCache::Enabled()) + return SectionRegionCache::Put(cacheChannel, nCacheKey, pRegion); + return pRegion; } diff --git a/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp b/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp index 6c94a46..3f3fa60 100644 --- a/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp +++ b/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "SectionPerfCounter.h" +#include "SectionRegionCache.h" #include #include @@ -179,7 +180,7 @@ namespace SectionPerf ++s.nRegionTexts; - const unsigned long long h = Hash64(pszTxt, nLen); + const unsigned long long h = SectionRegionCache::HashSatText(pszTxt, nLen); if (s.hashSeen.find(h) != s.hashSeen.end()) { ++s.nSimulatedHits; @@ -343,6 +344,20 @@ namespace SectionPerf } } + void AppendReport(const char* pszText) + { + if (pszText == NULL) + return; + const std::string sPath = TempReportPath(); + FILE* fp = NULL; + if (fopen_s(&fp, sPath.c_str(), "a") == 0 && fp != NULL) + { + fprintf(fp, "%s", pszText); + if (pszText[0] != 0 && pszText[strlen(pszText)-1] != '\n') + fprintf(fp, "%s", "\n"); + fclose(fp); + } + } void Reset() { Stats& s = GetStats(); diff --git a/SourceCode/Code2026/CoreLibrary/SectionRegionCache.cpp b/SourceCode/Code2026/CoreLibrary/SectionRegionCache.cpp new file mode 100644 index 0000000..0334f09 --- /dev/null +++ b/SourceCode/Code2026/CoreLibrary/SectionRegionCache.cpp @@ -0,0 +1,344 @@ +#include "stdafx.h" +#include "SectionRegionCache.h" +#include "AecDbObjXDataMap.h" +#include "SectionPerfCounter.h" + +#include +#include +#include +#include +#include + +namespace +{ + // 每个通道的条目上限。实测大图纸唯一 SAT 文本约 2600 条,2048 会把热条目挤掉, + // 所以默认给 4096;可用 AEC_SECTION_CACHE_MAX 覆盖(内存吃紧时调小)。 + const size_t kDefaultMaxEntries = 4096; + size_t GetMaxEntries() + { + static size_t s_nMax = 0; + if (s_nMax == 0) + { + const char* psz = ::getenv("AEC_SECTION_CACHE_MAX"); + s_nMax = (psz != NULL) ? (size_t)atoi(psz) : 0; + if (s_nMax < 16) + s_nMax = kDefaultMaxEntries; + } + return s_nMax; + } + + static const char* kChannelName[] = { "text", "attr", "param" }; + + struct ChannelData + { + ChannelData() : nHits(0), nMisses(0), nEvicted(0) {} + + std::map map; + std::list fifo; + unsigned long long nHits; + unsigned long long nMisses; + unsigned long long nEvicted; + + void ReleaseAll() + { + for (std::map::iterator it = map.begin(); it != map.end(); ++it) + { + AcDbRegion* p = it->second; + if (p != NULL) + delete p; + } + map.clear(); + fifo.clear(); + nEvicted = 0; + } + }; + + struct State + { + State() : bInited(false), bEnabled(true) {} + + bool bInited; + bool bEnabled; + ChannelData channels[SectionRegionCache::kChannelCount]; + CRITICAL_SECTION cs; + }; + + // 函数内 static:CoreLibrary 是静态库且只被 CoreCommand 链接, + // 所以 CoreCommand.arx 内只有这一份缓存。 + // 注意:不在析构里 delete 母体(DLL 卸载时 modeler 可能已先关, + // 析构顺序不安全);由 ClearAll() 在 kUnloadAppMsg 里显式释放。 + State& GetState() + { + static State s_state; + if (!s_state.bInited) + { + ::InitializeCriticalSection(&s_state.cs); + const char* psz = ::getenv("AEC_SECTION_CACHE"); + s_state.bEnabled = !(psz != NULL && psz[0] == '0'); + s_state.bInited = true; + } + return s_state; + } + + struct AutoLock + { + explicit AutoLock(CRITICAL_SECTION* pcs) : m_pcs(pcs) { ::EnterCriticalSection(m_pcs); } + ~AutoLock() { ::LeaveCriticalSection(m_pcs); } + CRITICAL_SECTION* m_pcs; + }; + + unsigned long long HashAdd(unsigned long long h, const void* pData, size_t nLen) + { + if (pData == NULL || nLen == 0) + return h; + const unsigned char* p = (const unsigned char*)pData; + for (size_t i = 0; i < nLen; ++i) + { + h ^= (unsigned long long)p[i]; + h *= 1099511628211ULL; + } + return h; + } +} + +namespace { + // 诊断用:AEC_SAT_SAMPLE=n 时把前 n 个"未命中的 SAT 文本"导出,便于比对差异来源 + void DumpSample(const void* pData, size_t nLen) + { + static int s_nRemain = -1; + if (s_nRemain < 0) + { + const char* psz = ::getenv("AEC_SAT_SAMPLE"); + s_nRemain = (psz != NULL) ? atoi(psz) : 0; + } + if (s_nRemain <= 0) + return; + --s_nRemain; + + char szTmp[MAX_PATH + 1]; + ::memset(szTmp, 0, sizeof(szTmp)); + if (::GetTempPathA(MAX_PATH, szTmp) == 0) + return; + std::string sPath(szTmp); + sPath += "aec_sat_samples.txt"; + + FILE* fp = NULL; + if (fopen_s(&fp, sPath.c_str(), "ab") == 0 && fp != NULL) + { + fprintf(fp, "===== SAMPLE len=%llu =====\n", (unsigned long long)nLen); + fwrite(pData, 1, nLen, fp); + fprintf(fp, "\n===== END =====\n"); + fclose(fp); + } + } +} +namespace SectionRegionCache +{ + bool Enabled() + { + return GetState().bEnabled; + } + + unsigned long long HashBytes(const void* pData, size_t nLen) + { + return HashAdd(1469598103934665603ULL, pData, nLen); + } + + unsigned long long HashSatText(const void* pData, size_t nLen) + { + if (pData == NULL || nLen == 0) + return 1469598103934665603ULL; + + const char* p = (const char*)pData; + const char* pEnd = p + nLen; + + // 找第 2 行(SAT 的 16 号记录:Autodesk 版本 + 存盘时间戳) + const char* pNL1 = (const char*)memchr(p, '\n', nLen); + if (pNL1 != NULL && pNL1 + 1 < pEnd) + { + const char* pLine2 = pNL1 + 1; + const char* pNL2 = (const char*)memchr(pLine2, '\n', (size_t)(pEnd - pLine2)); + if (pNL2 != NULL) + { + // 只在第 2 行确实是 Autodesk 头时才跳过,避免误伤别的格式 + bool bHeader = false; + for (const char* q = pLine2; q + 9 <= pNL2; ++q) + { + if (_strnicmp(q, "Autodesk", 8) == 0) + { + bHeader = true; + break; + } + } + if (bHeader) + { + unsigned long long h = HashBytes(p, (size_t)(pNL1 - p)); + return HashAdd(h, pNL2 + 1, (size_t)(pEnd - (pNL2 + 1))); + } + } + } + + return HashBytes(pData, nLen); + } + + unsigned long long BuildSpecAttrHash(const AecDbObjXDataMap& map, const TCHAR* pszClass) + { + unsigned long long h = 1469598103934665603ULL; + + // 类名参与:不同实体类即使数据相同也绝不能共用截面 + CStringA sCls(pszClass != NULL ? pszClass : _T("?")); + h = HashAdd(h, (LPCSTR)sCls, strlen((LPCSTR)sCls)); + + // std::map 按 key 排序,遍历顺序稳定 + for (AecDbObjXDataMap::It it = const_cast(map).Begin(); + it != const_cast(map).End(); ++it) + { + const CString& sKey = it->first; + const AecXData& xd = it->second; + + // 编号每根管子唯一,必须排除 + if (sKey.CompareNoCase(_T("No.")) == 0) + continue; + + h = HashAdd(h, sKey.GetString(), (size_t)sKey.GetLength() * sizeof(TCHAR)); + h = HashAdd(h, &xd.nVt, sizeof(xd.nVt)); + + switch (xd.nVt) + { + case AecXData::kTypeShort: + h = HashAdd(h, &xd.iVal, sizeof(xd.iVal)); + break; + case AecXData::kTypeInt: + case AecXData::kTypeInt64: + h = HashAdd(h, &xd.nVal, sizeof(xd.nVal)); + break; + case AecXData::kTypeDouble: + h = HashAdd(h, &xd.dVal[0], sizeof(double)); + break; + case AecXData::kTypeString: + if (xd.pszVal != NULL) + h = HashAdd(h, xd.pszVal, _tcslen(xd.pszVal) * sizeof(TCHAR)); + break; + default: + // Id / 点 / 向量 / 矩阵 / 二进制:与截面无关或随实体变化,跳过 + break; + } + } + return h; + } + + AcDbRegion* Acquire(Channel ch, unsigned long long nKey, const void* pSampleData, size_t nSampleLen) + { + State& s = GetState(); + if (!s.bEnabled || ch < 0 || ch >= kChannelCount) + return NULL; + + AutoLock lock(&s.cs); + ChannelData& cd = s.channels[ch]; + + std::map::iterator it = cd.map.find(nKey); + if (it == cd.map.end() || it->second == NULL) + { + ++cd.nMisses; + if (ch == SectionRegionCache::kText && pSampleData != NULL) + DumpSample(pSampleData, nSampleLen); + return NULL; + } + + ++cd.nHits; + SectionPerf::Scope hitScope("cache.hit.clone"); + return (AcDbRegion*)it->second->clone(); + } + + AcDbRegion* Put(Channel ch, unsigned long long nKey, AcDbRegion* pMaster) + { + State& s = GetState(); + if (pMaster == NULL) + return NULL; + if (!s.bEnabled || ch < 0 || ch >= kChannelCount) + return pMaster; // 关闭时原样返回,调用方照旧拥有 + + AcDbRegion* pForCaller = NULL; + { + SectionPerf::Scope putScope("cache.miss.clone"); + pForCaller = (AcDbRegion*)pMaster->clone(); + } + + AutoLock lock(&s.cs); + ChannelData& cd = s.channels[ch]; + + std::map::iterator it = cd.map.find(nKey); + if (it != cd.map.end()) + { + // 已有同 key(并发或重复入库):丢弃新的母体 + delete pMaster; + return pForCaller; + } + + // 超出上限先淘汰最旧的 + while (cd.map.size() >= GetMaxEntries() && !cd.fifo.empty()) + { + const unsigned long long nOld = cd.fifo.front(); + cd.fifo.pop_front(); + std::map::iterator itOld = cd.map.find(nOld); + if (itOld != cd.map.end()) + { + AcDbRegion* pOld = itOld->second; + cd.map.erase(itOld); + delete pOld; + ++cd.nEvicted; + } + } + + cd.map[nKey] = pMaster; + cd.fifo.push_back(nKey); + return pForCaller; + } + + void Dump() + { + State& s = GetState(); + std::string out; + char szLine[512]; + + out += "\n---- section region cache ----\n"; + out += " channel entries hits misses hit_rate evicted\n"; + for (int i = 0; i < (int)kChannelCount; ++i) + { + const ChannelData& cd = s.channels[i]; + const unsigned long long nTotal = cd.nHits + cd.nMisses; + const double rate = nTotal ? (100.0 * (double)cd.nHits / (double)nTotal) : 0.0; + sprintf_s(szLine, sizeof(szLine), " %-9s %-9llu %-7llu %-7llu %7.1f%% %llu\n", + kChannelName[i], (unsigned long long)cd.map.size(), + cd.nHits, cd.nMisses, rate, cd.nEvicted); + out += szLine; + } + + const char* psz = out.c_str(); + while (*psz != '\0') + { + const char* pszEnd = strchr(psz, '\n'); + const size_t nSeg = (pszEnd != NULL) ? (size_t)(pszEnd - psz) : strlen(psz); + if (nSeg > 0 && nSeg < sizeof(szLine)) + { + memcpy(szLine, psz, nSeg); + szLine[nSeg] = '\0'; + acutPrintf(_T("\n%hs"), szLine); + } + if (pszEnd == NULL) + break; + psz = pszEnd + 1; + } + + // 同时写进与 SectionPerf 相同的报告文件,便于脚本回收 + SectionPerf::AppendReport(out.c_str()); + } + + void ClearAll() + { + State& s = GetState(); + AutoLock lock(&s.cs); + for (int i = 0; i < (int)kChannelCount; ++i) + s.channels[i].ReleaseAll(); + } +} diff --git a/SourceCode/Code2026/CoreLibrary/cmakelists.txt b/SourceCode/Code2026/CoreLibrary/cmakelists.txt index 3494639..d33ffca 100644 --- a/SourceCode/Code2026/CoreLibrary/cmakelists.txt +++ b/SourceCode/Code2026/CoreLibrary/cmakelists.txt @@ -12,6 +12,7 @@ set(SOURCES "AecComplexCurveDraw.cpp" "AecCoverBooleanOper.cpp" "SectionPerfCounter.cpp" + "SectionRegionCache.cpp" "AecDb3dRoad.cpp" "AecDb3dRoadAxisLine.cpp" "AecDb3dRoadFitting.cpp" diff --git a/SourceCode/Code2026/inc/SectionPerfCounter.h b/SourceCode/Code2026/inc/SectionPerfCounter.h index cbd9463..fe7bcfa 100644 --- a/SourceCode/Code2026/inc/SectionPerfCounter.h +++ b/SourceCode/Code2026/inc/SectionPerfCounter.h @@ -76,6 +76,9 @@ namespace SectionPerf // 打印 + 写文件 void Dump(); + // 只追加文本到 %TEMP%ec_section_perf.txt(供其它模块复用同一份报告) + void AppendReport(const char* pszText); + // 清零 void Reset(); } diff --git a/SourceCode/Code2026/inc/SectionRegionCache.h b/SourceCode/Code2026/inc/SectionRegionCache.h new file mode 100644 index 0000000..040f35b --- /dev/null +++ b/SourceCode/Code2026/inc/SectionRegionCache.h @@ -0,0 +1,65 @@ +// SectionRegionCache.h —— 截面(Region)母体缓存 +// +// 背景(见 pipe-section-region-performance-plan.md): +// 一次开图实测要做 5641 次截面恢复(22.9 s),而其中大量内容重复。 +// 恢复一次 = 写临时 SAT 文件 + 从盘 acisIn + 删文件 + 杀软扫描,单次 2.7~5.5 ms。 +// +// 三个通道,按"key 的正确性强度"分开用: +// kParam —— 纯参数函数(如 AecDbEwFitting::GetSection(dD, dt)),同参数必同几何,最强 +// kAttr —— 实体属性 key(仅用于"同数据必然同截面"的类,目前只有 AecDbEwPipe) +// kText —— SAT 文本内容 hash,对所有类都成立(字节相同即几何相同) +// +// 约定: +// - 缓存持有"母体",永不外泄;Acquire/Put 都返回 clone,调用方拥有返回值。 +// - 母体不入库、不参与绘制,只在缓存里待着。 +// - 每次访问都 clone(一次区域深拷贝,远低于一次文件往返)。 +// - 环境变量 AEC_SECTION_CACHE=0 可整体关闭(用于 A/B 对比)。 + +#ifndef SECTION_REGION_CACHE_H +#define SECTION_REGION_CACHE_H + +#include + +class AcDbRegion; +class AecDbObjXDataMap; + +namespace SectionRegionCache +{ + enum Channel + { + kText = 0, // SAT 文本内容 hash + kAttr = 1, // 实体属性 hash(仅管道) + kParam = 2, // 纯参数 hash(管件 GetSection) + kChannelCount = 3 + }; + + // 默认开启;AEC_SECTION_CACHE=0 关闭 + bool Enabled(); + + // FNV-1a 64 位 + unsigned long long HashBytes(const void* pData, size_t nLen); + + // SAT 文本哈希:跳过 ACIS 头里的时间戳行。 + // 实测同一几何存盘时间不同会让整份文本不同,直接用内容 hash 会几乎全部判成"唯一"。 + unsigned long long HashSatText(const void* pData, size_t nLen); + + // 属性 key:类名 + 全部 spec XData,排除编号("No."), + // 并跳过 Id / 点 / 向量 / 矩阵 / 二进制类型(与截面无关或随实体变化)。 + unsigned long long BuildSpecAttrHash(const AecDbObjXDataMap& map, const TCHAR* pszClass); + + // 命中返回 clone;未命中返回 NULL + // pSampleData/nSampleLen 仅在诊断(AEC_SAT_SAMPLE)时用于导出未命中的原文,可传 NULL + AcDbRegion* Acquire(Channel ch, unsigned long long nKey, + const void* pSampleData = NULL, size_t nSampleLen = 0); + + // 接管 pMaster 所有权并返回它的 clone(未命中后的入库路径) + AcDbRegion* Put(Channel ch, unsigned long long nKey, AcDbRegion* pMaster); + + // 打印各通道命中率与条目数 + void Dump(); + + // 清空并释放全部母体(卸载时调用,避免 DLL 析构顺序问题) + void ClearAll(); +} + +#endif // SECTION_REGION_CACHE_H diff --git a/pipe-section-region-performance-plan.md b/pipe-section-region-performance-plan.md index 19db499..a69142b 100644 --- a/pipe-section-region-performance-plan.md +++ b/pipe-section-region-performance-plan.md @@ -232,6 +232,58 @@ TempFiles=6773 TempBytes=9,341,125 WriteBacks=1140 - 母体不被任何调用方 `delete`。 - miss 路径的 `DeleteFile` 逻辑保持原样(含失败分支)。 +### Phase 1 实施结果(2026-10-28,big1.dwg / R230 Debug) + +已实现 `CoreLibrary/SectionRegionCache.{h,cpp}`,三个通道: + +| 通道 | key | 用在哪 | +| --- | --- | --- | +| `kParam` | 纯参数 hash(dD/dt/HLR 标志) | `AecDbEwFitting::GetSection` 两个重载 | +| `kAttr` | 类名 + spec XData | 仅 `AecDbEwPipe`(其余派生类实例间差异就是截面,不适用) | +| `kText` | SAT 文本内容 hash | `AecDbEwPipeBase::RegionAcisInDwgInFields` 的其它类 + `CoreTools::StringToRegion` 两个重载 | + +命中返回 `clone()`,未命中走原逻辑后 `Put()` 入库;母体不外泄;`AEC_SECTION_CACHE=0` 可整体关闭做 A/B。 + +#### 关键发现:SAT 文本第 2 行带存盘时间戳 + +导出未命中的 SAT 文本后发现:同一几何存盘时间不同,整份文本就不同—— + +``` +700 56 1 0 +16 Autodesk AutoCAD 19 ASM 219.0.0.5600 NT 24 Thu Oct 22 10:19:20 2026 ← 时间戳 +1 9.9999999999999995e-007 1e-010 +``` + +采样里两份 2596 字节、内容完全一致的文本,只差 `10:19:20` 与 `10:19:16`。 +这就是内容 hash 命中率只有 54 % 的真正原因,与"截面是否相同"无关。 + +修复:`SectionRegionCache::HashSatText()` 在计算 hash 时跳过第 2 行 +(仅当该行含 "Autodesk" 时才跳过,避免误伤其它格式)。计数器改用同一口径。 + +#### 实测对比 + +| 指标 | 基线(`AEC_SECTION_CACHE=0`) | 启用缓存 + 时间戳归一化 | +| --- | ---: | ---: | +| 开图墙钟 | 108~113 s | **98 s** | +| 截面恢复总耗时 | 22.4 s | **9.87 s**(−56 %) | +| `CoreTools::StringToRegion` | 14.05 s | 7.73 s | +| `EwPipe::RegionAcisIn` | 8.38 s | 2.14 s | +| `Fitting::GetSection` | 0.13 s | 0.035 s | +| 临时文件 | 6773 次 / 9.34 MB | **2875 次 / 3.61 MB** | +| 缓存命中 | — | text 65.5 %、attr 84.1 %、param 88.8 % | +| clone 开销 | — | 命中 58 µs、未命中 73 µs(合计仅 0.37 s) | + +clone 成本可以忽略,说明"每次无论命中与否都发 clone"这个保守设计没有代价。 + +#### 仍未解决 + +1. `EwPipe::RegionAcisout` 仍是 **1140 次 / 2.38 s**,开图期间把几何又序列化回 SAT, + 与缓存无关,应单独立项。 +2. `Fitting::GetSection` 在**开图**阶段只有 232 次调用(0.13 s),收益很小; + 它的价值可能在绘制/平移时,需要另一次针对"打开后操作"的测量才能确认。 +3. `StringToRegion` 仍有 1570 次未命中(唯一文本 1562),这部分是真实的不同截面, + 想再提升只能靠业务层的 key,暂不做。 + ### Phase 2:临时目录治理(可选,止血) Phase 1 之后残余落盘只剩"每个唯一截面一次",收益小;但如果现场仍被杀软拖慢,可做这一步: diff --git a/tools/run_acad_perf.ps1 b/tools/run_acad_perf.ps1 index 893c9db..3135eab 100644 --- a/tools/run_acad_perf.ps1 +++ b/tools/run_acad_perf.ps1 @@ -22,7 +22,8 @@ param( [string]$Acad = 'C:\Program Files\Autodesk\AutoCAD 2020\acad.exe', [string]$Profile = 'SHS-ENVI2026', [string]$Dwg = 'E:\big1.dwg', - [int] $TimeoutSec = 900 + [int] $TimeoutSec = 900, + [switch]$CacheOff ) $ErrorActionPreference = 'Continue' @@ -41,6 +42,8 @@ Write-Output ("report : " + $report) $existing = @(Get-Process acad -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id) $env:AEC_PERF_COUNTER = '1' +if ($CacheOff) { $env:AEC_SECTION_CACHE = '0' } else { $env:AEC_SECTION_CACHE = '1' } +Write-Output ("cache : " + $(if ($CacheOff) { 'OFF (A/B baseline)' } else { 'ON' })) $acadArgs = @('/p', $Profile, '/b', $script, $Dwg) $sw = [System.Diagnostics.Stopwatch]::StartNew() $p = Start-Process -FilePath $Acad -ArgumentList $acadArgs -PassThru