docs: 记录属性 key 命中率实测结果 #99
No files matched your search
@@ -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)
|
||||
{
|
||||
//原生写法
|
||||
|
||||
@@ -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<unsigned long long> seen;
|
||||
};
|
||||
|
||||
struct Stats
|
||||
{
|
||||
Stats()
|
||||
@@ -49,6 +61,9 @@ namespace
|
||||
unsigned long long nTempFileBytes; // 临时文件累计字节
|
||||
|
||||
unsigned long long nWriteBacks; // Region -> SAT 文本 次数
|
||||
|
||||
// 属性 key:class 名 -> 唯一/命中统计
|
||||
std::map<std::string, KeyStat> 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<std::string, KeyStat>::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<std::string, Entry>::const_iterator it = s.entries.begin();
|
||||
@@ -277,5 +355,6 @@ namespace SectionPerf
|
||||
s.nTempFiles = 0;
|
||||
s.nTempFileBytes = 0;
|
||||
s.nWriteBacks = 0;
|
||||
s.attrStats.clear();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
#define SECTION_PERF_COUNTER_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string>
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -148,6 +148,45 @@ TempFiles=6773 TempBytes=9,341,125 WriteBacks=1140
|
||||
减去 clone 成本(几十 µs 级,合计约 0.1~0.3 s)→ **净省约 12 s / 108 s ≈ 11 %**,
|
||||
并少约 4000 次临时文件写盘。与基于 trace 的估算(10~14 %)吻合。
|
||||
|
||||
#### 属性 key 命中率实测(同日第二次跑图,113 s)
|
||||
|
||||
在 `AecDbEwPipeBase::RegionAcisInDwgInFields` 里按"类名 + 除编号外的全部 spec XData"
|
||||
建第二套 key(跳过 Id / 点 / 向量 / 矩阵 / 二进制类型),与内容 hash 并行统计:
|
||||
|
||||
```
|
||||
---- attr key (by class, would-be cache hit rate) ----
|
||||
total unique hits hit_rate class
|
||||
1088 173 915 84.1% AecDbEwPipe
|
||||
1172 3 1169 99.7% AecDbEwPipeBase
|
||||
96 1 95 99.0% AecDbWeir
|
||||
48 1 47 97.9% AecDbColumn
|
||||
84 2 82 97.6% AecDbBeam
|
||||
40 1 39 97.5% AecDbMultiPartEntity
|
||||
184 44 140 76.1% AecDbXWall
|
||||
78 22 56 71.8% AecDbGuardBar
|
||||
214 122 92 43.0% AecDbXSlab
|
||||
...(合计 3082 次,2672 命中)
|
||||
```
|
||||
|
||||
**属性 key 合计命中率 86.7 %,明显高于内容 hash 的 54.0 %**——证实"同数据的管子/管件截面必然相同"。
|
||||
|
||||
但要注意覆盖率:属性 key 只覆盖 `EwPipe::RegionAcisIn` 这条路径(3082 / 5641 次)。
|
||||
另外 2559 次走 `CoreTools::StringToRegion`(设备、保温等),拿不到 spec 数据,只能用内容 hash。
|
||||
|
||||
所以 Phase 1 应是混合策略:
|
||||
|
||||
| 路径 | key | 可用次数 | 预计命中 |
|
||||
| --- | --- | ---: | ---: |
|
||||
| 管道/管件族(`AecDbEwPipeBase::RegionAcisInDwgInFields`) | 属性 key | 3082 | 2672 |
|
||||
| `CoreTools::StringToRegion` / `StringToBody` | 内容 hash | 2559 | ≈1380(按 54 %) |
|
||||
|
||||
预计节省:2672 × 2.72 ms + 1380 × 5.54 ms ≈ **14.9 s**,
|
||||
占恢复总耗时 22.6 s 的 66 %,占 113 s 开图的 **约 13 %**(比纯内容 hash 多省 2~3 s)。
|
||||
|
||||
**尚未验证**:属性 key 命中时两份 SAT 文本是否真的相同。若存在"同数据不同文本"
|
||||
(旧版本存盘、不同代码路径)的情况,直接复用会有风险,需要 textHash 兜底校验,
|
||||
代价是每次多算一次 hash(几微秒)。这一项应在 Phase 1 实现时顺带统计。
|
||||
|
||||
三个待追查项(不阻塞 Phase 1):
|
||||
|
||||
1. **命中率只有 54 %**。2597 种唯一文本对 5641 次恢复,说明同 DN 的 SAT 文本并非完全一致。
|
||||
|
||||
Reference in new issue
Block a user