如何在C++中计算代码片段的执行时间

128

我需要计算一个C++代码片段的执行时间,以秒为单位。它必须能在Windows和Unix机器上运行。

我使用以下代码来实现这个目标。(在前面导入 )

clock_t startTime = clock();
// some code here
// to compute its execution duration in runtime
cout << double( clock() - startTime ) / (double)CLOCKS_PER_SEC<< " seconds." << endl;

然而,对于像 a = a + 1 这样的小输入或短语句,我得到了“0秒”的结果。我认为它必须是类似于 0.0000001 秒之类的东西。

我记得 Java 中的 System.nanoTime() 在这种情况下效果很好。但是我无法从 C++ 的 clock() 函数中获得完全相同的功能。

你有解决方案吗?


31
请记住,任何基于时间差比较的方法可能都不准确,因为操作系统可能不会从头到尾运行您的线程。它可能会中断它并与其他线程混合运行,这将显着影响完成您的操作所需的实际时间。您可以多次运行并平均结果;您可以最小化正在运行的其他进程的数量。但是,这些都无法完全消除线程挂起的影响。 - Mordachai
16
Mordachi,为什么你想要消除它?你希望看到你的函数在真实环境下的表现,而不是在一个魔法领域里,那里线程永远不会被中断。只要你运行几次并取平均值,它就会非常准确。 - Andreas Bonini
14
Andreas,如果原帖想比较他的代码与不同算法的性能表现,Mordachai的评论是相关的。例如,如果他今天下午运行了几次时钟测试,明天早上又测试了一个不同的算法,那么他的比较可能就不可靠了,因为他下午可能会与更多进程共享资源,而早晨则可能不会。或者也有可能一组代码会导致操作系统给它更少的处理时间。如果他想进行基于时间的比较,这种类型的性能测量不可靠的原因有很多。 - weberc2
4
我知道我正在回复一条旧评论,但是对于像我这样偶然看到这条评论的人来说,为了测试算法的性能,你需要取几次运行中的最短时间,而不是平均时间。 最短时间基本上是由于操作系统中的最少干扰所计时的代码时间。 - Baruch
这里有更多用于基准测试的代码片段:https://dev59.com/C2Eh5IYBdhLWcg3wdjTt - user9869932
显示剩余2条评论
18个回答

117
你可以使用我写的这个函数。你调用GetTimeMs64()函数,它返回自Unix时代以来使用系统时钟流逝的毫秒数,就像使用time(NULL)一样,只不过是以毫秒为单位。
它适用于Windows和Linux;它是线程安全的。
请注意,Windows的时间粒度为15毫秒;在Linux上则取决于实现方式,但通常也是15毫秒。
#ifdef _WIN32
#include <Windows.h>
#else
#include <sys/time.h>
#include <ctime>
#endif

/* Remove if already defined */
typedef long long int64; typedef unsigned long long uint64;

/* Returns the amount of milliseconds elapsed since the UNIX epoch. Works on both
 * windows and linux. */

uint64 GetTimeMs64()
{
#ifdef _WIN32
 /* Windows */
 FILETIME ft;
 LARGE_INTEGER li;

 /* Get the amount of 100 nano seconds intervals elapsed since January 1, 1601 (UTC) and copy it
  * to a LARGE_INTEGER structure. */
 GetSystemTimeAsFileTime(&ft);
 li.LowPart = ft.dwLowDateTime;
 li.HighPart = ft.dwHighDateTime;

 uint64 ret = li.QuadPart;
 ret -= 116444736000000000LL; /* Convert from file time to UNIX epoch time. */
 ret /= 10000; /* From 100 nano seconds (10^-7) to 1 millisecond (10^-3) intervals */

 return ret;
#else
 /* Linux */
 struct timeval tv;

 gettimeofday(&tv, NULL);

 uint64 ret = tv.tv_usec;
 /* Convert from micro seconds (10^-6) to milliseconds (10^-3) */
 ret /= 1000;

 /* Adds the seconds (10^0) after converting them to milliseconds (10^-3) */
 ret += (tv.tv_sec * 1000);

 return ret;
#endif
}

