65.9K
CodeProject 正在变化。 阅读更多。
Home

简单的性能计时器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.21/5 (5投票s)

2016年1月11日

CPOL

6分钟阅读

viewsIcon

23236

downloadIcon

418

描述了一个可用于分析应用程序中 cpp 代码的简单性能计时器

引言

在进行代码剖析时,拥有高级且易于使用的工具会很有帮助。但如果我们想进行一些简单的测试/基准测试呢?也许自定义代码可以完成这项工作?

让我们来看看 C++ 应用程序的简单性能计时器。

一个任务听起来可能很简单:检测 ABC 模块中的哪部分代码执行花费的时间最多。或者另一个场景:比较 Xyz 算法和 Zyx 算法的执行时间。

有时,我不会使用和设置高级剖析工具,而是使用自己的自定义剖析代码。大多数时候,我只需要一个好的计时器和一个在屏幕/输出上打印内容的方法。仅此而已。通常,这对于大多数情况来说已经足够了……或者至少是一个更深入、更高级剖析会话的良好开端。

简要规格

我们想要什么?

  • 我想测量我代码中任何函数甚至例程一部分的执行时间。
  • 需要添加到例程中的剖析代码必须非常简单,理想情况下只需要一行额外的代码。
  • 应该有一个标志来全局启用/禁用剖析。

其他现有库

上面列出的库功能更强大,配备了一些统计功能,可以为您正在测试的代码提供大量信息。本文描述的方法更适合小型项目或快速实验,只需少量额外工作。

定时器

一个好的计时器是我们机制的核心。

这里简要总结了可用选项

  • 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 结果的图片。
enter image description here

待办事项

  • 在打印数据时添加文件/行信息?
  • 对 VS2015/GCC 版本使用 std::chrono

摘要

这篇帖子描述了一个方便的性能计时器。如果您只需要检查某些代码/系统的执行时间,只需包含一个头文件(+ 添加相关的 .cpp 文件)并在分析过的位置使用 SIMPLEPERF_FUNCSTARTSIMPLEPERF_START(str)/END。最终输出应有助于您找到热点……所有这些都无需使用高级工具/机制。

资源

历史

  • 2016 年 1 月 11 日 - 初始版本,基于 我的博文
© . All rights reserved.