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

Celero - C++ 基准测试创作库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (19投票s)

2013 年 1 月 10 日

Apache

7分钟阅读

viewsIcon

49338

downloadIcon

841

本文讨论了如何实现和使用一个基于模板的 C++ 基准测试库。

Sample Image

引言

为代码开发一致且有意义的基准测试结果是一项复杂的工作。存在应用程序外部的测量工具(Intel® VTune™ Amplifier、SmartBear AQTime、Valgrind),但它们有时对小型团队来说过于昂贵或使用起来很麻烦。该项目 Celero,旨在成为一个可以添加到 C++ 项目中的小型库,并以一种易于重现、共享和在单次运行、开发者或项目之间进行比较的方式执行代码的基准测试。Celero 使用类似于 GoogleTest 的框架,使其 API 更易于使用并集成到项目中。让自动化基准测试成为您开发流程中与自动化测试一样重要的一部分。

背景

编写基准测试的总体目标是衡量一段代码的性能。基准测试对于比较解决同一问题的多种解决方案以选择最合适的解决方案很有用。有时,基准测试可以突出设计或算法更改的性能影响,并以有意义的方式量化它们。

通过衡量代码性能,您可以消除关于什么才是“正确”解决方案的假设错误。只有通过测量,您才能确认例如使用查找表比计算值更快。这样的传闻(经常被重复)可能导致糟糕的设计决策,并最终导致代码变慢。

编写良好基准测试代码的目标是消除所有噪声和开销,只测量被测代码。测量中的噪声源包括时钟分辨率噪声、操作系统后台操作、测试设置/拆卸、框架开销以及其他不相关的系统活动。

在理论层面,我们希望测量“t”,即执行被测代码的时间。在现实中,我们测量的是“t”加上所有这些测量噪声。

Sample Image

这些对“t”测量的额外贡献会随时间波动。因此,我们希望尝试隔离“t”。实现这一点的方法是进行多次测量,但只保留最小的总和。最小的总和必然是噪声贡献最小且最接近实际时间“t”的那一个。

获得此测量值后,它本身意义不大。创建一个基准测试以供比较很重要。基准测试通常应该是您正在度量其解决方案的“经典”或“纯粹”解决方案。一旦有了基准测试,您就有了一个有意义的时间来比较您的算法。仅仅说您的花哨排序算法 (fSort) 在 10 毫秒内对一百万个元素进行了排序是不够的。但是,将其与像快速排序 (qSort) 这样的经典排序算法基准测试进行比较,然后您就可以说 fSort 在一百万个元素上的速度比 qSort 快 50%。这是一个有意义且强大的测量。

实现

Celero 大量利用 C++11 特性,这些特性在 Visual C++ 2012 和 GCC 4.7 中都可用。这极大地有助于使代码干净且可移植。为了更方便地采用代码,用户所需的所有定义都定义在一个单文件 `Celero.h` 的 `celero` 命名空间内。

`Celero.h` 包含宏定义,这些宏定义将每个用户基准测试用例转换为其自己的唯一类,并带有相关的测试夹具(如果有),然后将测试用例注册到 `Factory`。宏自动将基准测试用例与它们相关的基准测试关联起来,以便在运行时可以计算出基准测试相对的数字。这种关联由 `TestVector` 维护。

`TestVector` 利用 PImpl idiom 来帮助隐藏实现,并将 `Celero.h` 的包含开销降至最低。

Celero 将其输出报告到命令行。由于颜色更美观(并且可能有助于提高结果的人为因素/可读性),因此需要一些比 `std::cout` 更高级的东西。`Console.h` 定义了一个简单的颜色函数 `SetConsoleColor`,它被 `celero::print` 命名空间中的函数使用,以方便地格式化程序的输出。

基准测试执行时间的测量发生在 `TestFixture` 基类中,所有编写的基准测试最终都派生自该类。首先,执行测试夹具设置代码。然后,检索测试的开始时间并以微秒为单位存储(使用无符号长整型)。这样做是为了减少浮点误差。接下来,执行指定数量的操作(迭代)。完成后,检索结束时间,拆卸测试夹具,然后返回执行的测量时间并将结果保存。

此循环会重复指定次数的样本。如果未指定样本(零),则会重复测试,直到它至少运行一秒钟或至少取了 30 个样本。在编写代码的这部分时,有一个明确的“if-else”关系。然而,大部分代码在“if”和“else”部分重复。这里可以使用一个老式的函数,但利用 `std::function` 定义一个可以调用的 lambda 来保持所有代码的整洁非常自然。(C++11 真棒。)最后,将结果打印到屏幕上。

使用代码

Celero 使用 CMake 提供跨平台构建。由于使用了 C++11,它需要一个现代的编译器(Visual C++ 2012 或 GCC 4.7+)。

一旦 Celero 被添加到您的项目中。您可以创建专用的基准测试项目和源文件。为了方便起见,有一个单一的头文件和一个 `CELERO_MAIN` 宏,可用于提供您的基准测试项目的 `main()` 函数,该函数将自动执行您的所有基准测试。

这是一个简单的 Celero 基准测试示例

#include <celero/Celero.h>

CELERO_MAIN;

// Run an automatic baseline.  
// Celero will help make sure enough samples are taken to get a reasonable measurement
BASELINE(CeleroBenchTest, Baseline, 0, 7100000)
{
    celero::DoNotOptimizeAway(static_cast<float>(sin(3.14159265)));
}

