简单的性能计时器






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 日 - 初始版本,基于 我的博文