1
供日后参考:我只需将其放入头文件中并使用它。很高兴拥有它。 - Daniel Handojo
1
我相信如果系统时钟被更改,方法gettimeofday可能会给出意外的结果。如果这对您是一个问题,您可能需要考虑使用clock_gettime代替。 - Azmisov
这个 Windows 方法相对于 GetTickCount 有什么优势吗? - MicroVirus
不能使用 gcc -std=c99 编译。 - Assimilater
@MicroVirus:是的,GetTickCount 是自系统启动以来经过的时间,而我的函数返回自UNIX纪元以来的时间,这意味着您可以将其用于日期和时间。如果您只对两个事件之间经过的时间感兴趣,那么我的函数仍然是更好的选择,因为它是一个int64;GetTickCount是一个int32,并且每50天会溢出,这意味着如果您注册的两个事件在溢出之间,您可能会得到奇怪的结果。 - Andreas Bonini
对于Windows的最高分辨率:请参阅MSDN上的获取高分辨率时间戳。QueryPerformanceCounter是关键点,文档还展示了一个示例代码 - starriet

42

我有另一个可行的示例,它使用微秒(UNIX,POSIX等)。

    #include <sys/time.h>
    typedef unsigned long long timestamp_t;

    static timestamp_t
    get_timestamp ()
    {
      struct timeval now;
      gettimeofday (&now, NULL);
      return  now.tv_usec + (timestamp_t)now.tv_sec * 1000000;
    }

    ...
    timestamp_t t0 = get_timestamp();
    // Process
    timestamp_t t1 = get_timestamp();

    double secs = (t1 - t0) / 1000000.0L;

这是我们编写此代码的文件:

https://github.com/arhuaco/junkcode/blob/master/emqbit-bench/bench.c


5
你应该在示例开头添加#include <sys/time.h> - niekas

42
这里提供了一个简单的C++11解决方案,可以给您满意的分辨率。
#include <iostream>
#include <chrono>

class Timer
{
public:
    Timer() : beg_(clock_::now()) {}
    void reset() { beg_ = clock_::now(); }
    double elapsed() const { 
        return std::chrono::duration_cast<second_>
            (clock_::now() - beg_).count(); }

private:
    typedef std::chrono::high_resolution_clock clock_;
    typedef std::chrono::duration<double, std::ratio<1> > second_;
    std::chrono::time_point<clock_> beg_;
};

在*nix系统上,对于c++03。
#include <iostream>
#include <ctime>

class Timer
{
public:
    Timer() { clock_gettime(CLOCK_REALTIME, &beg_); }

    double elapsed() {
        clock_gettime(CLOCK_REALTIME, &end_);
        return end_.tv_sec - beg_.tv_sec +
            (end_.tv_nsec - beg_.tv_nsec) / 1000000000.;
    }

    void reset() { clock_gettime(CLOCK_REALTIME, &beg_); }

private:
    timespec beg_, end_;
};

以下是使用示例:
int main()
{
    Timer tmr;
    double t = tmr.elapsed();
    std::cout << t << std::endl;

    tmr.reset();
    t = tmr.elapsed();
    std::cout << t << std::endl;

    return 0;
}

来自 https://gist.github.com/gongzhitaao/7062087


我在使用你的C++11解决方案时遇到了这个错误:/usr/lib/x86_64-linux-gnu/libstdc++.so.6: version GLIBCXX_3.4.19 not found (required by ../cpu_2d/g500) - user9869932
@julianromera 你使用的是哪个平台?你是否安装了libstdc++库和g++? - gongzhitaao
这是一个Linux Ubuntu 12的Slurm网格。我刚刚修好了它。我在链接器末尾添加了-static-libstdc++。谢谢你的询问@gongzhitaao。 - user9869932

19
#include <boost/progress.hpp>

