使用C++编写计时器函数以提供纳秒级时间

111

我希望能够计算API返回值所需的时间。 这种操作所需的时间是以纳秒为单位的。由于API是一个C++类或函数,因此我将使用timer.h来进行计算:

  #include <ctime>
  #include <iostream>

  using namespace std;

  int main(int argc, char** argv) {

      clock_t start;
      double diff;
      start = clock();
      diff = ( std::clock() - start ) / (double)CLOCKS_PER_SEC;
      cout<<"printf: "<< diff <<'\n';

      return 0;
  }

以上代码给出了时间的秒数。如何以纳秒为单位获得相同的时间,并获得更高的精度?


以上代码计算的是秒数,我想要得到纳秒的答案... - gagneet
1
需要将平台添加到问题中(最好也加入标题),以获得一个好的答案。 - Patrick Johnmeyer
除了获取时间之外,还需要查找微基准测试问题(这非常复杂)-仅执行一次并在开始和结束时获取时间,可能无法提供足够的精度。 - Blaisorblade
@Blaisorblade:特别是在我的一些测试中发现,clock()并不像我想象的那样快。 - Mooing Duck
17个回答

86

关于在循环中多次运行函数的问题,其他人的发言是正确的。

对于Linux(和BSD),您需要使用 clock_gettime()

#include <sys/time.h>

int main()
{
   timespec ts;
   // clock_gettime(CLOCK_MONOTONIC, &ts); // Works on FreeBSD
   clock_gettime(CLOCK_REALTIME, &ts); // Works on Linux
}

对于 Windows 系统,您需要使用 QueryPerformanceCounter。这里还有更多关于 QPC 的内容。

显然,在某些芯片组上存在已知的 问题,因此您可能希望确保您没有这些芯片组。此外,一些双核 AMD 处理器也可能会导致 问题。请参见 sebbbi 的第二篇帖子,他在其中指出:

QueryPerformanceCounter()和QueryPerformanceFrequency()提供了更好的分辨率,但存在不同的问题。例如,在Windows XP中,所有AMD Athlon X2双核CPU返回任何一个核心的PC“随机”(PC有时会稍微往回跳),除非您专门安装AMD双核驱动程序包以解决此问题。我们没有注意到其他双核及以上的CPU存在类似问题(p4双核,p4 ht,core2双核,core2四核,Phenom四核)。

编辑2013/07/16:

根据http://msdn.microsoft.com/en-us/library/windows/desktop/ee417693(v=vs.85).aspx所述,在某些情况下,对于QPC的有效性存在争议。

虽然QueryPerformanceCounter和QueryPerformanceFrequency通常会调整多个处理器,但BIOS或驱动程序中的错误可能导致这些例程在线程从一个处理器移动到另一个处理器时返回不同的值...

然而,这个StackOverflow答案https://dev59.com/anRB5IYBdhLWcg3wyqKo#4588605表明QPC在Win XP服务包2之后的任何MS操作系统上都可以正常工作。

本文显示Windows 7可以确定处理器是否具有不变TSC,并在它们没有时回退到外部计时器。http://performancebydesign.blogspot.com/2012/03/high-resolution-clocks-and-timers-for.html跨处理器的同步仍然是一个问题。

与计时器相关的其他好文章:

更多详细信息请参见注释。


