【问题标题】:C++ Linux fastest way to measure time (faster than std::chrono) ? Benchmark includedC++ Linux 最快的时间测量方法(比 std::chrono 快)?包括基准
【发布时间】:2022-01-23 10:10:02
【问题描述】:
#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: 接受的答案不包括将周期数实际转换为纳秒的方法。如果有人能做到这一点,那将非常有帮助。

【问题讨论】:

  • clock_gettime 的 librt 可能很有用。
  • 如果您的编译器支持内部函数,请尝试__rdtsc
  • 你如何测量untimed代码?您如何衡量 40% 的差异? 40% 是否还包括计时器本身的设置和拆卸?还是输出?
  • 为什么需要测量?您是否考虑过使用分析器?它们在那里,因此您不必自己将测量代码添加到您的代码中。他们构建调用图,以便您可以准确找出瓶颈所在。考虑询问 CPU 它运行了多少个周期(尽管您仍然会遇到多线程和其他应用程序,这会给您的测量增加噪音)
  • 测量不是免费的。您是否在测量过程中打印出结果?如果是这样,请删除这些。

标签: c++ linux performance optimization time


【解决方案1】:

您想要的称为“微基准测试”。它会变得非常复杂。我假设您在 x86_64 上使用 Ubuntu Linux。这不适用于 ARM、ARM64 或任何其他平台。

std::chrono 在 Linux 上的 libstdc++ (gcc) 和 libc++ (clang) 中实现,作为对 GLIBC(C 库)的简单封装,它完成了所有繁重的工作。如果您查看 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 年),但它足够最新,可以很好地介绍微基准测试:

How to Benchmark Code Execution Times on Intel® IA-32 and IA-64 Instruction Set Architectures

【讨论】:

  • 内核不会暴露这些信息,至少我知道。正确的做法是在循环之前和之后读取 tsc 以及相应的 clock_gettime/chrono 调用并计算每个周期的平均时间。或者你可以使用这样的模块:github.com/trailofbits/tsc_freq_khz
  • 谢谢。它比使用 std::chrono 快 2.5 倍,所以我想这就是答案
  • 如果您的 TSC 计数器的频率为 3.2GHz,这通常是您 CPU 的最大频率,那么平均每纳秒会有 3.2 个周期。请记住这一点。
  • 当我知道我正在以最大 cpu 速度运行时,我偶尔会将它用于微基准测试。以皮秒为单位报告频率知识启用的时间很方便。尽管正如您在回答中正确指出的那样,在微基准测试中需要注意很多陷阱。
  • @HowardHinnant 所以在使用这些东西多年后,我得出的结论是最好将它留在循环中。因为有时您在最高 2.5GHz 的开发盒中运行,但它会在超频的 5GHz 服务器上运行。在周期中,这些数字通常甚至与我 2011 年运行在 1.5 GHz 的笔记本电脑相匹配。并且所有 Intel/AMD/Agner 报告也都处于周期中,因此更容易关联。随着时间的推移,我刚刚学会了记住周期。
猜你喜欢
  • 2020-01-05
  • 2017-08-31
  • 2014-07-16
  • 1970-01-01
  • 2015-02-07
  • 2013-11-12
  • 2022-07-06
  • 2012-07-09
  • 2017-03-08
相关资源
最近更新 更多