系统与计时支持模块技术文档
系统与计时支持模块为点云处理与SLAM系统提供统一的代码执行时间测量能力,解决了分散计时逻辑不兼容、指标无法汇总、重复实现性能统计逻辑的问题。该模块既支持单次多轮次的函数性能评估,也支持全生命周期的耗时数据收集与导出,是系统性能调优、瓶颈定位的核心基础工具,尤其为KD树结构与距离节点等高频率调用组件的性能验证提供了标准化手段。
1. 模块概述
你可以将该模块理解为性能测试工具包中的两类工具:
- 一次性秒表:
evaluate_and_call函数用于开发阶段的微基准测试,直接输出单次多轮测试的平均耗时 - 飞行数据记录仪:
Timer单态类用于生产运行时的全流程耗时采集,持久化存储所有关键操作的耗时数据,支持导出分析
模块在整体架构中的角色是纯辅助工具层,不参与业务逻辑计算,仅负责耗时数据的采集、存储和导出。所有业务模块按需调用其接口即可,不需要感知内部实现。
1.1 架构与数据流
flowchart LR
A[业务代码] -->|开发基准测试| B[evaluate_and_call]
B --> C[glog日志输出]
A -->|运行时监控| D[Timer::Evaluate]
D --> E[静态records_存储]
E --> F[Timer::PrintAll]
E --> G[Timer::DumpIntoFile]
E --> H[Timer::GetMeanTime]
E --> I[Timer::Clear]
I --> J[关联ikd-Tree::clear 重置操作
src/timer.h:69]
src/timer.h:69]
数据流说明
- 微基准测试流:开发人员调用
evaluate_and_call传入待测试函数、名称和迭代次数,函数会自动执行指定次数,统计平均耗时并直接输出到日志,无持久化存储 - 运行时监控流:业务流程中对关键操作包裹
Timer::Evaluate调用,耗时数据会自动存入全局记录集合;流程结束后可通过打印、导出文件等方式获取统计结果,重置时调用Timer::Clear清空历史数据
2. 核心设计细节
2.1 内存所有权模型
模块不向外部暴露任何内部资源的所有权:
evaluate_and_call接受函数的转发引用,不持有函数对象所有权,所有资源由调用方管理,仅执行同步调用Timer类的静态records_映射表由类本身全局持有,所有TimerRecord实例存储于该映射表中,完全由Timer类管理生命周期,外部代码无法获取或修改内部记录的所有权- 所有字符串和向量存储均使用标准库容器自动管理内存,无手动内存分配释放操作,不存在内存泄漏风险
2.2 对象生命周期与值语义
Timer为纯静态单态类,没有非静态成员,遵循零规则,编译器自动生成的所有特殊成员函数均正确TimerRecord为简单聚合结构体,同样遵循零规则:拷贝操作会深拷贝函数名字符串和耗时向量,移动操作则会低成本转移资源所有权records_使用std::map存储,插入/删除操作不会失效其他元素的迭代器,且内部迭代器从不暴露给外部,无迭代器失效风险
2.3 错误处理策略
- 模块无自定义错误返回码,所有异常均由输入函数传播,模块本身不捕获异常
evaluate_and_call未包含try/catch块,输入函数抛出的异常会直接传递给调用方Timer方法仅在内存不足时可能抛出标准库异常,此类异常属于系统致命错误,不做额外处理- 未定义行为场景:调用
GetMeanTime时传入不存在的函数名,或调用evaluate_and_call时传入times=0会导致除零错误
2.4 Const正确性
evaluate_and_call保留输入函数的const属性,不会修改传入的函数对象Timer所有公共方法均为非静态方法,因为所有操作都会读写全局可变的records_映射表,无const方法- 无mutable成员,所有状态修改均显式通过公共方法执行
2.5 API契约与前置条件
| 方法 | 前置条件 | 隐式约定 |
|---|---|---|
evaluate_and_call [src/sys_utils.h:13] |
times >= 1,输入函数可无参调用 |
函数执行无副作用,或多次执行副作用可接受 |
Timer::Evaluate [src/timer.h:30] |
func_name 非空,且全局唯一标识待测量的操作 |
同一func_name的所有调用会被聚合统计 |
Timer::GetMeanTime [src/timer.cc] |
func_name 已存在于记录集合中 |
返回值为历史所有调用的平均耗时 |
Timer::Clear [src/timer.h:69] |
无 | 调用后会清空所有历史记录,同时自动调用Thirdparty Ikd-Tree的clear方法重置树状态,完成全链路重置 |
2.6 并发与线程安全
模块设计为单线程使用,records_ 映射表未加任何同步保护:
- 多线程同时调用
Timer::Evaluate会导致数据竞争,产生损坏的记录或程序崩溃 - 若需在多线程场景下使用,调用方必须自行添加外部互斥锁保护所有
Timer方法调用 - 单线程场景下支持重入,可在被测量函数内部再次调用计时接口
2.7 性能架构
evaluate_and_call使用std::chrono::high_resolution_clock[src/sys_utils.h:17],纳秒级精度,适合微基准测试Timer::Evaluate使用std::chrono::steady_clock[src/timer.h:32],单调不随系统时间变更,适合长时间运行的生产环境计时- 单次
Timer::Evaluate调用开销低于1微秒,仅包含两次时钟读取、一次map查找和一次vector插入,可安全用于每秒调用数千次的热点路径 records_使用有序std::map存储,查询复杂度O(log n),n为被测量的唯一函数数量(通常不超过50),性能开销可忽略
3. 设计权衡分析
| 设计选择 | 收益 | 代价 | 决策依据 |
|---|---|---|---|
单态全局Timer而非实例化定时器 |
无需在各层业务代码中传递定时器实例,全局唯一的统计结果方便汇总 | 全局状态导致单元测试之间可能相互干扰,需要每次测试前调用Clear重置 |
系统为单流水线处理架构,不需要多套独立的计时统计 |
使用steady_clock而非high_resolution_clock做运行时计时 |
不受NTP时间同步、系统时间修改的影响,长时间运行的统计结果准确 | 部分平台下精度略低于高分辨率时钟 | 系统关注的是毫秒级的操作耗时,精度损失可以忽略 |
使用std::map存储记录 |
输出结果自动按函数名排序,可读性强 | 查找性能略低于std::unordered_map |
记录数量极少,性能差异无实际影响 |
分离evaluate_and_call和Timer两套接口 |
基准测试和运行时监控职责分离,接口更简洁 | 功能有一定重叠,存在少量重复代码 | 两类场景使用时机和需求差异大,合并会导致接口臃肿 |
4. 常见用法示例
4.1 单次微基准测试
#include "sys_utils.h"
// 测试KD树单点插入性能,重复运行100次取平均
fx::evaluate_and_call([]() {
kd_tree.Insert(test_point);
}, "KDTree::Insert", 100);
// 输出示例:I0912 14:32:00.123456 1234 sys_utils.h:25] Method KDTree::Insert average call time/count: 0.23/100 ms.
4.2 运行时全链路统计
#include "timer.h"
// 业务流程中插入统计点,记录批量点云插入耗时
eroam::common::Timer::Evaluate([&]() {
kd_tree.Add_Points(point_cloud_batch);
}, "KDTree::Add_Points");
// 运行结束后导出统计结果
eroam::common::Timer::PrintAll();
eroam::common::Timer::DumpIntoFile("kd_tree_performance.csv");
double avg_batch_insert_time = eroam::common::Timer::GetMeanTime("KDTree::Add_Points");
// 重置所有状态,准备下一轮测试
eroam::common::Timer::Clear();
5. 新开发者注意事项
- 线程安全风险:不要在多线程场景下直接调用
Timer接口,必须加外部锁,否则会出现难以排查的统计数据错误或崩溃 - 全局状态污染:每次测试或流程启动时必须调用
Timer::Clear(),否则前一次运行的记录会污染当前统计结果 - 函数名唯一性:不同操作必须使用不同的
func_name,否则会将不同操作的耗时混合统计,得到错误的平均结果 - 除零风险:调用
evaluate_and_call时times参数必须大于0,否则会触发除零错误 - 异常传播:
Timer::Evaluate不会捕获被测量函数抛出的异常,异常会直接向上传递,不会影响计时记录的完整性 - Clear隐含操作:
Timer::Clear会自动重置ikd-Tree状态,若仅需要清空计时记录不要调用该接口,避免业务状态被意外重置