1
我曾在一台旧的双路 Xeon PC 上看到 TSC 时钟偏差,但远不及启用 C1 时钟斜坡的 Athlon X2 那么严重。启用 C1 时钟斜坡后,执行 HLT 指令会减慢时钟速度,导致空闲核心上的 TSC 增量比活动核心上的增量更慢。 - bk1e
6
CLOCK_MONOTONIC 在我可用的 Linux 版本上可行。 - Bernard
1
@Bernard - 自我上次查看此内容后,那一定是新添加的。谢谢你提醒我。 - grieve
3
实际上,如果可用,您需要使用“CLOCK_MONOTONIC_RAW”来获得未经NTP调整的硬件时间。 - user405725
1
@grieve:QPC API页面 - http://msdn.microsoft.com/en-us/library/ms644904(v=vs.85).aspx - 表示“由于基本输入/输出系统(BIOS)或硬件抽象层(HAL)中的错误,您可能会在不同处理器上获得不同的结果。”微软指责固件-仍然不可靠。另一个页面,“构建日期:6/12/2013”,表示可靠的QPC使用需要“3.在单个线程上计算所有时间。” / “4...最好将线程保持在单个处理器上。”:http://msdn.microsoft.com/en-us/library/windows/desktop/ee417693(v=vs.85).aspx - 没有提到在最近的Windows版本上是否可以使用。 - Tony Delroy
显示剩余6条评论

71

这个新的答案使用了C++11的<chrono>库。虽然有其他的答案展示了如何使用<chrono>,但是没有一个答案展示了如何将<chrono>与其他答案中提及的RDTSC工具结合使用。因此,我想展示如何将RDTSC<chrono>结合使用。此外,我还将演示如何在时钟上使用模板化测试代码,以便您可以快速地在RDTSC和系统内置时钟设施之间切换(这些设施可能基于clock()clock_gettime()和/或QueryPerformanceCounter)。

请注意,RDTSC指令是x86特定的。QueryPerformanceCounter仅适用于Windows。clock_gettime()仅适用于POSIX。下面我将介绍两个新时钟:std::chrono::high_resolution_clockstd::chrono::system_clock,它们现在是跨平台的,如果您能假定使用C++11的话。

首先,这里是如何利用Intel的rdtsc汇编指令创建一个兼容C++11的时钟。我将其称为x::clock

#include <chrono>

namespace x
{

struct clock
{
    typedef unsigned long long                 rep;
    typedef std::ratio<1, 2'800'000'000>       period; // My machine is 2.8 GHz
    typedef std::chrono::duration<rep, period> duration;
    typedef std::chrono::time_point<clock>     time_point;
    static const bool is_steady =              true;

    static time_point now() noexcept
    {
        unsigned lo, hi;
        asm volatile("rdtsc" : "=a" (lo), "=d" (hi));
        return time_point(duration(static_cast<rep>(hi) << 32 | lo));
    }
};

}  // x
此时钟所做的只是计算CPU周期并将其存储在一个无符号64位整数中。您可能需要调整汇编语言语法以适应您的编译器。或者你的编译器可以提供一个内置函数来代替(例如:now() {return __rdtsc();})。
要构建一个时钟,您必须给它表示(存储类型)。您还必须提供时钟周期,它必须是编译时常量,即使您的机器在不同的电源模式下可能会更改时钟速度。从这些信息中,您可以轻松地定义时钟的“本地”时间持续时间和时间点。
如果您只想输出时钟周期数,则实际上无论您为时钟周期数提供什么数字都没有关系。只有在您想把时钟周期数转换为某个实时单位(如纳秒)时,此常量才起作用。在这种情况下,您越能够准确提供时钟速度,转换为纳秒(毫秒等)的精度就越高。
以下是示例代码,展示了如何使用x::clock。实际上,我已经在时钟上为代码进行了模板化,因为我想展示您可以使用许多不同的时钟具有完全相同的语法。此特定测试显示在循环下运行要计时的内容时的循环开销:
#include <iostream>

