如何在Linux下用C++测量时间的最快方法(比std::chrono更快)?包含基准测试。

4
#include <iostream>
#include <chrono>
using namespace std;

class MyTimer {
 private:
  std::chrono::time_point<std::chrono::steady_clock> starter;
  std::chrono::time_point<std::chrono::steady_clock> ender;

 public:
  void startCounter() {
    starter = std::chrono::steady_clock::now();
  }

  double getCounter() {
    ender = std::chrono::steady_clock::now();
    return double(std::chrono::duration_cast<std::chrono::nanoseconds>(ender - starter).count()) /
           1000000;  // millisecond output
  }
  
  // timer need to have nanosecond precision
  int64_t getCounterNs() {
    return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - starter).count();
  }
};

MyTimer timer1, timer2, timerMain;
volatile int64_t dummy = 0, res1 = 0, res2 = 0;

// time run without any time measure
void func0() {
    dummy++;
}

// we're trying to measure the cost of startCounter() and getCounterNs(), not "dummy++"
void func1() {
    timer1.startCounter();  
    dummy++;
    res1 += timer1.getCounterNs();
}

void func2() {
    // start your counter here
    dummy++;
    // res2 += end your counter here
}

int main()
{
    int i, ntest = 1000 * 1000 * 100;
    int64_t runtime0, runtime1, runtime2;

    timerMain.startCounter();
    for (i=1; i<=ntest; i++) func0();
    runtime0 = timerMain.getCounter();
    cout << "Time0 = " << runtime0 << "ms\n";

    timerMain.startCounter();
    for (i=1; i<=ntest; i++) func1();
    runtime1 = timerMain.getCounter();
    cout << "Time1 = " << runtime1 << "ms\n";

    timerMain.startCounter();
    for (i=1; i<=ntest; i++) func2();
    runtime2 = timerMain.getCounter();
    cout << "Time2 = " << runtime2 << "ms\n";

    return 0;
}

我试图对一个程序进行分析,其中某些关键部分的执行时间在小于50纳秒左右。我发现使用std::chrono的计时器类太耗费资源了(带有计时的代码比无计时的代码多花费40%的时间)。如何制作一个更快的计时器类?

我认为一些特定于操作系统的系统调用是最快的解决方案。平台是Linux Ubuntu。

编辑: 所有代码都是使用 -O3 编译的。确保每个计时器只被初始化一次,因此测量成本仅由 startMeasure/stopMeasure 函数引起。我没有进行任何文本打印。

编辑2: 接受的答案并未包括实际将循环次数转换为纳秒的方法。如果有人能够这样做,那将非常有帮助。


你如何衡量“非定时”代码?你如何测量40%的差异?这40%是否包括计时器本身的设置和拆卸?还是输出? - Some programmer dude
2
为什么需要测量?您是否考虑过使用分析器?它们存在的目的是让您不必自己添加测量代码。它们构建调用图,以便您可以确定瓶颈在哪里。考虑询问 CPU 运行了多少个周期(尽管您仍然会遇到多线程和其他应用程序给您的测量结果带来噪音的情况)。 - Pepijn Kramer
1
进行测量并非免费。您在测量过程中是否打印结果?如果是,请将其删除。 - Pepijn Kramer
你在调用构造函数、析构函数和函数调用时浪费了时间。如果你想要更快的计时器,考虑使用一些内联函数调用。 - Pepijn Kramer
@PepijnKramer 所有的 MyTimer 定时器对象都在全局空间中声明。构造函数/析构函数永远不会在 func0()、func1()、func2() 中被调用。 - Huy Le
显示剩余12条评论
1个回答

9
你需要做的是所谓的“微基准测试”。它可能会变得非常复杂。我假设你正在使用 x86_64 上的 Ubuntu Linux。这不适用于 ARM、ARM64 或任何其他平台。
在 Linux 上,std::chrono 实现在 libstdc++(gcc)和 libc++(clang)中只是一个薄包装器,包装了 GLIBC,即 C 库,GLIBC 完成了所有繁重的工作。如果你查看 std::chrono::steady_clock::now(),你会看到调用 clock_gettime()。
clock_gettime() 是一个 VDSO,也就是内核代码在用户空间运行的方式。它应该非常快,但是可能需要进行一些管理工作,并且每 n 次调用可能需要花费很长时间。因此,我不建议用于微基准测试。
几乎每个平台都有一个周期计数器,x86 有汇编指令 rdtsc。可以通过制作 asm 调用或使用特定于编译器的内置函数 __builtin_ia32_rdtsc() 或 __rdtsc() 将此指令插入到代码中。
这些调用将返回表示自机器上电以来时钟数的 64 位整数。rdtsc 不是立即执行的,但速度很快,大约需要 15-40 个时钟周期才能完成。
不能保证此计数器在所有核心上都相同,因此当进程从一个核心移动到另一个核心时请注意。但在现代系统中,这通常不会成为问题。
rdtsc 的另一个问题是编译器通常会重新排序指令,如果发现它们没有副作用,而不幸的是 rdtsc 就是其中之一。因此,如果您看到编译器对您进行了诡计,请在这些计数器读取周围使用虚假屏障。
还有一个大问题是 CPU 的乱序执行本身。不仅编译器可以更改执行顺序,CPU 也可以。自 x86 486 以来,英特尔 CPU 就开始流水线处理,因此多个指令可以同时执行。所以你可能会测量到错误的执行时间。
我建议你熟悉微基准测试的类量子问题。这并不直接。
请注意,rdtsc() 将返回时钟周期数。你需要使用时间戳计数器频率将其转换为纳秒。
以下是一个示例:
#include <iostream>
#include <cstdio>

void dosomething() {
    // yada yada
}

int main() {
    double sum = 0;
    const uint32_t numloops = 100000000;
    for ( uint32_t j=0; j<numloops; ++j ) {
        uint64_t t0 = __builtin_ia32_rdtsc();
        dosomething();
        uint64_t t1 = __builtin_ia32_rdtsc();
        uint64_t elapsed = t1-t0;
        sum += elapsed;
    }
    std::cout << "Average:" << sum/numloops << std::endl;
}

这篇文章有点过时(2010年),但足够更新,可以为您介绍微基准测试:

如何在Intel® IA-32和IA-64指令集架构上进行基准代码执行时间测试


1
那些信息至少在我所知道的内核中是不公开的。正确的做法是在循环之前和之后进行tsc读取,以及相应的clock_gettime/chrono调用,并计算每个周期的平均时间。或者您可以使用像这样的模块:https://github.com/trailofbits/tsc_freq_khz - user8143588
1
谢谢。使用它比使用std::chrono快2.5倍,所以我想这就是答案。 - Huy Le
1
如果您的TSC计数器频率为3.2GHz,这通常是您的CPU的最大频率,则每纳秒平均会有3.2个周期。请记住这一点。 - user8143588
1
我偶尔会在我知道我正在以最大CPU速度运行时使用它进行微基准测试。它方便报告皮秒时间,而频率的了解使其成为可能。尽管正如你在答案中指出的那样,在微基准测试中有很多要注意的陷阱。 - Howard Hinnant
1
@HowardHinnant,经过多年使用这些东西的经验,我得出结论,最好将其保留在周期中。因为有时你在开发框中运行时最大只有2.5GHz,但是该程序将在超频的5 GHz服务器上运行。在周期中,数字通常甚至与我2011年的1.5 GHz笔记本电脑相匹配。而且所有的Intel / AMD / Agner报告也都是以周期为单位的,因此更容易进行相关性分析。随着时间的推移,我只是学会了记忆周期。 - user8143588
显示剩余5条评论

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