From 764e544db44f1d16b446ba4d813d84f50c33bebf Mon Sep 17 00:00:00 2001 From: company-desktop-computer Date: Wed, 28 Oct 2026 20:06:06 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E5=A2=9E=E5=8A=A0=E5=B1=9E=E6=80=A7=20?= =?UTF-8?q?key=20=E5=91=BD=E4=B8=AD=E7=8E=87=E7=BB=9F=E8=AE=A1=EF=BC=88Pha?= =?UTF-8?q?se=200=20=E8=A1=A5=E5=85=85=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 意图:内容 hash 只看到 54% 命中率,但业务上"同数据的管子/管件截面必然相同", 所以要用实体属性做 key 再量一次,决定 Phase 1 到底用哪种缓存键。 新增 SectionPerf::AttrKey(RAII): - 构造时按类名(isA()->name())初始化 hash 与统计桶,析构时记一次 - Add() 增量喂字节,调用方决定哪些字段参与 key - 统计按实体类分开(total / unique / hits / hit_rate) 接入 AecDbEwPipeBase::RegionAcisInDwgInFields:key = 类名 + 全部 spec XData, 但排除编号("No.",每根管子唯一)并跳过 Id / 点 / 向量 / 矩阵 / 二进制类型 (与截面无关或随实体变化)。只在 AEC_PERF_COUNTER=1 时构建,Release 无开销。 实测(big1.dwg,AutoCAD 2020 / R230 Debug,113 s 开图): 属性 key 合计 3082 次 / 2672 命中 = 86.7% AecDbEwPipe 1088 次 173 唯一 84.1% AecDbEwPipeBase 1172 次 3 唯一 99.7% (其余类 43%~99%) 内容 hash 54.0% 结论:属性 key 显著优于内容 hash,但只覆盖 3082/5641 次恢复 (另 2559 次走 CoreTools::StringToRegion,拿不到 spec 数据,只能用内容 hash)。 混合策略预计省 14.9 s / 22.6 s 恢复开销,约占 113 s 开图的 13%。 数据已写入 pipe-section-region-performance-plan.md,并标注一个未验证点: 属性 key 命中时两份 SAT 文本是否真的相同(决定 Phase 1 是否需要 textHash 兜底校验)。 验证:R230 Debug 编译通过并部署,用 tools/run_acad_perf.ps1 自动跑图取数。 --- .../Code2026/CoreLibrary/AecDbEwPipeBase.cpp | 42 ++++++++++ .../CoreLibrary/SectionPerfCounter.cpp | 79 +++++++++++++++++++ SourceCode/Code2026/inc/SectionPerfCounter.h | 22 ++++++ 3 files changed, 143 insertions(+) diff --git a/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp b/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp index f38d72d..8607c24 100644 --- a/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp +++ b/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp @@ -823,6 +823,48 @@ AcDbRegion* AecDbEwPipeBase::RegionAcisInDwgInFields(AcDbDwgFiler* pFiler) // 同一份 SAT 文本被恢复多少次:唯一 hash 数 / 模拟命中率 SectionPerf::NoteRegionText(pszTxt, (size_t)nLen); + // Phase 0:属性 key 命中率(只统计,不做缓存)。 + // 管道/管件没有自定义截面,同一套 spec 数据必然得到同一截面, + // 所以这里把 "类名 + 除编号外的全部 spec XData" 当作 key。 + // 跳过 Id / 点 / 向量 / 矩阵 / 二进制:它们与截面无关或随每根管子变化。 + if (SectionPerf::Enabled()) + { + SectionPerf::AttrKey attrKey(isA()->name()); + for (AecDbObjXDataMap::It it = m_mapSpecXData.Begin(); it != m_mapSpecXData.End(); ++it) + { + const CString& sKey = it->first; + const AecXData& xd = it->second; + + // 编号是每根管子唯一的,必须排除,否则命中率恒为 0 + if (sKey.CompareNoCase(_T("No.")) == 0) + continue; + + attrKey.Add(sKey.GetString(), (size_t)sKey.GetLength() * sizeof(TCHAR)); + attrKey.Add(&xd.nVt, sizeof(xd.nVt)); + + switch (xd.nVt) + { + case AecXData::kTypeShort: + attrKey.Add(&xd.iVal, sizeof(xd.iVal)); + break; + case AecXData::kTypeInt: + case AecXData::kTypeInt64: + attrKey.Add(&xd.nVal, sizeof(xd.nVal)); + break; + case AecXData::kTypeDouble: + attrKey.Add(&xd.dVal[0], sizeof(double)); + break; + case AecXData::kTypeString: + if (xd.pszVal != NULL) + attrKey.Add(xd.pszVal, _tcslen(xd.pszVal) * sizeof(TCHAR)); + break; + default: + break; + } + } + } + + if (sFileName.CompareNoCase(_T("errorfile")) == 0) { //原生写法 diff --git a/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp b/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp index 47b6d0c..6c94a46 100644 --- a/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp +++ b/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp @@ -23,6 +23,18 @@ namespace // 万一文本各不相同(说明缓存没意义)也不让内存无限涨 const size_t kMaxUniqueHash = 50000; + // 单个属性 key 的统计(按实体类分开看,管道与管件的命中率往往不一样) + struct KeyStat + { + KeyStat() : nTotal(0), nUnique(0), nHits(0), bOverflow(false) {} + + unsigned long long nTotal; + size_t nUnique; + unsigned long long nHits; + bool bOverflow; + std::set seen; + }; + struct Stats { Stats() @@ -49,6 +61,9 @@ namespace unsigned long long nTempFileBytes; // 临时文件累计字节 unsigned long long nWriteBacks; // Region -> SAT 文本 次数 + + // 属性 key:class 名 -> 唯一/命中统计 + std::map attrStats; }; // 故意用函数内 static:AGENTS.md 禁止文件级 static。 @@ -199,6 +214,53 @@ namespace SectionPerf (void)pszTag; } + AttrKey::AttrKey(const TCHAR* pszClass) + : m_nHash(1469598103934665603ULL), m_bActive(GetStats().bEnabled) + { + if (!m_bActive) + return; + // CStringA 按当前代码页做宽窄转换;非 UNICODE 构建下直接构造 + CStringA sCls(pszClass != NULL ? pszClass : _T("?")); + m_sClass = (LPCSTR)sCls; + Add(m_sClass.data(), m_sClass.size()); + } + + AttrKey::~AttrKey() + { + if (!m_bActive) + return; + + // 与 NoteRegionText 相同口径:首次出现记唯一,重复出现记一次“命中” + KeyStat& st = GetStats().attrStats[m_sClass]; + ++st.nTotal; + if (st.seen.find(m_nHash) != st.seen.end()) + { + ++st.nHits; + return; + } + if (st.seen.size() < kMaxUniqueHash) + { + st.seen.insert(m_nHash); + ++st.nUnique; + } + else + { + st.bOverflow = true; + } + } + + void AttrKey::Add(const void* pData, size_t nLen) + { + if (!m_bActive || pData == NULL || nLen == 0) + return; + const unsigned char* p = (const unsigned char*)pData; + for (size_t i = 0; i < nLen; ++i) + { + m_nHash ^= (unsigned long long)p[i]; + m_nHash *= 1099511628211ULL; + } + } + void Dump() { Stats& s = GetStats(); @@ -225,6 +287,22 @@ namespace SectionPerf s.nTempFiles, s.nTempFileBytes, s.nWriteBacks); out += szLine; + if (!s.attrStats.empty()) + { + out += "---- attr key (by class, would-be cache hit rate) ----\n"; + out += " total unique hits hit_rate class\n"; + for (std::map::const_iterator it = s.attrStats.begin(); + it != s.attrStats.end(); ++it) + { + const KeyStat& k = it->second; + const double rate = k.nTotal ? (100.0 * (double)k.nHits / (double)k.nTotal) : 0.0; + sprintf_s(szLine, sizeof(szLine), " %-8llu %-9llu %-6llu %7.1f%% %s%s\n", + k.nTotal, (unsigned long long)k.nUnique, k.nHits, rate, + it->first.c_str(), k.bOverflow ? " (capped)" : ""); + out += szLine; + } + } + out += "---- timing (by tag) ----\n"; out += " count total_ms avg_us max_us tag\n"; for (std::map::const_iterator it = s.entries.begin(); @@ -277,5 +355,6 @@ namespace SectionPerf s.nTempFiles = 0; s.nTempFileBytes = 0; s.nWriteBacks = 0; + s.attrStats.clear(); } } diff --git a/SourceCode/Code2026/inc/SectionPerfCounter.h b/SourceCode/Code2026/inc/SectionPerfCounter.h index bc0f19f..cbd9463 100644 --- a/SourceCode/Code2026/inc/SectionPerfCounter.h +++ b/SourceCode/Code2026/inc/SectionPerfCounter.h @@ -19,6 +19,7 @@ #define SECTION_PERF_COUNTER_H #include +#include namespace SectionPerf { @@ -45,6 +46,27 @@ namespace SectionPerf // 统计唯一 hash 数与被重复命中的次数(模拟命中率) void NoteRegionText(const char* pszTxt, size_t nLen); + // 属性 key:用"实体数据"而不是 SAT 文本做 key 时的模拟命中统计。 + // 调用方把参与 key 的字段逐个 Add 进来,析构时统一记一次。 + // 用途:比较"内容 hash"与"属性 key"两种缓存键的命中率,再决定 Phase 1 用哪个。 + class AttrKey + { + public: + // pszClass 一般是 isA()->name(),用于隔离不同实体类 + explicit AttrKey(const TCHAR* pszClass); + ~AttrKey(); + + void Add(const void* pData, size_t nLen); + + private: + AttrKey(const AttrKey&); + AttrKey& operator=(const AttrKey&); + + unsigned long long m_nHash; + std::string m_sClass; + bool m_bActive; + }; + // 记录一次临时文件写入 void NoteTempFile(size_t nBytes);