简单的性能计时器






4.21/5 (5投票s)
描述了一个可用于分析应用程序中 cpp 代码的简单性能计时器
引言
在进行代码剖析时,拥有高级且易于使用的工具会很有帮助。但如果我们想进行一些简单的测试/基准测试呢?也许自定义代码可以完成这项工作?
让我们来看看 C++ 应用程序的简单性能计时器。
一个任务听起来可能很简单:检测 ABC 模块中的哪部分代码执行花费的时间最多。或者另一个场景:比较 Xyz 算法和 Zyx 算法的执行时间。
有时,我不会使用和设置高级剖析工具,而是使用自己的自定义剖析代码。大多数时候,我只需要一个好的计时器和一个在屏幕/输出上打印内容的方法。仅此而已。通常,这对于大多数情况来说已经足够了……或者至少是一个更深入、更高级剖析会话的良好开端。
简要规格
我们想要什么?
- 我想测量我代码中任何函数甚至例程一部分的执行时间。
- 需要添加到例程中的剖析代码必须非常简单,理想情况下只需要一行额外的代码。
- 应该有一个标志来全局启用/禁用剖析。
其他现有库
- google/benchmark - https://github.com/google/benchmark -
- Celero - Codeproject 上的文章
- Nonius -https://nonius.io/
上面列出的库功能更强大,配备了一些统计功能,可以为您正在测试的代码提供大量信息。本文描述的方法更适合小型项目或快速实验,只需少量额外工作。
定时器
一个好的计时器是我们机制的核心。
这里简要总结了可用选项
- RDTSC 指令 - 返回自重置以来的 CPU 周期数,64 位变量。使用此指令非常底层,但这可能不是我们需要的。CPU 周期不是稳定的时间事件:节能、上下文切换……请参阅 RandomAscii 的一篇有趣的读物:Sandybridge 时代的 rdtsc。
- Windows 上的高性能计时器 - 请参阅 获取高分辨率时间戳。它提供最高可能的精度(<1us)。
- GetTickCount- 10 到 16 毫秒的分辨率
- timeGetTime- 使用系统时钟(因此分辨率与- GetTickCount相同),但分辨率可以提高到 1ms(通过- timeBeginPeriod)。请参阅 RandomASCII 博客上 GetTickCount 与 timeGetTime 的完整比较。
- std::chrono- 最后,还有 STL 库中的计时器!- system_clock- 系统时间
- steady_clock- 单调时钟,请参阅此 SO 问题中的 system_clock 与 steady_clock 的区别。
- high_resolution_clock- 最高可能的分辨率,跨平台!警告:它可能是 system 或 steady clock 的别名……取决于系统功能。
 
显然,我们通常应该使用 std::high_resolution_clock,但不幸的是,它在 VS2013(我开发原始解决方案的地方)中不起作用。
这在 VS 2015 中已修复:请参阅 vs 团队的这篇博文。
总的来说,如果您使用的是最新的编译器/库,那么 std::chrono 将按预期工作。如果您有较旧的工具,最好仔细检查。
输出
我们希望在哪里获取结果?在简单场景中,我们可能只使用 printf/cout。另一个选项是直接记录到某个日志文件或使用 Debug View。
性能开销
测量某个效果可能会改变结果。我们的剖析代码对经过的时间影响有多大?如果它的时间占比较长(相对于我们测量代码的时间),我们可能需要以某种方式推迟该过程。
例如,如果我想测量一个仅运行几微秒的小方法的执行时间,那么(每次调用该方法时)将输出写入文件可能比整个函数还长!
因此,我们可以只测量经过的时间(假设它非常快),然后推迟将数据写入输出的过程。
解决方案
简单来说
void longFunction()
{
    SIMPLEPERF_FUNCSTART;
    SIMPLEPERF_START("loop ");
    for (int i = 0; i < 10; ++i)
    {
        SIMPLEPERF_SCOPED("inside loop ");
        //::Sleep(10);
        internalCall();
    }
    SIMPLEPERF_END;
}
它会在程序结束时显示
main : 14837.797000
  longFunction : 0.120000
    loop  : 0.109000
      inside loop  : 0.018000
        internalCall : 0.008000
      inside loop  : 0.011000
        internalCall : 0.009000
      ...
      inside loop  : 0.005000
        internalCall : 0.002000
  shortMethod : 15.226000
    loop  : 15.222000
我们有 3 个基本宏可以使用
* SIMPLEPERF_FUNCSTART - 只需将其放在函数/方法的开头。它将显示函数名称并打印其执行时间。
* SIMPLEPERF_SCOPED(str) - 将其放在作用域的开头。
* SIMPLEPERF_START(str) - 将其放在函数内部,作为自定义标记,您没有打开作用域。
* SIMPLEPERF_END - 需要关闭 SIMPLEPERF_START。
* 加上
* 添加 #include "SimplePerfTimer.h
* 通过设置 #define ENABLE_SIMPLEPERF 来启用它(为简单起见,也包括在 SimplePerfTimer.h 中)。
此外,代码支持两种模式
- 即时:将在获得经过时间后立即打印。打印可能会影响某些性能。
- 保留:将收集数据,以便在程序结束时显示。
在保留模式下,我们可以调用
- SIMPLEPERF_REPORTALL- 显示当前数据
- SIMPLEPERF_REPORTALL_ATEXIT- 将显示数据,但在- main()完成后。实际上可以在程序中的任何时间调用。
需要将标志 #define SIMPLEPERF_SHOWIMMEDIATE true 设置为 true 才能使用保留模式。
问题
整个计时器可能在多核、多线程代码中不起作用,因为它不使用任何临界区来保护共享数据,也不关心代码运行的线程。如果您需要更高级的计时器,那么您可能会对 Preshing on Programming 上的文章感兴趣:多线程 API 的 C++ 剖析模块。
实现细节
计时器的核心思想是使用析构函数来收集数据。这样,当某个 timer 对象超出作用域时,我们就会获得数据。这对于整个函数/显式作用域尤其有用。
{ // scope start
   my_perf_timer t;
}
在基本的即时形式中,计时器仅在构造函数中保存时间(使用 QueryPerformanceCounter),然后在析构函数中测量结束时间并将其打印到输出。
在保留模式下,我们还需要存储数据以备将来使用。我只是创建一个 static 向量,在构造函数中添加一个新条目,然后在析构函数中填充最终时间。我还负责缩进,以便输出看起来不错。
在存储库中,还有一个只有头文件的版本(稍作简化,仅使用即时模式):请参阅 SimplePerfTimerHeaderOnly.h。
这是一张展示 Debug view 中 timer 结果的图片。

待办事项
- 在打印数据时添加文件/行信息?
- 对 VS2015/GCC 版本使用 std::chrono
摘要
这篇帖子描述了一个方便的性能计时器。如果您只需要检查某些代码/系统的执行时间,只需包含一个头文件(+ 添加相关的 .cpp 文件)并在分析过的位置使用 SIMPLEPERF_FUNCSTART 或 SIMPLEPERF_START(str)/END。最终输出应有助于您找到热点……所有这些都无需使用高级工具/机制。
资源
- MSDN:获取高分辨率时间戳
- MSDN:游戏计时和多核处理器
- Preshing on Programming:多线程 API 的 C++ 剖析模块
- Codeproject:计时器教程
- StackOverflow:std::chrono::high_resolution_clock 的分辨率与测量不符
历史
- 2016 年 1 月 11 日 - 初始版本,基于 我的博文