using namespace boost;

int main (int argc, const char * argv[])
{
  progress_timer timer;

  // do stuff, preferably in a 100x loop to make it take longer.

  return 0;
}

progress_timer超出范围时,它将打印自创建以来经过的时间。

更新:这里是一个不需要Boost即可工作的版本(在macOS/iOS上测试通过):

#include <chrono>
#include <string>
#include <iostream>
#include <math.h>
#include <unistd.h>

class NLTimerScoped {
private:
    const std::chrono::steady_clock::time_point start;
    const std::string name;

public:
    NLTimerScoped( const std::string & name ) : name( name ), start( std::chrono::steady_clock::now() ) {
    }


    ~NLTimerScoped() {
        const auto end(std::chrono::steady_clock::now());
        const auto duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>( end - start ).count();

        std::cout << name << " duration: " << duration_ms << "ms" << std::endl;
    }

};

int main(int argc, const char * argv[]) {

    {
        NLTimerScoped timer( "sin sum" );

        float a = 0.0f;

        for ( int i=0; i < 1000000; i++ ) {
            a += sin( (float) i / 100 );
        }

        std::cout << "sin sum = " << a << std::endl;
    }



    {
        NLTimerScoped timer( "sleep( 4 )" );

        sleep( 4 );
    }



    return 0;
}


2
这个可行,但请注意,progress_timer已被弃用(在boost 1.50之前的某个时候)- auto_cpu_timer可能更合适。 - davidA
3
@meowsqueak 嗯,auto_cpu_timer 看起来需要链接 Boost 系统库,所以它不再是一个只需头文件的解决方案了。有点遗憾...这使得其他选项突然变得更加吸引人。 - Tomas Andrle
1
是的,这是一个很好的观点,如果您还没有链接Boost,那么这会带来更多麻烦。但是,如果您已经链接了Boost,它可以很好地工作。 - davidA
@meowsqueak 是的,或者如果要进行一些快速基准测试,只需获取Boost的旧版本即可。 - Tomas Andrle
@TomasAndrle 这个链接已经不存在了。 - Zheng Qu

5

Windows提供了QueryPerformanceCounter()函数,而Unix则有gettimeofday()函数。这两个函数都可以测量至少1微秒的差异。


但是使用windows.h是受限制的。同样编译的源代码必须在Windows和Unix上运行。如何解决这个问题? - ahmet alp balkan
2
然后寻找一些包装库 - Captain Comic
5
“the same compiled source”听起来像是你想在两个系统上运行相同的二进制文件,但这似乎不是情况。如果你的意思是“相同的源代码”,那么使用#ifdef应该可以(根据你接受的答案来看是没问题的),然后我就不明白问题所在了:“#ifdef WIN32 #include <windows.h> ... #else ... #endif”。 - just somebody

3

(适用于Windows的解决方案)

目前(约在2017年左右),获取准确计时的方法是使用“QueryPerformanceCounter”。这种方法具有给出非常精确结果的好处,且被微软公司推荐。只需将代码块放入新的控制台应用程序中即可获得一个可工作的示例。这里有一篇详细的讨论:获取高分辨率时间戳

#include <iostream>
#include <tchar.h>
#include <windows.h>