template <class clock>
void
test_empty_loop()
{
    // Define real time units
    typedef std::chrono::duration<unsigned long long, std::pico> picoseconds;
    // or:
    // typedef std::chrono::nanoseconds nanoseconds;
    // Define double-based unit of clock tick
    typedef std::chrono::duration<double, typename clock::period> Cycle;
    using std::chrono::duration_cast;
    const int N = 100000000;
    // Do it
    auto t0 = clock::now();
    for (int j = 0; j < N; ++j)
        asm volatile("");
    auto t1 = clock::now();
    // Get the clock ticks per iteration
    auto ticks_per_iter = Cycle(t1-t0)/N;
    std::cout << ticks_per_iter.count() << " clock ticks per iteration\n";
    // Convert to real time units
    std::cout << duration_cast<picoseconds>(ticks_per_iter).count()
              << "ps per iteration\n";
}
该代码的第一步是创建一个“实时”单位以显示结果。我选择了皮秒,但您可以选择任何您喜欢的单位,无论是整数还是基于浮点数的单位。例如,有一个预制的std :: chrono :: nanoseconds单位可以使用。
另一个例子是我想打印每次迭代的平均时钟周期数作为浮点数,因此我创建了另一个基于double的持续时间,它具有与时钟的滴答声相同的单位(在代码中称为Cycle)。
循环使用对两侧的clock :: now()的调用计时。如果您想命名从此函数返回的类型,则为:
typename clock::time_point t0 = clock::now();

(正如在 x::clock 示例中清晰地显示的那样,以及系统提供的时钟一样)

要获取浮点时钟滴答的持续时间,只需减去两个时间点,要获取每次迭代的值,请将该持续时间除以迭代次数。

您可以使用count()成员函数来获取任何持续时间的计数。 这将返回内部表示。 最后,我使用std::chrono::duration_cast将持续时间Cycle转换为持续时间picoseconds并打印出来。

使用此代码很简单:

int main()
{
    std::cout << "\nUsing rdtsc:\n";
    test_empty_loop<x::clock>();

    std::cout << "\nUsing std::chrono::high_resolution_clock:\n";
    test_empty_loop<std::chrono::high_resolution_clock>();

    std::cout << "\nUsing std::chrono::system_clock:\n";
    test_empty_loop<std::chrono::system_clock>();
}

我使用我们自制的 x::clock 进行测试,并将这些结果与使用两个系统提供的时钟进行比较:std::chrono::high_resolution_clockstd::chrono::system_clock。对于我来说,打印出来的结果是:

Using rdtsc:
1.72632 clock ticks per iteration
616ps per iteration

Using std::chrono::high_resolution_clock:
0.620105 clock ticks per iteration
620ps per iteration

Using std::chrono::system_clock:
0.00062457 clock ticks per iteration
624ps per iteration

这表明每个时钟具有不同的滴答周期,因为每个时钟的每次迭代的滴答数差别非常大。然而,当转换为已知的时间单位(例如皮秒)时,每个时钟的结果大致相同(您的结果可能会有所不同)。

请注意,我的代码完全没有“魔法转换常量”。实际上,在整个示例中只有两个神奇数字:

  1. 我的机器的时钟速度,以定义x::clock
  2. 要测试的迭代次数。如果更改此数字会导致结果大不相同,则应该将迭代次数增加,或在测试时清空计算机上的竞争进程。

7
“RDTSC只适用于英特尔”的意思实际上指的是x86架构及其派生版,对吧? AMD、Cyrix、Transmeta的x86芯片都有该指令,而英特尔的RISC和ARM处理器则没有。 - Ben Voigt
2
@BenVoigt:+1 是的,你的更正非常正确,谢谢。 - Howard Hinnant
1
CPU限制会对此产生什么影响?时钟速度不是根据CPU负载而变化的吗? - Tejas Kale
@TejasKale:这在回答中用两个连续段落描述,开始于“要构建一个时钟...”。通常计时代码不会测量阻塞线程的工作(但它可以)。因此,通常您的CPU不会节流。但是,如果您正在测量涉及睡眠、互斥锁、条件变量等的代码,则rdtsc时钟可能会对其他单位进行不准确的转换。最好设置您的测量方式,以便您可以轻松更改和比较时钟(如本答案所示)。 - Howard Hinnant

31

带有这种精度,最好使用CPU时钟而不是系统调用例如clock()进行推理。不要忘记,如果执行指令需要超过一纳秒的时间......拥有纳秒级别的准确性几乎是不可能的。

