系统与计时支持模块技术文档

系统与计时支持模块为点云处理与SLAM系统提供统一的代码执行时间测量能力,解决了分散计时逻辑不兼容、指标无法汇总、重复实现性能统计逻辑的问题。该模块既支持单次多轮次的函数性能评估,也支持全生命周期的耗时数据收集与导出,是系统性能调优、瓶颈定位的核心基础工具,尤其为KD树结构与距离节点等高频率调用组件的性能验证提供了标准化手段。

1. 模块概述

你可以将该模块理解为性能测试工具包中的两类工具:

  1. 一次性秒表:evaluate_and_call 函数用于开发阶段的微基准测试,直接输出单次多轮测试的平均耗时
  2. 飞行数据记录仪: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]

数据流说明

  • 微基准测试流:开发人员调用 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_callTimer两套接口 基准测试和运行时监控职责分离,接口更简洁 功能有一定重叠,存在少量重复代码 两类场景使用时机和需求差异大,合并会导致接口臃肿

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. 新开发者注意事项

  1. 线程安全风险:不要在多线程场景下直接调用Timer接口,必须加外部锁,否则会出现难以排查的统计数据错误或崩溃
  2. 全局状态污染:每次测试或流程启动时必须调用Timer::Clear(),否则前一次运行的记录会污染当前统计结果
  3. 函数名唯一性:不同操作必须使用不同的func_name,否则会将不同操作的耗时混合统计,得到错误的平均结果
  4. 除零风险:调用evaluate_and_calltimes参数必须大于0,否则会触发除零错误
  5. 异常传播Timer::Evaluate不会捕获被测量函数抛出的异常,异常会直接向上传递,不会影响计时记录的完整性
  6. Clear隐含操作Timer::Clear会自动重置ikd-Tree状态,若仅需要清空计时记录不要调用该接口,避免业务状态被意外重置