int main()
{
constexpr int MAX_ITER{ 10000 };
constexpr __int64 us_per_hour{ 3600000000ull }; // 3.6e+09
constexpr __int64 us_per_min{ 60000000ull };
constexpr __int64 us_per_sec{ 1000000ull };
constexpr __int64 us_per_ms{ 1000ull };

// easy to work with
__int64 startTick, endTick, ticksPerSecond, totalTicks = 0ull;

QueryPerformanceFrequency((LARGE_INTEGER *)&ticksPerSecond);

for (int iter = 0; iter < MAX_ITER; ++iter) {// start looping
    QueryPerformanceCounter((LARGE_INTEGER *)&startTick); // Get start tick
    // code to be timed
    std::cout << "cur_tick = " << iter << "\n";
    QueryPerformanceCounter((LARGE_INTEGER *)&endTick); // Get end tick
    totalTicks += endTick - startTick; // accumulate time taken
}

// convert to elapsed microseconds
__int64 totalMicroSeconds =  (totalTicks * 1000000ull)/ ticksPerSecond;

__int64 hours = totalMicroSeconds / us_per_hour;
totalMicroSeconds %= us_per_hour;
__int64 minutes = totalMicroSeconds / us_per_min;
totalMicroSeconds %= us_per_min;
__int64 seconds = totalMicroSeconds / us_per_sec;
totalMicroSeconds %= us_per_sec;
__int64 milliseconds = totalMicroSeconds / us_per_ms;
totalMicroSeconds %= us_per_ms;


std::cout << "Total time: " << hours << "h ";
std::cout << minutes << "m " << seconds << "s " << milliseconds << "ms ";
std::cout << totalMicroSeconds << "us\n";

return 0;
}

