在C++中获取准确的执行时间(微秒级)

51

我想要获取由C++编写的程序准确的微秒级别的执行时间。

我尝试使用clock_t来获取执行时间,但是它不够准确。

(请注意,微基准测试很困难。准确的计时器只是获得短时间区间的有意义结果所必需的一小部分。参见“性能评估的成语方式?”以了解更一般的注意事项)


2
你为什么觉得它不准确? - Digital_Reality
3
考虑到执行时间受CPU负载、内存可用性、缓存、各种可能的I/O操作、线程调度等因素影响,您确定需要那么高的精确度吗? - qdii
如果这里的某个答案解决了你的问题,请将其标记为已接受。这样,你的问题将不再出现在未回答的部分。 - OlivierLi
我需要一个微秒级别的执行时间,而clock_t只提供毫秒级别的执行时间... - user3323616
4个回答

106
如果您正在使用C++11或更高版本,可以使用std::chrono::high_resolution_clock。 一个简单的用例:
auto start = std::chrono::high_resolution_clock::now();
...
auto elapsed = std::chrono::high_resolution_clock::now() - start;

long long microseconds = std::chrono::duration_cast<std::chrono::microseconds>(
        elapsed).count();

这种解决方案的优点在于它具有可移植性。


请注意,微基准测试很难。很容易测量错误的内容(例如您的基准测试被优化掉),或者在计时区域中包含页面故障,或者未考虑 CPU 频率空闲与超频。

有关一些常规提示,请参见 评估性能的惯用方法?,例如通过首先测试另一个来进行理智检查,并查看是否更改了显示较快的内容。


3
注意:在GCC 4.8.1版本及以上的g++编译器中,可以使用纳秒(nanoseconds)功能。 - user2249683
9
为了测试一个程序在微秒级别上的性能,应该忽略设置和销毁部分。 - stefan
5
@qdii 实际上,不同的操作系统需要不同的设置时间。这就是为什么。如果你包括这一部分,那么你测量的不是应用程序的性能,而是操作系统的性能。除非你是操作系统开发人员,否则你无法对设置时间做任何事情。即使在一个操作系统上,硬盘也可能需要旋转才能读取可执行文件。你真的想精确到微秒测量吗? - stefan
2
@stefan 比如说,我可能想这么做是因为我是一名游戏开发者,我想知道我的游戏在 Windows 8 上启动需要多长时间。作为开发者,我对可执行文件的大小有影响:我可以将资源嵌入二进制可执行文件的部分中,去除符号,优化空间等。我认为问题需要澄清,以便确定要测量什么。 - qdii
3
@qdii 好的,那是一个应用程序。但是你真的会仅通过时间来衡量吗?你肯定需要使用分析工具来确定是哪个部分导致了高设置时间。任何程序的启动应该是“瞬间感觉到的”。我认为您不需要衡量这一点。 - stefan
显示剩余6条评论

29

以下是如何在C++中获取类似于C语言的毫秒、微秒和纳秒时间戳的方法:

新的C++11 std::chrono 库是我曾经见过并尝试使用的最复杂的一堆C++代码,但至少它是跨平台的!

因此,如果您想将其简化并使其更像"C语言",包括删除所有类型安全的类,这里有3个简单易用的函数可以获取毫秒、微秒和纳秒级别的时间戳……这只花了我大约12小时的时间来编写*:

