为你的代码计时

编写代码时,有时我们无法确定两种实现方式哪一种性能更优。此时该如何判断?一种简单的做法是为代码计时,查看其运行耗时。自 C++11 起,标准库提供了 chrono 头文件以完成这一任务;然而,chrono 的用法略显晦涩。好消息是,我们可以将所需计时功能封装为一个类,方便在自己的程序中直接调用。

计时器类

以下为该类的实现:

#include <chrono> // 引入 std::chrono 相关功能

class Timer
{
private:
    // 类型别名,简化嵌套类型的书写
    using Clock = std::chrono::steady_clock;
    using Second = std::chrono::duration<double, std::ratio<1>>;

    std::chrono::time_point<Clock> m_beg{ Clock::now() };

public:
    void reset()
    {
        m_beg = Clock::now();
    }

    double elapsed() const
    {
        return std::chrono::duration_cast<Second>(Clock::now() - m_beg).count();
    }
};

仅此而已!使用时,只需在 main 函数(或任何需要开始计时的位置)顶部实例化一个 Timer 对象,然后在想查看耗时时调用 elapsed() 成员函数即可。

#include <iostream>

int main()
{
    Timer t;

    // 需要计时的代码放在此处

    std::cout << "耗时: " << t.elapsed() << " 秒\n";

    return 0;
}

示例:对选择排序计时

接下来,我们以实际示例演示:对一个包含 10 000 个元素的数组进行排序。首先使用前面章节编写的选择排序算法:

#include <array>
#include <chrono>    // 引入 std::chrono 相关功能
#include <cstddef>   // 引入 std::size_t
#include <iostream>
#include <numeric>   // 引入 std::iota

const int g_arrayElements{ 10000 };

class Timer
{
private:
    using Clock = std::chrono::steady_clock;
    using Second = std::chrono::duration<double, std::ratio<1>>;

    std::chrono::time_point<Clock> m_beg{ Clock::now() };

public:
    void reset()
    {
        m_beg = Clock::now();
    }

    double elapsed() const
    {
        return std::chrono::duration_cast<Second>(Clock::now() - m_beg).count();
    }
};

void sortArray(std::array<int, g_arrayElements>& array)
{
    // 遍历数组中的每个元素
    //(最后一个元素无需处理,循环结束时自然已排序)
    for (std::size_t startIndex{ 0 }; startIndex < (g_arrayElements - 1); ++startIndex)
    {
        // smallestIndex 记录本轮迭代中遇到的最小元素索引
        // 初始假设本轮首元素即为最小
        std::size_t smallestIndex{ startIndex };

        // 在剩余部分中寻找更小元素
        for (std::size_t currentIndex{ startIndex + 1 }; currentIndex < g_arrayElements; ++currentIndex)
        {
            // 若找到比之前记录更小的元素
            if (array[currentIndex] < array[smallestIndex])
            {
                // 更新最小元素索引
                smallestIndex = currentIndex;
            }
        }

        // 此时 smallestIndex 指向剩余数组中最小元素
        // 将其与本轮起始元素交换,从而将该最小元素放到正确位置
        std::swap(array[startIndex], array[smallestIndex]);
    }
}

int main()
{
    std::array<int, g_arrayElements> array;
    std::iota(array.rbegin(), array.rend(), 1); // 用 10000 至 1 填充数组

    Timer t;

    sortArray(array);

    std::cout << "耗时: " << t.elapsed() << " 秒\n";

    return 0;
}

在作者机器上,三次运行耗时分别为 0.0507、0.0506 和 0.0498 秒,因此可认为约为 0.05 秒。

示例:对 std::sort 计时

下面改用标准库的 std::sort 进行同样测试:

#include <algorithm> // 引入 std::sort
#include <array>
#include <chrono>
#include <cstddef>
#include <iostream>
#include <numeric>

const int g_arrayElements{ 10000 };

class Timer
{
private:
    using Clock = std::chrono::steady_clock;
    using Second = std::chrono::duration<double, std::ratio<1>>;

    std::chrono::time_point<Clock> m_beg{ Clock::now() };

public:
    void reset()
    {
        m_beg = Clock::now();
    }

    double elapsed() const
    {
        return std::chrono::duration_cast<Second>(Clock::now() - m_beg).count();
    }
};

int main()
{
    std::array<int, g_arrayElements> array;
    std::iota(array.rbegin(), array.rend(), 1);

    Timer t;

    std::ranges::sort(array); // C++20 起可用
    // 若编译器不支持 C++20,可改为:
    // std::sort(array.begin(), array.end());

    std::cout << "耗时: " << t.elapsed() << " 秒\n";

    return 0;
}

作者机器上的三次结果分别为 0.000693、0.000692 和 0.000699 秒,基本稳定在 0.0007 秒左右。换言之,在此例中,std::sort 比我们手写选择排序快约 100 倍!

影响程序性能的因素

为程序计时虽简单,但结果会受到诸多因素影响。了解如何正确测量并识别这些干扰因素至关重要。

首先,务必使用 Release(发布)构建目标,而非 Debug(调试)构建目标。调试版本通常禁用优化,而优化对结果影响巨大。例如,在 Debug 模式下运行上述 std::sort 示例,作者机器耗时 0.0235 秒,比 Release 模式慢 33 倍!

其次,系统后台任务也会干扰计时。请确保系统未在执行 CPU、内存或磁盘密集型任务(如玩游戏、文件检索、杀毒扫描或后台更新)。看似无害的闲置浏览器,也可能在标签页轮换广告横幅并解析大量 JavaScript 时瞬间将 CPU 占用拉满。尽可能关闭无关应用,可减小结果波动。

第三,若程序使用随机数生成器,特定的随机序列可能影响耗时。例如,对随机数组排序时,每次运行所需交换次数不同,导致时间波动。若希望多轮运行结果更一致,可临时用固定值(而非 std::random_device 或系统时钟)为随机数引擎播种,使每次生成的序列相同。但若程序性能高度依赖随机序列,则可能得出具有误导性的整体结论。

第四,切勿把等待用户输入的时间计入测量。如确需用户输入,应设法以非阻塞方式提供(例如通过命令行参数、文件输入或绕过输入的代码路径)。

性能测量方法

测量程序性能时,至少收集 3 组数据。若结果相近,则可视为该机器上程序的真实性能;否则,应继续测量,直到出现一组相近的数值,并识别离群值。由于系统后台活动,出现个别离群值并不罕见。

若结果分散、无法聚合,则表明程序性能受到系统其他任务或应用内随机因素的显著影响。

由于性能受硬件速度、操作系统、后台进程等众多因素影响,绝对性能指标(如“程序运行了 10 秒”)通常仅对了解其在某台特定机器上的表现有意义。换一台机器,同一程序可能只需 1 秒、10 秒甚至 1 分钟,不实际测量难以预知。

然而,在单一机器上,相对性能比较仍有价值。我们可以在同一台机器上对同一程序的多个变体进行测量,以判断哪种变体性能最佳。例如,若变体 1 耗时 10 秒,变体 2 耗时 8 秒,则无论机器绝对速度如何,变体 2 在相似硬件上通常都会更快。

测量完第二种变体后,建议重新测量第一种变体,作为一致性校验。若第一种变体的结果与初始测量一致(如仍为 10 秒),则可认为两种变体的测量条件一致,结论可靠:变体 2 确实更快。

反之,若第一种变体的结果与之前不再一致,则说明机器环境发生变化,已影响性能,此时难以判断差异源于变体本身还是系统因素。此时应舍弃已有结果,重新测量。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"