3
在我编写的一些程序中,我使用RDTS来实现这个目的。RDTSC不是关于时间的,而是关于处理器启动以来的周期数。您必须在系统上进行校准才能获得秒为单位的结果,但当您想评估性能时,它真的很方便,直接使用周期数比尝试将它们转换回秒更好。
(上面的链接是到法语维基百科页面,但它有C++代码示例,英文版本在这里

2
一个完整且可靠的线程调度解决方案,应该是每个测试都能产生相同时间的结果,就是将程序编译成独立于操作系统的程序,并启动计算机以在无操作系统环境下运行程序。然而,这在很大程度上是不切实际的,最好也很困难。
一个很好的替代方法是将当前线程的亲和性设置为1个核心,并将优先级设置为最高。这种替代方法应该提供足够一致的结果。
假设您在最终生成的构建中使用了-Ofast(或者至少使用了-O3),并忽略了“死”代码消除的问题,与-Ofast相比,-Og执行非常少的优化;因此,-Og可能会误导代码在最终产品中的实际速度。
此外,所有速度测试(在某种程度上)都会伪造:在使用-Ofast编译的最终生产产品中,每个代码片段/部分/函数都不是孤立的;相反,每个代码片段都不断地流入下一个,从而使编译器有可能从各个地方加入、合并和优化代码片段。
同时,如果您正在对重度使用realloc()的代码片段进行基准测试,那么在内存碎片化严重的生产产品中,该代码片段可能会运行得更慢。因此,表达式“整体大于部分之和”适用于这种情况,因为最终生成的构建中的代码可能比您正在进行速度测试的单个代码片段运行得明显更快或更慢。
可以减少不一致性的部分解决方案是在速度测试中使用-Ofast,并向涉及测试的变量添加asm volatile("" :: "r"(var)),以防止死代码/循环消除。
以下是如何在Windows计算机上对平方根函数进行基准测试的示例。
// set USE_ASM_TO_PREVENT_ELIMINATION  to 0 to prevent `asm volatile("" :: "r"(var))`
// set USE_ASM_TO_PREVENT_ELIMINATION  to 1 to enforce `asm volatile("" :: "r"(var))`
#define USE_ASM_TO_PREVENT_ELIMINATION 1

#include <iostream>
#include <iomanip>
#include <cstdio>
#include <chrono>
#include <cmath>
#include <windows.h>
#include <intrin.h>
#pragma intrinsic(__rdtsc)
#include <cstdint>

class Timer {
public:
    Timer() : beg_(clock_::now()) {}
    void reset() { beg_ = clock_::now(); }
    double elapsed() const { 
        return std::chrono::duration_cast<second_>
            (clock_::now() - beg_).count(); }
private:
    typedef std::chrono::high_resolution_clock clock_;
    typedef std::chrono::duration<double, std::ratio<1> > second_;
    std::chrono::time_point<clock_> beg_;
};

unsigned int guess_sqrt32(register unsigned int n) {
    register unsigned int g = 0x8000;
    if(g*g > n) {
        g ^= 0x8000;
    }
    g |= 0x4000;
    if(g*g > n) {
        g ^= 0x4000;
    }
    g |= 0x2000;
    if(g*g > n) {
        g ^= 0x2000;
    }
    g |= 0x1000;
    if(g*g > n) {
        g ^= 0x1000;
    }
    g |= 0x0800;
    if(g*g > n) {
        g ^= 0x0800;
    }
    g |= 0x0400;
    if(g*g > n) {
        g ^= 0x0400;
    }
    g |= 0x0200;
    if(g*g > n) {
        g ^= 0x0200;
    }
    g |= 0x0100;
    if(g*g > n) {
        g ^= 0x0100;
    }
    g |= 0x0080;
    if(g*g > n) {
        g ^= 0x0080;
    }
    g |= 0x0040;
    if(g*g > n) {
        g ^= 0x0040;
    }
    g |= 0x0020;
    if(g*g > n) {
        g ^= 0x0020;
    }
    g |= 0x0010;
    if(g*g > n) {
        g ^= 0x0010;
    }
    g |= 0x0008;
    if(g*g > n) {
        g ^= 0x0008;
    }
    g |= 0x0004;
    if(g*g > n) {
        g ^= 0x0004;
    }
    g |= 0x0002;
    if(g*g > n) {
        g ^= 0x0002;
    }
    g |= 0x0001;
    if(g*g > n) {
        g ^= 0x0001;
    }
    return g;
}

unsigned int empty_function( unsigned int _input ) {
    return _input;
}

unsigned long long empty_ticks=0;
double empty_seconds=0;
Timer my_time;

template<unsigned int benchmark_repetitions>
void benchmark( char* function_name, auto (*function_to_do)( auto ) ) {
    register unsigned int i=benchmark_repetitions;
    register unsigned long long start=0;
    my_time.reset();
    start=__rdtsc();
    while ( i-- ) {
        auto result = (*function_to_do)( i << 7 );
        #if USE_ASM_TO_PREVENT_ELIMINATION == 1
            asm volatile("" :: "r"(
                // There is no data type in C++ that is smaller than a char, so it will
                //  not throw a segmentation fault error to reinterpret any arbitrary
                //  data type as a char. Although, the compiler might not like it.
                result
            ));
        #endif
    }
    if ( function_name == nullptr ) {
        empty_ticks = (__rdtsc()-start);
        empty_seconds = my_time.elapsed();
        std::cout<< "Empty:\n" << empty_ticks
              << " ticks\n" << benchmark_repetitions << " repetitions\n"
               << std::setprecision(15) << empty_seconds
                << " seconds\n\n";
    } else {
        std::cout<< function_name<<":\n" << (__rdtsc()-start-empty_ticks)
              << " ticks\n" << benchmark_repetitions << " repetitions\n"
               << std::setprecision(15) << (my_time.elapsed()-empty_seconds)
                << " seconds\n\n";
    }
}


int main( void ) {
    void* Cur_Thread=   GetCurrentThread();
    void* Cur_Process=  GetCurrentProcess();
    unsigned long long  Current_Affinity;
    unsigned long long  System_Affinity;
    unsigned long long furthest_affinity;
    unsigned long long nearest_affinity;
    
    if( ! SetThreadPriority(Cur_Thread,THREAD_PRIORITY_TIME_CRITICAL) ) {
        SetThreadPriority( Cur_Thread, THREAD_PRIORITY_HIGHEST );
    }
    if( ! SetPriorityClass(Cur_Process,REALTIME_PRIORITY_CLASS) ) {
        SetPriorityClass( Cur_Process, HIGH_PRIORITY_CLASS );
    }
    GetProcessAffinityMask( Cur_Process, &Current_Affinity, &System_Affinity );
    furthest_affinity = 0x8000000000000000ULL>>__builtin_clzll(Current_Affinity);
    nearest_affinity  = 0x0000000000000001ULL<<__builtin_ctzll(Current_Affinity);
    SetProcessAffinityMask( Cur_Process, furthest_affinity );
    SetThreadAffinityMask( Cur_Thread, furthest_affinity );
    
    const int repetitions=524288;
    
    benchmark<repetitions>( nullptr, empty_function );
    benchmark<repetitions>( "Standard Square Root", standard_sqrt );
    benchmark<repetitions>( "Original Guess Square Root", original_guess_sqrt32 );
    benchmark<repetitions>( "New Guess Square Root", new_guess_sqrt32 );
    
    
    SetThreadPriority( Cur_Thread, THREAD_PRIORITY_IDLE );
    SetPriorityClass( Cur_Process, IDLE_PRIORITY_CLASS );
    SetProcessAffinityMask( Cur_Process, nearest_affinity );
    SetThreadAffinityMask( Cur_Thread, nearest_affinity );
    for (;;) { getchar(); }
    
    return 0;
}

此外,感谢Mike Jarvis提供的计时器。

请注意(非常重要):如果您将运行更大的代码片段,则必须降低迭代次数,以防止计算机冻结。


2
除了禁用优化之外,回答很好。对-O0代码进行基准测试是一种浪费时间的行为,因为与正常的-O2-O3 -march=native相比,-O0的开销会因代码和工作负载而异。例如,额外命名的tmp变量在-O0下会增加时间成本。还有其他方法可以避免被优化掉的情况,例如使用volatile、非内联函数或空的内联汇编语句来隐藏优化器中的内容。 -O0甚至不能使用,因为代码在-O0下具有不同的瓶颈,而不是相同但更糟糕的瓶颈。 - Peter Cordes
1
哎呀,对于某些代码来说,-Og 仍然不是很现实。至少 -O2,最好是 -O3 更为现实。使用 asm volatile("" ::: "+r"(var)) 或其他方法使编译器将一个值存储在寄存器中,并通过此方式打败常量传播。 - Peter Cordes
@PeterCordes 再次感谢您的见解。我已经使用了 -O3 更新了内容,并在代码片段中使用了 asm volatile("" ::: "+r"(var)) - Jack G
1
asm volatile("" ::: "+r"( i )); 似乎是不必要的。在优化后的代码中,没有理由强制编译器将循环内的 ii<<7 材料化。这会防止它优化为每次移位而不是 tmp -= 128。如果函数调用的结果是非 void 的,则使用函数调用的结果是好的。例如:int result = (*function_to_do)( i << 7 );。你可以在该结果上使用 asm 语句。 - Peter Cordes
这段代码能编译通过吗?你没有定义 standard_sqrt,而且我认为 (int) (reinterpret_cast<char>(result)) 不是一个左值。("+r" 是一个读写寄存器操作数;你可能只需要 asm volatile("" :: "r"(input_var) ) 而不是 asm volatile("" : "+r"(compiler_assumes_this_is_modified) )。顺便说一下,我打错字了:输出操作数在第一个冒号后面,破坏项在第三个冒号后面。所以 asm("" ::: "memory") 是一个内存屏障,但你不能把 "+r"(var) 放在那里。请参见 https://stackoverflow.com/tags/inline-assembly/info。 - Peter Cordes
显示剩余2条评论

2

我建议使用标准库函数来获取系统时间信息。

如果你想要更精确的分辨率,那么执行更多次数的迭代。不是只运行程序一次并获得样本,而是运行1000次或更多次。


2
最好多次运行内部循环并仅在性能计时一次后通过分割内部循环重复平均值,而不是多次运行整个过程(循环+性能计时)并平均。这将减少性能计时代码与实际分析部分的开销。
为适当的系统包装计时器调用。对于Windows,QueryPerformanceCounter非常快速且“安全”使用。
您也可以在任何现代X86 PC上使用“rdtsc”,但某些多核机器上可能存在问题(核心跳跃可能会更改定时器),或者如果您打开了某种速度步进功能。

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