NB:在下面的代码中,您可能会考虑使用std::chrono::steady_clock而不是std::chrono::high_resolution_clock。它们在这里(https://en.cppreference.com/w/cpp/chrono)的定义如下:

  • steady_clock (C++11) - 永远不会被调整的单调时钟
  • high_resolution_clock (C++11) - 可用最短滴答周期的时钟
#include <chrono>

// NB: ALL OF THESE 3 FUNCTIONS BELOW USE SIGNED VALUES INTERNALLY AND WILL
// EVENTUALLY OVERFLOW (AFTER 200+ YEARS OR SO), AFTER WHICH POINT THEY WILL
// HAVE *SIGNED OVERFLOW*, WHICH IS UNDEFINED BEHAVIOR (IE: A BUG) FOR C/C++.
// But...that's ok...this "bug" is designed into the C++11 specification, so
// whatever. Your machine won't run for 200 years anyway...

// Get time stamp in milliseconds.
uint64_t millis()
{
    uint64_t ms = std::chrono::duration_cast<std::chrono::milliseconds>(
            std::chrono::high_resolution_clock::now().time_since_epoch())
            .count();
    return ms; 
}

// Get time stamp in microseconds.
uint64_t micros()
{
    uint64_t us = std::chrono::duration_cast<std::chrono::microseconds>(
            std::chrono::high_resolution_clock::now().time_since_epoch())
            .count();
    return us; 
}

// Get time stamp in nanoseconds.
uint64_t nanos()
{
    uint64_t ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
            std::chrono::high_resolution_clock::now().time_since_epoch())
            .count();
    return ns; 
}

* (抱歉,我更多地是嵌入式开发人员而不是标准计算机程序员,所以所有这些高级、抽象的静态成员-类-命名空间-命名空间-命名空间的东西都让我感到困惑。别担心,我会变得更好的。)

问:为什么要使用std::chrono

答:因为C++程序员喜欢用一些疯狂的东西,所以他们让它来处理单位。以下是一些C++怪异行为和使用std::chrono的示例。请参考cppreference社区维基页面: https://en.cppreference.com/w/cpp/chrono/duration

enter image description here

所以你可以声明一个1秒的变量,并像这样无需转换就将其更改为微秒:
// Create a time object of type `std::chrono::seconds` & initialize it to 1 sec
std::chrono::seconds time_sec(1); 
// integer scale conversion with no precision loss: no cast
std::cout << std::chrono::microseconds(time_sec).count() << " microseconds\n";

你甚至可以像这样指定时间,这在我看来非常奇怪,过度了。C++14实际上已经重载了字符msusns等作为函数调用运算符,以此初始化各种类型的std::chrono对象:

enter image description here

auto time_sec = 1s; // <== notice the 's' inside the code there 
                    // to specify 's'econds!
// OR:
std::chrono::seconds time_sec = 1s;
// integer scale conversion with no precision loss: no cast
std::cout << std::chrono::microseconds(time_sec).count() << " microseconds\n";

Here are some more examples:

std::chrono::milliseconds time_ms = 1ms;
// OR:
auto time_ms = 1ms;

std::chrono::microseconds time_us = 1us;
// OR:
auto time_us = 1us;

std::chrono::nanoseconds time_ns = 1ns;
// OR:
auto time_ns = 1ns;

就我个人而言,我更愿意简化语言并像我已经做的那样执行此操作,以及在 C 和 C++ 中在几十年前就已经实现的方式:

// Notice the `_sec` at the end of the variable name to remind me this 
// variable has units of *seconds*!
uint64_t time_sec = 1; 

这里是一些参考资料:

Clock types (https://en.cppreference.com/w/cpp/chrono):
  1. system_clock 系统时钟
  2. steady_clock 稳定时钟
  3. high_resolution_clock 高精度时钟
  4. utc_clock UTC 时钟
  5. tai_clock TAI 时钟
  6. gps_clock GPS 时钟
  7. file_clock 文件时钟
  8. 等。
  • 在 C++ 中获取准确的执行时间(微秒)(由@OlivierLi 回答)
  • http://en.cppreference.com/w/cpp/chrono/time_point/time_since_epoch
  • http://en.cppreference.com/w/cpp/chrono/duration - 显示诸如小时、分钟、秒、毫秒等类型
  • http://en.cppreference.com/w/cpp/chrono/system_clock/now
  • 我需要观看的视频:

    1. CppCon 2016: Howard Hinnant “A <chrono> Tutorial"

    相关内容:

    我有三组时间戳函数(相互交叉链接):
    1. 对于C时间戳,请参见我的答案:如何在C中获取微秒级时间戳?
    2. 对于C++高分辨率时间戳,请参见我的答案:如何准确获取C++执行时间(微秒)?
    3. 对于Python高分辨率时间戳,请参见我的答案:如何在Python中获取毫秒和微秒分辨率的时间戳?
  • [我的答案] 使用运算符""s与gcc一起使用std::chrono--不要忘记using namespace声明以访问std::chrono::duration<>字面值

  • 附录

    "用户自定义字面量" (自C++11起)的更多内容:

    operator"" mysuffix() 运算符重载/用户自定义字面量/后缀函数(自C++11起)是上述奇怪的 auto time_ms = 1ms; 的工作原理。写 1ms 实际上是调用了函数 operator"" ms(),并将 1 作为输入参数传递进去,就像你写了这样一个函数调用: operator"" ms(1)。要了解更多关于这个概念的信息,请参见这里的参考页面:cppreference.com: 用户自定义字面量 (自C++11起)

    下面是一个基本示例,用于定义一个用户自定义字面量/后缀函数,并使用它:

    // 1. Define a function
    // used as conversion from degrees (input param) to radians (returned output)
    constexpr long double operator"" _deg(long double deg)
    {
        long double radians = deg * 3.14159265358979323846264L / 180;
        return radians;
    }
    
    // 2. Use it
    double x_rad = 90.0_deg;
    

    为什么不像C和C++几十年来一直使用的double x_rad = degToRad(90.0);这样做呢?我不知道。可能与C++程序员的思维方式有关吧。也许他们正在尝试使现代C++更具有Pythonic特性。

    这种魔法也是潜在非常有用的C++fmt库的工作原理,https://github.com/fmtlib/fmt在这里。它由Victor Zverovich编写,也是C++20的std::format的作者。您可以在这里看到函数detail::udl_formatter<char> operator"" _format(const char* s, size_t n)的定义here。它的用法如下:

    "Hello {}"_format("World");
    

    输出:

    你好,世界

    这将把字符串"World"插入到第一个字符串中的{}位置。以下是另一个示例:

    "I have {} eggs and {} chickens."_format(num_eggs, num_chickens);
    

    示例输出:

    我有29个鸡蛋和42只鸡。

    这与Python中的str.format字符串格式化非常相似。在此处阅读fmt库文档here


    3
    多年过去了,即使我现在已经专业地使用C++好几个月了,但我仍然对诸如秒、毫秒、微秒和纳秒等简单事物的不必要复杂性感到十分困惑(还有其他许多问题),这是由C++引入的。 - Gabriel Staples
    2
    你可以使用语言Y来编写X的语言习惯,但这可能会变得混乱。无论将哪两种语言替换为X和Y,这都是正确的。在这种情况下,是C和C++。以下是如何正确使用<chrono>的好例子:https://dev59.com/Nbjna4cB1Zd3GeqP6jz6#59446610。使其成为一个好例子的最大特点是不断地逃离chrono类型系统进入整数类型。 - Howard Hinnant
    谢谢你亲自回复,霍华德,我会看一下的。我现在正在学习许多复杂的C++方面。我还有很多要学习的。然而,我认为_C开发人员可以使用C++编译器编写更好、更清晰的"C"(我们称之为"C+")比他/她使用C编译器编写的代码更好_。C++有许多精彩的补充,但我发现大多数C++开发人员对它的使用过于复杂,特别是对于嵌入式系统来说。我希望看到C++ 编译器 在嵌入式系统中得到更广泛的应用,但不是使用每个现代C++语言特性和习惯用法。 - Gabriel Staples
    1
    就我个人而言,我曾经对 git 的感觉和你现在对 <chrono> 的感觉一模一样。但是,在一个朋友帮助我克服了最初的障碍之后,再加上 github.com 的加入,我现在不会使用任何其他的源代码控制系统。我能理解你的立场。 - Howard Hinnant
    感谢您提供这个惊人的答案。我有一个问题:为什么要使用high_resolution_clock?它总是最精确的吗?似乎有些人说它不是,比如这个答案。我猜@HowardHinnant也不推荐使用high_resolution_clock。那么...用毫秒或微秒测量时间的最佳方法是什么? - starriet
    显示剩余2条评论

    9

    如果您想知道在Unix shell中执行程序需要多长时间,请使用Linux time 命令,如下所示:

    time ./a.out 
    
    real    0m0.001s
    user    0m0.000s
    sys     0m0.000s
    

    其次,如果你想要测试程序代码(C)中执行多少条语句所需的时间,可以尝试使用gettimeofday()函数,使用如下:

    #include <sys/time.h>
    struct timeval  tv1, tv2;
    gettimeofday(&tv1, NULL);
    /* Program code to execute here */
    gettimeofday(&tv2, NULL);
    printf("Time taken in execution = %f seconds\n",
         (double) (tv2.tv_usec - tv1.tv_usec) / 1000000 +
         (double) (tv2.tv_sec - tv1.tv_sec));
    

    3
    使用clock_gettime()代替 - 《Man Pages》指出 - gettimeofday()返回的时间受到系统时间不连续跳跃的影响(例如,如果系统管理员手动更改系统时间)。 如果需要一个单调递增的时钟,请参见clock_gettime(2)。The Opengroup表示 - 应用程序应该使用clock_gettime()函数,而不是过时的gettimeofday()函数。 - Xofo

    1
    如果你使用的是Windows系统,你可以使用QueryPerformanceCounter
    请参阅如何在Visual C++中使用QueryPerformanceCounter函数计时代码
    __int64 ctr1 = 0, ctr2 = 0, freq = 0;
    int acc = 0, i = 0;
    
    // Start timing the code.
    if (QueryPerformanceCounter((LARGE_INTEGER *)&ctr1)!= 0)
    {
        // Code segment is being timed.
        for (i=0; i<100; i++) acc++;
    
        // Finish timing the code.
        QueryPerformanceCounter((LARGE_INTEGER *)&ctr2);
    
        Console::WriteLine("Start Value: {0}",ctr1.ToString());
        Console::WriteLine("End Value: {0}",ctr2.ToString());
    
        QueryPerformanceFrequency((LARGE_INTEGER *)&freq);
    
        Console::WriteLine(S"QueryPerformanceCounter minimum resolution: 1/{0} Seconds.",freq.ToString());
        // In Visual Studio 2005, this line should be changed to:     Console::WriteLine("QueryPerformanceCounter minimum resolution: 1/{0} Seconds.",freq.ToString()); 
        Console::WriteLine("100 Increment time: {0} seconds.",((ctr2 - ctr1) * 1.0 / freq).ToString());
    }
    else
    {
        DWORD dwError = GetLastError();
        Console::WriteLine(S"Error value = {0}",dwError.ToString());// In Visual Studio 2005, this line should be changed to: Console::WriteLine("Error value = {0}",dwError.ToString());
    }
    
    // Make the console window wait.
    Console::WriteLine();
    Console::Write("Press ENTER to finish.");
    Console::Read();
    
    return 0;
    

    你可以在整个进程生命周期内,或者在主函数周围调用CreateProcess(...)WaitForSingleObject(...)

    但他想要程序的执行时间,而不是函数。这包括通常无法访问的设置部分。 - qdii
    1
    @qdii:他可以将它放在对CreateProcess(...)WaitForSingleObject(...)的调用周围,以覆盖整个进程生命周期,否则放在主函数周围。这是一个合理的解决方案,我不认为它真的应该被否定... - parrowdice
    +1,尽管它依赖于平台。为什么?很简单:在该平台的几个编译器上,要么std::chrono::high_resolution_clock不可用,要么设置为不太高的计时器。这种特定的解决方案是必需的。 - stefan
    @parrowdice 说得好,如果你修改你的回答并包含那个内容,我会把我的踩转成赞。 - qdii
    Boost.Chrono在Windows上拥有一个不错的高分辨率时钟。 - R. Martinho Fernandes

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接