尽管如此,类似于此的东西是一个开始:

以下是检索自上次启动CPU以来经过的80x86 CPU时钟计数器的实际代码。它适用于Pentium及以上(不支持386/486)。这段代码实际上是MS Visual C++特定的,但可以很容易地移植到其他任何支持内联汇编的编译器中。

inline __int64 GetCpuClocks()
{

    // Counter
    struct { int32 low, high; } counter;

    // Use RDTSC instruction to get clocks count
    __asm push EAX
    __asm push EDX
    __asm __emit 0fh __asm __emit 031h // RDTSC
    __asm mov counter.low, EAX
    __asm mov counter.high, EDX
    __asm pop EDX
    __asm pop EAX

    // Return result
    return *(__int64 *)(&counter);

}

这个函数还有一个极快的优点——通常执行不超过50个CPU周期。 使用计时数据
如果您需要将时钟计数转换为实际经过的时间,请将结果除以芯片的时钟速度。请记住,“额定”GHz可能与芯片的实际速度略有不同。要检查芯片的真实速度,可以使用几个非常好的实用程序或Win32调用QueryPerformanceFrequency()。

谢谢提供信息,这很有用。我没有考虑到计算时间所需的 CPU 周期,我认为这是一个需要记在心里的非常好的观点 :-) - gagneet
5
使用QueryPerformanceFrequency()将TSC计数转换为经过的时间可能无法正常工作。查询性能计数器(QueryPerformanceCounter())在可用时会在Vista上使用HPET(高精度事件计时器)。如果用户在boot.ini中添加/USEPMTIMER,则它将使用ACPI电源管理计时器。 - bk1e

25
为了正确地完成这个任务,你可以使用两种方法之一,要么使用 RDTSC,要么使用 clock_gettime()。第二种方法速度大约快2倍,并且有一个优点是可以提供正确的绝对时间。请注意,为了使 RDTSC 正确工作,您需要按照指示使用它(此页面上的其他评论存在错误,并且可能会导致某些处理器产生不正确的计时值)。
inline uint64_t rdtsc()
{
    uint32_t lo, hi;
    __asm__ __volatile__ (
      "xorl %%eax, %%eax\n"
      "cpuid\n"
      "rdtsc\n"
      : "=a" (lo), "=d" (hi)
      :
      : "%ebx", "%ecx" );
    return (uint64_t)hi << 32 | lo;
}

对于 clock_gettime 函数:(我随意选择了微秒分辨率)

#include <time.h>
#include <sys/timeb.h>
// needs -lrt (real-time lib)
// 1970-01-01 epoch UTC time, 1 mcs resolution (divide by 1M to get time_t)
uint64_t ClockGetTime()
{
    struct timespec ts;
    clock_gettime(CLOCK_REALTIME, &ts);
    return (uint64_t)ts.tv_sec * 1000000LL + (uint64_t)ts.tv_nsec / 1000LL;
}

时间和产生的数值:

Absolute values:
rdtsc           = 4571567254267600
clock_gettime   = 1278605535506855

Processing time: (10000000 runs)
rdtsc           = 2292547353
clock_gettime   = 1031119636

需要将fyi更改为"struct timespec"。 - Goblinhack
@Goblinhack 这对于C语言是必需的,对吗? - Marius
公平的观点 - 应该读标题 :) - Goblinhack
1
无论如何,每个十年我都会在这个问题上摇摆不定。 :P - Marius

24

我正在使用以下代码来获得所需的结果:

#include <time.h>
#include <iostream>
using namespace std;

int main (int argc, char** argv)
{
    // reset the clock
    timespec tS;
    tS.tv_sec = 0;
    tS.tv_nsec = 0;
    clock_settime(CLOCK_PROCESS_CPUTIME_ID, &tS);
    ...
    ... <code to check for the time to be put here>
    ...
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &tS);
    cout << "Time taken is: " << tS.tv_sec << " " << tS.tv_nsec << endl;

    return 0;
}

