perf: 给基础体构造加计时,定位到 DrawPipe/DrawCone/DrawLoft 共占 33% #109
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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 <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
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<std::string, Entry>& Stats()
|
||||
{
|
||||
static std::map<std::string, Entry> 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<std::string, Entry>& m = Stats();
|
||||
for (std::map<std::string, Entry>::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
|
||||
@@ -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` 这种莫名其妙的错。
|
||||
Reference in new issue
Block a user