18-4 代码计时
在编写代码时,有时你会遇到难以判断哪种方法性能更优的情况。那么该如何判断呢?
一种简单的方法是计时代码运行时间。C++11的chrono库提供了相关功能。不过该库的使用稍显晦涩。好消息是,我们可以轻松将所需的计时功能封装成类,供自身程序调用。
以下是该类实现:
#include <chrono> // for std::chrono functions
class Timer
{
private:
// Type aliases to make accessing nested type easier
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();
}
};
就是这样!要使用它,我们只需在主函数开头(或任何需要开始计时的位置)实例化一个Timer对象,然后在需要知道程序运行到当前点所耗时间时,调用elapsed()成员函数即可。
#include <iostream>
int main()
{
Timer t;
// Code to time goes here
std::cout << "Time elapsed: " << t.elapsed() << " seconds\n";
return 0;
}
现在,让我们通过一个实际示例来演示如何对包含10000个元素的数组进行排序。首先,我们使用前一章开发的选别排序算法:
#include <array>
#include <chrono> // for std::chrono functions
#include <cstddef> // for std::size_t
#include <iostream>
#include <numeric> // for std::iota
const int g_arrayElements { 10000 };
class Timer
{
private:
// Type aliases to make accessing nested type easier
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)
{
// Step through each element of the array
// (except the last one, which will already be sorted by the time we get there)
for (std::size_t startIndex{ 0 }; startIndex < (g_arrayElements - 1); ++startIndex)
{
// smallestIndex is the index of the smallest element we’ve encountered this iteration
// Start by assuming the smallest element is the first element of this iteration
std::size_t smallestIndex{ startIndex };
// Then look for a smaller element in the rest of the array
for (std::size_t currentIndex{ startIndex + 1 }; currentIndex < g_arrayElements; ++currentIndex)
{
// If we've found an element that is smaller than our previously found smallest
if (array[currentIndex] < array[smallestIndex])
{
// then keep track of it
smallestIndex = currentIndex;
}
}
// smallestIndex is now the smallest element in the remaining array
// swap our start element with our smallest element (this sorts it into the correct place)
std::swap(array[startIndex], array[smallestIndex]);
}
}
int main()
{
std::array<int, g_arrayElements> array;
std::iota(array.rbegin(), array.rend(), 1); // fill the array with values 10000 to 1
Timer t;
sortArray(array);
std::cout << "Time taken: " << t.elapsed() << " seconds\n";
return 0;
}



在作者的机器上,三次运行分别得到0.0507、0.0506和0.0498的耗时。因此我们可以将其近似为0.05秒。
现在,让我们使用标准库中的std::sort进行相同的测试。
#include <algorithm> // for std::sort
#include <array>
#include <chrono> // for std::chrono functions
#include <cstddef> // for std::size_t
#include <iostream>
#include <numeric> // for std::iota
const int g_arrayElements { 10000 };
class Timer
{
private:
// Type aliases to make accessing nested type easier
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); // fill the array with values 10000 to 1
Timer t;
std::ranges::sort(array); // Since C++20
// If your compiler isn't C++20-capable
// std::sort(array.begin(), array.end());
std::cout << "Time taken: " << t.elapsed() << " seconds\n";
return 0;
}



在作者的机器上,这产生了以下结果:0.000693、0.000692 和 0.000699。因此基本都在 0.0007 左右。
换言之,在此情况下,std::sort 的运行速度比我们自己编写的选择排序快了 100 倍!
影响程序性能的因素
测量程序运行时间相当简单,但多种因素会显著影响结果。了解如何正确测量以及哪些因素会影响计时至关重要。
首先,请确保使用的是发布构建目标而非调试构建目标。调试构建目标通常会关闭优化功能,而优化对结果的影响可能非常显著。例如,在作者机器上使用调试构建目标运行上述 std::sort 示例耗时 0.0235 秒——耗时长达 33 倍!
其次,系统后台运行的其他任务可能影响计时结果。请确保系统未进行任何占用CPU、内存或硬盘资源的操作(如运行游戏、搜索文件、执行杀毒扫描或后台安装更新)。看似无害的操作(如闲置的网页浏览器)也可能导致CPU使用率瞬间飙升至100%——当活动标签页轮换新广告横幅时,浏览器需解析大量JavaScript代码。测量前关闭的应用程序越多,结果波动性越小。
第三,若程序使用随机数生成器,生成的随机数序列可能影响计时结果。例如对随机数数组进行排序时,每次运行的排序所需交换次数不同,导致排序结果存在差异。为获得更稳定的测试结果,可通过固定数值(而非std::random_device或系统时钟)临时初始化随机数生成器,使其每次运行产生相同序列。但需注意:若程序性能高度依赖随机序列特性,此操作可能导致整体结果失真。
第四,确保计时过程中不包含等待用户输入的时间,因为用户输入耗时不应纳入计时考量。若需用户输入,请考虑采用无需等待用户响应的输入方式(如命令行输入、文件读取,或设计绕过输入的代码路径)。
性能测量
在测量程序性能时,至少收集3组结果。若结果均相似,则很可能代表该程序在该机器上的实际性能。否则,请继续测量直至获得一组相似的结果(并理解哪些其他结果属于异常值)。由于系统在某些运行期间执行后台操作,出现一个或多个异常值的情况并不罕见。
若结果存在较大波动(且未形成明显聚类),则表明程序可能受到系统其他活动显著影响,或存在应用程序内部随机化的影响。
由于性能测量受诸多因素影响(尤其是硬件速度,同时还包括操作系统、运行中的应用程序等),绝对性能指标(例如“程序运行耗时10秒”)通常仅适用于评估程序在特定目标机器上的运行表现。在其他机器上,同一程序可能耗时1秒、10秒甚至1分钟。若未在不同硬件配置上进行全面测试,则难以准确判断。
但在单台机器上,相对性能测量具有参考价值。通过收集程序不同变体的运行结果,可确定哪个变体性能最优。例如:若变体1耗时10秒,变体2耗时8秒,则变体2在所有类似机器上都可能更快,无论该机器的绝对速度如何。
测量第二种变体后,建议重新测量第一种变体进行合理性验证。若其结果与初始测量值一致,则两种变体的结果具有可比性。例如:若变体1耗时10秒,变体2耗时8秒,随后重新测量变体1仍得出10秒结果,则可合理推断两次测量均准确无误,且变体2确实更快。
但若首次变体结果与该变体的初始测量值不再一致,则表明机器上发生了影响性能的状况,此时难以区分测量差异源于变体本身还是机器状态。这种情况下应舍弃现有结果并重新测量。

浙公网安备 33010602011771号