2
我点了踩,因为当我尝试应用这段代码时,我不得不先谷歌一下timespec为什么没有定义。然后我又不得不谷歌POSIX是什么...所以据我所理解,这段代码对于想要坚持使用标准库的Windows用户来说并不相关。 - Daniel Katz

8

对于 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,

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


5
一般来说,如果要计算调用函数所需时间,你需要多次进行测试而不是仅测试一次。如果你只调用一次函数并且它运行的时间非常短,你仍然需要调用计时器函数的开销,而你不知道这需要多长时间。
例如,如果你估计你的函数可能需要运行800纳秒,那么可以将其在一个循环中调用1000万次(这将需要大约8秒钟)。将总时间除以1000万即可得到每次调用的时间。

实际上,我正在尝试获取特定调用的API性能。对于每次运行,它可能会给出不同的时间,这可能会影响我为性能改进制作的图表...因此需要以纳秒为单位计时。但是,这是一个很好的想法,我会考虑的。 - gagneet

5

您可以在运行于x86处理器下的gcc中使用以下函数:

unsigned long long rdtsc()
{
  #define rdtsc(low, high) \
         __asm__ __volatile__("rdtsc" : "=a" (low), "=d" (high))

  unsigned int low, high;
  rdtsc(low, high);
  return ((ulonglong)high << 32) | low;
}

使用Digital Mars C++:

unsigned long long rdtsc()
{
   _asm
   {
        rdtsc
   }
}

这段代码读取芯片上的高性能计时器。在进行性能分析时,我会使用它。


2
这很有用,我会检查处理器是否是x86架构,因为我在使用苹果Mac进行实验...谢谢 :-) - gagneet
1
用户应该为高和低提供什么值?为什么要在函数体内定义宏?此外,ulonglong(可能是typedef为unsigned long long)不是标准类型。我想使用它,但不确定如何使用 ;) - Joseph Garvin
1
在Linux下,unsigned long不是正确的使用方式。您可能需要考虑改用int,因为long和long long在64位Linux上都是64位。 - Marius
3
现今TSC计时器常常不可靠:当频率改变时,它在许多处理器上会改变速度,并且在不同内核之间不一致,因此TSC并不总是增长。 - Blaisorblade
1
@Marius:我已经实现了你的评论,使用“unsigned int”作为内部类型。 - Blaisorblade

3

我在使用Borland代码,这里是代码:ti_hund有时会给我一个负数,但时间相当准确。

#include <dos.h>

void main() 
{
struct  time t;
int Hour,Min,Sec,Hun;
gettime(&t);
Hour=t.ti_hour;
Min=t.ti_min;
Sec=t.ti_sec;
Hun=t.ti_hund;
printf("Start time is: %2d:%02d:%02d.%02d\n",
   t.ti_hour, t.ti_min, t.ti_sec, t.ti_hund);
....
your code to time
...

// read the time here remove Hours and min if the time is in sec

gettime(&t);
printf("\nTid Hour:%d Min:%d Sec:%d  Hundreds:%d\n",t.ti_hour-Hour,
                             t.ti_min-Min,t.ti_sec-Sec,t.ti_hund-Hun);
printf("\n\nAlt Ferdig Press a Key\n\n");
getch();
} // end main

3
您可以使用嵌入式分析工具(适用于Windows和Linux,免费),它具有多平台计时器接口(在处理器周期计数中),并可提供每秒钟的周期数:
EProfilerTimer timer;
timer.Start();

... // Your code here

const uint64_t number_of_elapsed_cycles = timer.Stop();
const uint64_t nano_seconds_elapsed =
    mumber_of_elapsed_cycles / (double) timer.GetCyclesPerSecond() * 1000000000;

重新计算周期计数到时间可能是一项危险的操作,现代处理器可以动态改变CPU频率。因此,在进行分析之前,需要确定处理器频率以确保转换后的时间正确。

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