已关闭
[Performance]: msprof解析性能优化分析与实施指导报告 #100
Mrtutu创建于 7月12日关闭于 5 天前
7月12日 添加了label:performance
Mrtutu
7月12日 评论:
7月12日 评论:
👋 您好,欢迎向 MindStudio msprof 提交 Issue!
我们已收到您的反馈,感谢你对开源社区的支持。🎉
📅处理时效: 维护团队将在24小时内 查看并回复您的问题(工作日)。
🔍自助查询: 在等待期间,建议您先查阅以下资料,可能已有解决方案:
📖 MindStudio msprof官方文档
📝 贡献者指南
请确保 Issue 描述清晰,包含复现步骤和日志,这将帮助我们更快定位问题。谢谢!


7月12日 添加了label:triaged
7月12日 关联了里程碑:MindStudio 26.2.0
7月12日 修改了issue 的描述
WangJie
7月17日 评论:
7月17日 评论:
当前 IdPool::GetId 的实现中,有大量int2string,以及字符串拼接操作,造成了极大的性能开销,优化这部分逻辑可以获得10%左右性能收益
使用tuple直接作为map的key,优化后结果


Wangang Yu
7月22日 评论:
7月22日 评论:
/label add pending


7月22日 添加了label:pending
5 天前 issue状态由 TODO 改变为 DONE
5 天前 关闭了 issue
5 天前 添加了label:resolved
ccac302472e7452d9d0700bf1b8c1993.svg
提交提案之前,请先检索仓库内是否已有相同的提案,如已有请在同一提案中进行讨论。
性能优化具体描述
msprof 解析性能优化分析与实施指导报告
1. 报告目的
本文基于当前仓库中的
flame.svgperf 火焰图结果,以及对msprofC++/Python 解析与导出链路的代码分析,整理后续性能优化的候选方向、预期收益、实现难度、详细方案、风险与验证方法。本报告用于指导后续多轮性能优化,不直接等同于已经验证的收益结论。所有预期收益均为基于火焰图占比和代码结构的估算,最终必须通过同一数据集、同一命令、同一环境下的优化前后实测确认。
2. 输入与分析范围
2.1 输入资料
flame.svganalysis/csrc/application/export_manager.cppanalysis/csrc/domain/data_process/ai_task/api_processor.cppanalysis/csrc/domain/data_process/ai_task/task_processor.cppanalysis/csrc/domain/data_process/ai_task/communication_info_processor.cppanalysis/csrc/infrastructure/utils/time_utils.cppanalysis/csrc/infrastructure/utils/hp_float.hanalysis/csrc/domain/services/parser/parser_item_factory.cppanalysis/csrc/application/timeline/json_assembler.cppanalysis/csrc/application/database/db_assembler.hanalysis/csrc/infrastructure/db/include/connection.h2.2 重点分析范围
本报告聚焦
msprof解析/导出链路中的以下性能问题:不包含:
3. 当前火焰图关键结论
3.1 主要热点概览
ExportManager::ProcessData -> DataProcessor::RunApiProcessor::ProcessApiProcessor::FormatDataTaskProcessor::ProcessSingleDevice/ProcessTaskProcessor::FormatDataGetTimeFromHostCnt/GetTimeFromCntGetLocalTimeJsonAssembler::RunDBAssembler::RunCommunicationInfoProcessor::ProcessParserItemFactory::GetParseItemStarsSocParser::ParseData/ParseDataItem3.2 火焰图解释注意事项
ThreadPool::Loop/AddTask在火焰图中显示约 81%,但这是线程池 worker 的包含栈,不应直接理解为线程池本身消耗 81%。真正需要优化的是其子栈下的业务函数,例如DataProcessor、ApiProcessor、TaskProcessor、JsonAssembler等。Python 主入口在火焰图中也有约 14% 的解释器栈,但结合既有基线报告和代码路径看,当前 export/parse 重热点主要在 C++ 侧。Python 更像是调度入口或调用 C++ 扩展/子流程,不是本轮最大优先级。
4. 当前解析与导出链路架构
4.1 当前主流程
当前 export 侧主要是阶段式流程:
4.2 当前架构的主要问题
全局阶段 barrier 明显
ProcessData必须完成后,DB/Timeline/Summary 才开始。DataInventory 作为全局中间仓库导致内存和搬运成本偏高
vector<tuple<...>>。TraceEvent对象。并行粒度偏 processor 级
ExportManager::ProcessData基于 processor list 并行。输出阶段启动太晚
一些 summary 逻辑本质可以 streaming 聚合
5. 架构级优化项
A1. 将阶段式处理改为流水线 producer-consumer
当前瓶颈
当前形态:
这种形态导致 CPU 格式化、JSON 序列化、SQLite 写入基本串阶段执行。
目标形态
详细方案
引入
Batch<T>概念:template <typename T> struct DataBatch { std::vector<T> rows; uint16_t deviceId; bool eof = false; };为每类核心数据建立 bounded queue:
DataProcessor 不再一次性把全部数据写入 DataInventory,而是按 batch 推送:
DB writer 独立消费 batch:
Timeline writer 独立消费 batch:
Summary aggregator 独立消费 batch:
使用 bounded queue 控制内存:
错误传播:
分阶段落地建议
第一阶段不要直接重写全链路。建议先选择
ApiData做试点:确认输出一致和收益后,再逐步推广到 Task 和 Communication。
A2. 按 device/table 分片并行,减少 processor 长尾
TaskProcessor、CommunicationInfoProcessor、system processors当前瓶颈
例如
TaskProcessor::Process内部按 device 遍历:auto deviceList = Utils::File::GetFilesWithPrefix(profPath_, DEVICE_PREFIX); for (const auto& devicePath: deviceList) { flag = ProcessSingleDevice(devicePath, allProcessedData) && flag; }对于多 device 数据集,单 processor 内部仍可能串行。
详细方案
抽象 shard 任务:
每个 shard 只产生本 device 的结果:
最后统一 merge:
(timestamp, deviceId, streamId, taskId)排序。控制并行度:
A3. 从固定阶段调度演进为 DAG 调度
目标 DAG 示例
详细方案
定义节点类型:
SourceNode:读取原始 DB/bin。TransformNode:格式化和转换。SinkNode:DB/JSON/CSV 输出。AggregateNode:summary 聚合。定义依赖:
调度规则:
先以配置表方式定义 DAG,不建议一开始做过度抽象。
A4. DB writer 后台化并支持 batch/iterator 直接写
当前代码特征
SaveData当前接受vector<tuple<...>>:template<typename... Args> bool SaveData(const std::vector<std::tuple<Args...>> &data, const std::string &tableName, DBInfo& msprofDB)SQLite 插入已经在事务中执行:
sqlite3_exec(db_, "BEGIN", nullptr, nullptr, nullptr); ... sqlite3_exec(db_, "COMMIT", nullptr, nullptr, nullptr);因此重点不是“一行一事务”,而是:
vector<tuple<...>>详细方案
给 DB 层增加 iterator/adapter 版本:
template <typename Iter, typename Binder> bool InsertRows(const std::string& tableName, Iter begin, Iter end, Binder binder);对 DataInventory 中的数据直接 bind:
流水线阶段:
按表管理事务:
writer 线程模型:
A5. Timeline JSON 流式写,减少 TraceEvent 对象池
shared_ptr<TraceEvent>中间对象JsonAssembler::Run占 10.23%,大 timeline 数据集可能收益明显当前瓶颈
典型路径:
如
HcclAssembler::AssembleData、CannAssembler::AssembleData都会先生成事件对象,再写 JSON。详细方案
增加直接写函数:
void WriteApiTraceEvent(JsonWriter& writer, const ApiData& data, ...); void WriteHcclTraceEvent(JsonWriter& writer, const CommunicationTaskData& data, ...);元数据保留现有对象方式或改成 direct writer。
对大规模 duration/counter/flow event 直接写:
统一逗号和数组边界:
","连接多个 assembler。ArrayItemGuard或WriteSeparatorIfNeeded,避免手工字符串拼接。大文件输出建议支持分片临时文件:
这样 writer 可以边消费边落盘,而不是全部放在
StringBuffer。A6. Summary streaming 聚合
详细方案
定义 aggregator:
class SummaryAggregator { public: void Update(const DataBatch<AscendTaskData>& batch); void Finalize(DataInventory& output); };对只需要聚合值的 summary,不再保留全部明细。
对确实需要排序或 join 的 summary,保留必要索引,不保留完整原始对象。
对 export mode 做懒计算:
A7. 全局线程池与背压调度
ExportManager、ProcessControl、各 processor 内部并行详细方案
引入全局
ExecutionContext:CPU stage、I/O stage 分池:
shard 和 processor 任务都提交到同一个 scheduler。
bounded queue 提供自然背压。
6. 模块级优化项
M1. 时间换算 fast path,减少 HPFloat 热点
HPFloatGetTimeFromHostCnt/GetTimeFromCnt/GetLocalTime和HPFloat构造/运算成本GetTimeFromHostCnt/GetTimeFromCnt约 12.7%,GetLocalTime约 5%+,HPFloat构造/赋值/加法多处进入 Top 热点当前问题
当前时间换算返回
HPFloat:HPFloat GetTimeFromCnt(uint64_t sysCnt, uint64_t hostMonotonic, uint64_t referenceCnt, double frequency) { uint64_t timeDiff = ...; HPFloat res = static_cast<double>(timeDiff) / frequency; res = res << NS_US; res = HPFloat(hostMonotonic) +/- res; return res; }HPFloat模板构造函数会初始化 30 位 vector,并通过std::to_string进入字符串化和逐位处理:for (int32_t i = 0; i < defaultPrecision_; i++) { num_.emplace_back(0); } *this = value;在 API/Task/Communication 的逐行循环中,这个成本会被放大。
详细方案
新增轻量时间转换器:
class FastTimeConverter { public: explicit FastTimeConverter(const ProfTimeRecord& record, const SyscntConversionParams& params); uint64_t HostCntToLocalNs(uint64_t syscnt) const; uint64_t SyscntToLocalNs(uint64_t syscnt) const; uint64_t MonotonicToLocalNs(uint64_t timestamp) const; private: uint64_t baseTimeNs_; uint64_t hostMonotonic_; uint64_t hostCnt_; uint64_t sysCnt_; double hostFreq_; double freq_; };对默认频率场景走纯整数:
对非默认频率使用定点换算:
为避免 double 精度风险,可使用
long double或__int128:uint64_t deltaNs = static_cast<uint64_t>( std::llround(static_cast<long double>(diff) * 1000.0L / freq));保留原
HPFloatAPI:建立回归测试:
UINT64_MAX的边界。HPFloat结果逐条对比,允许明确的舍入误差范围。首批迁移目标
ApiProcessor::FormatDataTaskProcessor::FormatDataCommunicationInfoProcessor::UpdateCommunicationInfoProcessor::FormatDataMsprofTxHostProcessorDpuProcessorStepTraceProcessorGetTimeFromHostCnt/GetTimeFromSyscnt/GetLocalTime调用点M2. 修复 ParserItemFactory 整表复制
GetParseItem使用引用查找,不复制注册表和内层 mapunordered_map/std::function分配和复制ParserItemFactory::GetParseItem约 2.83%,并伴随unordered_map<ParserType,...std::function...>分配热点当前问题
当前实现:
std::unordered_map<ParserType, ItemFuncMap> parserItemFuncs = GetContainer(); auto it = parserItemFuncs.find(parserType); ... auto parserFunc = it->second; auto ans = parserFunc.find(itemType);问题:
GetContainer()返回静态 map 引用,但赋值给局部变量会复制整个 map。auto parserFunc = it->second又复制内层 map。ItemFunc是std::function,复制成本更高。详细方案
改为引用:
const auto& parserItemFuncs = GetContainer(); auto it = parserItemFuncs.find(parserType); ... const auto& parserFunc = it->second; auto ans = parserFunc.find(itemType);同时可考虑返回
const ItemFunc*或const ItemFunc&避免返回值复制,但这会改变接口,建议作为第二步。验证方法
ParserItemFactory::GetParseItem和unordered_map/std::function分配栈是否下降。M3. API ModelLoad 过滤从线性扫描改为按 threadId 区间索引
ApiProcessor中 ModelLoad 范围过滤当前问题
当前逻辑:
for (const auto& data : processedData) { if (data.apiName == "ModelLoad") { modelLoadDatas.push_back(data); } } FilterDataByStartTime<ApiData>(processedData, record.startTimeNs, PROCESSOR_NAME_API, [modelLoadDatas](const ApiData& data, uint64_t startTimeNs) { bool inRange = isContainedInRange(modelLoadDatas, data); ... });isContainedInRange对每条 data 遍历全部modelLoadDatas,并且 lambda 按值捕获modelLoadDatas会复制一次 vector。详细方案
建立按 threadId 分组的区间:
std::unordered_map<uint64_t, std::vector<Interval>> modelLoadRanges;每个 threadId 下按 start 排序,合并重叠区间。
判断 data 是否落入区间时二分查找:
lambda 捕获改为引用或外部函数对象。
如果 ModelLoad 数量很少,可设置阈值:
验证方法
processedData数量和关键字段。M4. CommunicationInfoProcessor 减少字符串 key 和重复 map 查找
CommunicationInfoProcessor中字符串拼接、unordered_map 查找、HPFloat 转换成本CommunicationInfoProcessor::Process6.69%,ProcessKfcData6.21%,FormatKfcData4.86%,Update4.67%当前问题
热点逻辑包括:
taskData.opKey = Join("_", opName, groupName, deviceId)。opDataMap[taskData.opKey]多次访问。GetGroupNameValue和多个GetEnumTypeValue。详细方案
map 预留容量:
opDataMap.reserve(communicationData.oriTaskData.size() / estimatedTasksPerOp); endpoints.reserve(...);使用
try_emplace:auto [it, inserted] = opDataMap.try_emplace(taskData.opKey); if (inserted) { ... }避免重复
operator[]:当前多次
opDataMap[taskData.opKey]会重复 hash/查找。应保存 iterator。groupName 映射缓存:
enum 映射缓存:
对
rdmaType/transportType/dataType/linkType这种重复字符串建立局部 cache。opKey 结构化:
中期可将字符串 key 改为结构体:
struct CommOpKey { std::string opName; std::string groupName; uint16_t deviceId; };或对字符串做 interning 后使用整数 id key。
时间转换纳入 M1 fast path。
验证方法
CommunicationTaskData和CommunicationOpData行数、opKey、timestamp/end、relay/retry。M5. TaskProcessor GetTaskType 与时间换算优化
TaskProcessor::FormatData中时间换算 fast path,并减少 task type map 拷贝TaskProcessor::FormatData11.30%,GetTaskType1.72%当前问题
TaskProcessor::FormatData每条数据:HPFloat start{tmpStart}; HPFloat end = start + HPFloat(data.duration); data.timestamp = GetLocalTime(start, timeRecord).Uint64(); data.end = GetLocalTime(end, timeRecord).Uint64(); data.taskType = GetTaskType(...);GetTaskType内部:std::map<std::string, std::string> sqeType; if (Context::IsStarsChip(platformVersion)) { sqeType = STARS_SQE_TYPE_TABLE; } else { sqeType = HW_SQE_TYPE_TABLE; }这里每次调用都可能复制 map。
详细方案
时间转换使用 M1 fast path。
GetTaskType中表改为 const 引用:const auto& sqeType = Context::IsStarsChip(platformVersion) ? STARS_SQE_TYPE_TABLE : HW_SQE_TYPE_TABLE;对常见
(hostType, deviceType, platformVersion)做小型缓存:如果
deviceType是数字字符串,避免每次IsNumber+ map 查找,可缓存deviceType -> resolvedType。M6. JsonWriter 与 Timeline 事件序列化优化
JsonAssembler::Run、TraceEvent::DumpJson、rapidjson 写字符串成本JsonAssembler::Run10.23%,JsonWriter::operator[]/Member、rapidjsonString/WriteString多处上榜详细方案
大量事件直接写 JSON,不创建
TraceEvent对象。对固定字段名使用常量:
constexpr const char* FIELD_NAME = "name"; constexpr const char* FIELD_PID = "pid";避免时间戳先转 string 再写:
DurationEvent的ts_是std::string。JsonWriter支持 reserve:StringBuffer时 reserve。超大 JSON 支持 streaming file writer:
StringBuffer全量持有,再FlushToFile。FileWriteStream或项目已有 DumpTool 分片写。对
ProcessArgs里重复 key/value 的字段做直接写函数。M7. DBAssembler 减少 tuple 中间层
vector<tuple>构造DBAssembler::Run5.75%,SQLite bind/step 单点占比不高,说明中间转换也需关注详细方案
对高频大表先做专用 direct saver:
保留通用
SaveData(vector<tuple>)作为 fallback。新增 adapter:
struct ApiDataBinder { static constexpr auto TableName = TABLE_NAME_CANN_API; void Bind(Connection& conn, const ApiData& row) const; };先减少一层 vector,后续再后台化。
M8. LoadData 查询裁剪与早过滤
示例
TaskProcessor::LoadData已经有:WHERE a.device_task_type != 'UNKNOWN'可以进一步评估是否能在 SQL 中提前过滤 start time、source、level 等条件,减少进入 C++ 循环的数据。
注意
不要盲目把所有过滤都下推到 SQL。需要确认:
M9. DataInventory 内存与并发访问优化
详细方案
数据按 device 或 table shard 存储。
下游消费完成后及时释放:
对只读共享数据使用
shared_ptr<const vector<T>>。对单消费者数据使用 move。
为 DAG 节点声明数据生命周期,自动释放 no-longer-used data。
M10. ProcessControl/ThreadPool 开销治理
RunPreparedProcess/Process::Run约 15% 包含栈,需要结合子栈分析详细方案
ProcessControl::RunProcesses中每个 level 创建一个 ThreadPool。可改为复用全局 pool。小任务数量较少时,避免创建线程池,直接串行执行。
与 A7 全局调度器合并。
7. 优化项优先级总表
ParserItemFactory::GetParseItem去复制8. 推荐实施路线
第 0 阶段:建立可复现基线
目标:
建议数据:
第 1 阶段:低风险局部优化
候选:
ParserItemFactory去复制。GetTaskTypemap 引用优化。目标:
第 2 阶段:时间换算 fast path
候选:
FastTimeConverter。ApiProcessor::FormatData。TaskProcessor::FormatData。CommunicationInfoProcessor。目标:
第 3 阶段:高频业务 processor 优化
候选:
目标:
第 4 阶段:输出侧优化
候选:
目标:
第 5 阶段:架构级流水线与 DAG
候选:
ApiData单链路 producer-consumer 试点。TaskData和CommunicationData分片。目标:
9. 验证与度量方法
9.1 功能正确性
每轮优化必须验证:
建议增加自动对比脚本:
9.2 性能指标
每轮至少记录:
9.3 收益判定
建议规则:
如果优化收益低于测试噪声,需要增加运行次数到 5 次或 10 次。
9.4 回归数据集
至少覆盖:
10. 风险与控制策略
10.1 时间换算风险
风险:
HPFloat可能用于规避 double 精度问题。控制:
10.2 流水线顺序风险
风险:
控制:
10.3 并发与线程安全风险
风险:
控制:
10.4 过度架构化风险
风险:
控制:
ApiData单链路试点。11. 建议的首轮任务清单
首轮建议选择低风险且火焰图有明确证据的优化:
ParserItemFactory::GetParseItem整表复制。TaskProcessor::GetTaskType使用 const 引用避免 map 拷贝。ApiProcessor的 ModelLoad lambda 改引用捕获,并准备区间索引 UT。FastTimeConverter原型和单测,但先不大规模迁移。首轮完成标准:
ParserItemFactory::GetParseItem占比下降。12. 建议的第二轮任务清单
第二轮开始攻击最大热点:
ApiProcessor::FormatData迁移到FastTimeConverter。TaskProcessor::FormatData迁移到FastTimeConverter。第二轮完成标准:
ApiProcessor::FormatData和TaskProcessor::FormatData火焰图占比明显下降。GetTimeFromHostCnt/GetTimeFromCnt/GetLocalTime/HPFloat相关热点下降。13. 长期目标架构
长期目标不是简单扩大线程池,而是形成以下结构:
关键原则:
14. 总结
当前火焰图显示,
msprofexport/parse 的主要瓶颈集中在 C++ 数据处理与输出链路,尤其是:FormatDataHPFloat驱动的时间换算短期应优先处理确定性强、风险低、收益可验证的模块优化,例如
ParserItemFactory去复制和时间换算 fast path。中期应优化 API/Task/Communication 的逐行处理和 JSON/DB 输出。长期则应将当前阶段式架构演进为 batch streaming + DAG scheduler,通过并行掩盖和内存生命周期治理获得更大的端到端收益。性能劣化说明
其他相关讨论
环境信息
欢迎加入社区,感谢您对社区的贡献 🎉!