// Run an automatic test.  
// Celero will help make sure enough samples are taken to get a reasonable measurement
BENCHMARK(CeleroBenchTest, Complex1, 0, 7100000)
{
    celero::DoNotOptimizeAway(static_cast<float>(sin(fmod(rand(), 3.14159265))));
}

// Run a manual test consisting of 1 sample of 7100000 operations per measurement.
// Celero will help make sure enough samples are taken to get a reasonable measurement
BENCHMARK(CeleroBenchTest, Complex2, 1, 7100000)
{
    celero::DoNotOptimizeAway(static_cast<float>(sin(fmod(rand(), 3.14159265))));
}

// Run a manual test consisting of 60 samples of 7100000 operations per measurement.
// Celero will help make sure enough samples are taken to get a reasonable measurement
BENCHMARK(CeleroBenchTest, Complex3, 60, 7100000)
{
    celero::DoNotOptimizeAway(static_cast<float>(sin(fmod(rand(), 3.14159265))));
}

在这段代码中,我们首先定义一个 `BASELINE` 测试用例。此模板接受四个参数

BASELINE(GroupName, BaselineName, Samples, Operations)

  • GroupName - 基准测试组的名称。这用于将运行和结果与它们相应的基准测试测量值批量分组。
  • BaselineName - 此基准测试的名称,用于报告目的。
  • Samples - 您希望对测试代码执行给定数量操作的总次数。
  • Operations - 您希望每次样本执行测试代码的总次数。

这里的样本和操作用于测量非常快的代码。如果您知道您的基准测试代码需要花费不到 100 毫秒的时间,例如,您的操作数将指示执行代码“operations”次后再进行测量。Samples 定义了要进行多少次测量。 

Celero 通过允许您指定零样本来帮助您。零样本将告诉 Celero 根据完成指定操作所需的时间来制作一些统计上显著的样本数。这些数字将在运行时报告。

提供了 `celero::DoNotOptimizeAway` 模板,以确保优化编译器不会消除您的函数或代码。由于此功能在所有示例基准测试及其基准测试中都使用,因此其时间开销在比较中被抵消了。

在定义了基准测试之后,然后定义各种基准测试。`BENCHMARK` 宏的语法与宏的语法相同。

结果

示例项目配置为在成功编译后自动执行基准测试代码。运行此基准测试在我的 PC 上产生了以下输出

[  CELERO  ]
[==========]
[ STAGE    ] Baselining
[==========]
[ RUN      ] CeleroBenchTest.Baseline -- Auto Run, 7100000 calls per run.
[   AUTO   ] CeleroBenchTest.Baseline -- 30 samples, 7100000 calls per run.
[     DONE ] CeleroBenchTest.Baseline  (0.517049 sec) [7100000 calls in 517049 usec] [0.072824 us/call] [13731773.971132 calls/sec]
[==========]
[ STAGE    ] Benchmarking
[==========]
[ RUN      ] CeleroBenchTest.Complex1 -- Auto Run, 7100000 calls per run.
[   AUTO   ] CeleroBenchTest.Complex1 -- 30 samples, 7100000 calls per run.
[     DONE ] CeleroBenchTest.Complex1  (2.192290 sec) [7100000 calls in 2192290 usec] [0.308773 us/call] [3238622.627481 calls/sec]
[ BASELINE ] CeleroBenchTest.Complex1 4.240004
[ RUN      ] CeleroBenchTest.Complex2 -- 1 run, 7100000 calls per run.
[     DONE ] CeleroBenchTest.Complex2  (2.199197 sec) [7100000 calls in 2199197 usec] [0.309746 us/call] [3228451.111929 calls/sec]
[ BASELINE ] CeleroBenchTest.Complex2 4.253363
[ RUN      ] CeleroBenchTest.Complex3 -- 60 samples, 7100000 calls per run.
[     DONE ] CeleroBenchTest.Complex3  (2.192378 sec) [7100000 calls in 2192378 usec] [0.308786 us/call] [3238492.632201 calls/sec]
[ BASELINE ] CeleroBenchTest.Complex3 4.240175
[==========]
[ STAGE    ] Completed.  4 tests complete.
[==========]

将首先执行的测试是组的基准测试。此基准测试表明它是“自动运行”,表示 Celero 将测量并决定执行代码的次数。在这种情况下,它运行了 30 个样本,其中包含 7100000 次迭代我们的测试代码。(对每组 7100000 次调用进行了测量,并重复了 30 次,取了最短时间。)总测量时间为 0.517049 秒。鉴于此,测量出基准测试代码的每次单独调用需要 0.072824 微秒。

基准测试完成后,将运行每个单独的测试。每个测试以相同的方式执行和测量,但是,会报告一个额外的指标:基准。这会将计算基准测试所需的时间与基准测试进行比较。数据显示 CeleroBenchTest.Complex1 的执行时间是基准测试的 4.240004 倍。

关注点

  • GitHub 项目位于 此处
  • 基准测试应始终在 Release 版本上进行。切勿测量 Debug 版本的性能并根据结果进行更改。(优化)编译器是您代码性能方面的朋友。
  • Celero 提供了其 API 的 Doxygen 文档。
  • Celero 为每个基准测试组支持测试夹具。
  • 边听乐队 "3" 的音乐边执行这段代码会使基准测试运行得更快。很有趣。

历史

  • 2013 年 1 月 - 首次发布。
  • 2013 年 1 月 31 日 - GitHub 更新,支持表格输出。
© . All rights reserved.