【问题标题】:Is clock_gettime() adequate for submicrosecond timing?clock_gettime() 是否足以满足亚微秒计时?
【发布时间】:2011-12-17 15:25:50
【问题描述】:

我需要一个高分辨率计时器,用于我们应用程序的 Linux 构建中的嵌入式分析器。我们的分析器测量的范围小到单个函数,因此它需要优于 25 纳秒的计时器精度。

以前我们的实现使用内联汇编和rdtsc 操作直接从CPU 查询高频定时器,但是this is problematic 并且需要经常重新校准。

所以我尝试使用clock_gettime 函数来查询CLOCK_PROCESS_CPUTIME_ID。文档声称这给了我纳秒级的时间,但我发现单次调用 clock_gettime() 的开销超过 250ns。这使得对事件进行 100ns 长的计时是不可能的,并且在计时器功能上具有如此高的开销会严重拖累应用程序的性能,使配置文件失真超出价值。 (我们每秒有数十万个分析节点。)

有没有一种方法可以调用 clock_gettime() 的开销小于 ¼μs? 或者有没有其他方法可以可靠地获得具有 rdtsc?

下面是我用来计时clock_gettime()的代码。

// calls gettimeofday() to return wall-clock time in seconds:
extern double Get_FloatTime();
enum { TESTRUNS = 1024*1024*4 };

// time the high-frequency timer against the wall clock
{
    double fa = Get_FloatTime();
    timespec spec; 
    clock_getres( CLOCK_PROCESS_CPUTIME_ID, &spec );
    printf("CLOCK_PROCESS_CPUTIME_ID resolution: %ld sec %ld nano\n", 
            spec.tv_sec, spec.tv_nsec );
    for ( int i = 0 ; i < TESTRUNS ; ++ i )
    {
        clock_gettime( CLOCK_PROCESS_CPUTIME_ID, &spec );
    }
    double fb = Get_FloatTime();
    printf( "clock_gettime %d iterations : %.6f msec %.3f microsec / call\n",
        TESTRUNS, ( fb - fa ) * 1000.0, (( fb - fa ) * 1000000.0) / TESTRUNS );
}
// and so on for CLOCK_MONOTONIC, CLOCK_REALTIME, CLOCK_THREAD_CPUTIME_ID.

结果:

CLOCK_PROCESS_CPUTIME_ID resolution: 0 sec 1 nano
clock_gettime 8388608 iterations : 3115.784947 msec 0.371 microsec / call
CLOCK_MONOTONIC resolution: 0 sec 1 nano
clock_gettime 8388608 iterations : 2505.122119 msec 0.299 microsec / call
CLOCK_REALTIME resolution: 0 sec 1 nano
clock_gettime 8388608 iterations : 2456.186031 msec 0.293 microsec / call
CLOCK_THREAD_CPUTIME_ID resolution: 0 sec 1 nano
clock_gettime 8388608 iterations : 2956.633930 msec 0.352 microsec / call

这是在标准的 Ubuntu 内核上。该应用程序是 Windows 应用程序的一个端口(我们的 rdtsc 内联汇编工作得很好)。

附录:

x86-64 GCC 是否有一些与__rdtsc() 等效的内在属性,所以我至少可以避免内联汇编?

【问题讨论】:

  • 这个问题的答案可能对你有帮助:stackoverflow.com/questions/638269/…
  • @Crash: My sympathies :) 想吃点儿烤肉吗?谁最能加速一些代码?
  • @Mike 我希望!现在,我更像是“我们需要将这段代码加速 20%,否则我们就完蛋了”。查看采样分析器中的函数列表,主循环的比例不超过 2%。 (我尝试了你的秒表和调试器中断技巧,并从 20 个不同的暂停中获得了 20 个不同的调用堆栈。)
  • @Crash:我相信你做到了。我所做的是查看每个示例,然后向自己解释(在纸上或在我的脑海中进行描述)该程序当时在做什么以及为什么这样做。这意味着要注意堆栈每一层的源代码。 (这也可能意味着查看其他状态信息,例如相关变量。)如果有些事情不是严格必须完成的,并且如果您在 >1 个样本上看到类似的事情,请修复它并获得您的加速。您的代码可能非常紧凑,但如果有什么要挤出来的,应该可以找到它。
  • @Crash:例如,请耐心等待。我经常在数据结构代码中找到示例,例如索引、递增迭代器或测试结束条件。我可以在不同例程的不同代码行上看到这一点,因此没有任何代码行或例程上升到显着百分比。即使只是其中的一项,例如索引或递增,也可能不会上升到显着的百分比。但是综合起来,他们可以。通常普通的旧数组虽然可能不那么正统,但可以节省所有时间。

标签: linux performance ubuntu profiling


【解决方案1】:

没有。您必须使用特定于平台的代码来执行此操作。在 x86 和 x86-64 上,您可以使用 'rdtsc' 读取 Time Stamp Counter

只需移植您正在使用的 rdtsc 程序集。

__inline__ uint64_t rdtsc(void) {
  uint32_t lo, hi;
  __asm__ __volatile__ (      // serialize
  "xorl %%eax,%%eax \n        cpuid"
  ::: "%rax", "%rbx", "%rcx", "%rdx");
  /* We cannot use "=A", since this would use %rax on x86_64 and return only the lower 32bits of the TSC */
  __asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
  return (uint64_t)hi << 32 | lo;
}

【讨论】:

  • 他说他之前的实现使用了 rdtsc 但有 problems 并且不喜欢重新校准。
  • 这些问题已经过时了六年,现在已成为历史垃圾箱的一部分。您不太可能看到没有恒定 TSC 的现代服务器。
  • @David 你有资源支持吗?在过去几年中,与多 CPU 同步和时钟节流节能功能相关的时序问题有何变化?
  • 如果您检查 /proc/cpuinfo,您应该会在每个现代 CPU 上看到“constant_tsc”。如果 TSC 没有在内核之间同步,或者在它们应该保持不变的时候不是恒定的,那就是一个错误。 (你应该举报。)
  • 我同意@DavidSchwartz:例如对于我的沙桥箱:我看到flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl pni monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr dca sse4_1 sse4_2 x2apic popcnt lahf_lm ida constant_tsc 设置。您也可以参考download.intel.com/design/processor/manuals/253668.pdf 第 16.12.1 节“不变 TSC”
【解决方案2】:

您正在使用控制参数调用clock_getttime,这意味着api正在通过if-else树分支来查看您想要什么样的时间。我知道你不能通过这个调用来避免这种情况,但是看看你是否可以深入研究系统代码并调用内核最终直接调用的内容。另外,我注意到您包括循环时间(i++ 和条件分支)。

【讨论】:

    【解决方案3】:

    我需要一个高分辨率计时器,用于我们应用程序的 Linux 版本中的嵌入式分析器。我们的分析器测量的范围小到单个函数,因此它需要优于 25 纳秒的计时器精度。

    您考虑过oprofile 还是perf?您可以使用 CPU 上的性能计数器硬件来获取分析数据,而无需向代码本身添加检测。您可以查看每个函数甚至每行代码的数据。 “唯一”的缺点是它不会测量所消耗的挂钟时间,它会测量消耗的 CPU 时间,因此它并不适合所有调查。

    【讨论】:

    • 如果我只是在我的板凳上进行分析,那就没问题了。但我正在尝试修复我们已经使用了一段时间的嵌入式检测分析器,并且我们的许多其他工具都依赖于它。
    【解决方案4】:

    试试clockid_t CLOCK_MONOTONIC_RAW?

    CLOCK_MONOTONIC_RAW(从 Linux 2.6.28 开始;特定于 Linux) 类似于 CLOCK_MONOTONIC,但提供对 不受 NTP 约束的基于硬件的原始时间 调整或由执行的增量调整 调整时间(3)。

    来自Man7.org

    【讨论】:

    • 在 x86/x86_64 Linux(至少高达 4.15)上,读取时钟的调用实际上比 CLOCK_MONOTONIC_RAW 更慢(因为它不使用 vDSO 并进行真正的系统调用)相比之下到何时使用CLOCK_MONOTONIC。有关某些数据,请参阅 stackoverflow.com/a/13096917/9109338 中的结果。如果这很重要,请务必检查您平台的行为!
    【解决方案5】:

    我在我的系统上运行了一些基准测试,这是一个四核 E5645 Xeon,支持运行内核 3.2.54 的恒定 TSC,结果是:

    clock_gettime(CLOCK_MONOTONIC_RAW)       100ns/call
    clock_gettime(CLOCK_MONOTONIC)           25ns/call
    clock_gettime(CLOCK_REALTIME)            25ns/call
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID)  400ns/call
    rdtsc (implementation @DavidSchwarz)     600ns/call
    

    所以看起来在一个相当现代的系统上,(接受的答案)rdtsc 是最糟糕的路线。

    【讨论】:

    • 不确定您是如何准确测量的,但我看到完全不同的结果(调用每个选项 1e9 次)。 rdtsc 明显快于 [CLOCK_REALTIME] 选项
    • 在我的 2.6.32-431.el6.x86_64 和 3.10.0-693.21.1.el7.x86_64 机器上,rdtsc 比 clock_gettime() 快 80%。一个是 i7,另一个是 Xeon。
    • 这里的rdtsc 选项有问题:每次调用大约需要 20 ns。最快的clock_gettime 选项(25 ns 选项)基于 rdtsc,加上一些额外的工作将时间转换为挂钟时间,因此它们不能真正更快。跨度>
    【解决方案6】:

    很难给出一个全球适用的答案,因为硬件和软件的实现会有很大差异。

    但是,是的,大​​多数现代平台都会有一个合适的 clock_gettime 调用,该调用完全使用 VDSO 机制在用户空间中实现,根据我的经验,完成需要 20 到 30 纳秒(但请参阅 Wojciech's comment below 关于争用)。

    在内部,这是使用 rdtscrdtscp 进行计时的细粒度部分,加上调整以使其与挂钟时间同步(取决于您选择的时钟)和乘法将您平台上 rdtsc 的任何单位转换为纳秒。

    clock_gettime 提供的时钟并非所有都会实现这种快速方法,obvious 并不总是会实现。通常CLOCK_MONOTONIC 是一个不错的选择,但您应该在自己的系统上测试它

    【讨论】:

    • > 并且可靠地需要大约 20 到 30 纳秒才能完成 - 这不是真的。即使是由 vDSO 辅助的 clock_gettime() 对 CLOCK_MONOTONIC 和 CLOCK_REALTIME 的调用也已知在竞争场景(现实生活中的大多数生产环境)中需要几微秒才能完成。
    • @WojciechKudla - 很公平,我没见过,但我可以相信。是不是因为快速路径上有原子操作避免时间倒退之类的?如果不是,如何发生争用?我已将我使用的语言从“可靠”弱化为“以我的经验”,并添加了指向您评论的链接。
    • 在这种情况下满足意味着有许多来自多个核心的当前时间请求。是的,你是对的 - 在 clock_gettime 的 vdso 实现中存在一些不太可能的分支。
    • @WojciechKudla - 这不会导致问题:这些线路将在所有内核之间以 S 状态共享,并在任何地方以全 L1 速度访问。没有它,任何共享 r/o 或大部分是 r/o 内存都会消耗现代处理器!
    • 恐怕这又错了。我已经研究这个问题 15 年了,现在为超低延迟商店工作。高速缓存行在被内核无效之前保持在 S 状态的时间最短。它不仅仅是原始的计时器值,还有偏差、计时器域和一些标志。当您有很多 CPU 积极地读取缓存行而另一个 CPU 不断使其无效时,您将遇到性能瓶颈。这就是为什么存在诸如 MCS 锁定之类的东西的原因,仅仅是因为争用会破坏共享访问的性能。
    【解决方案7】:

    当您调用 clock_gettime() 函数时会发生这种情况。

    根据您选择的时钟,它将调用相应的函数。 (来自内核的 vclock_gettime.c 文件)

    int clock_gettime(clockid_t, struct __kernel_old_timespec *)
        __attribute__((weak, alias("__vdso_clock_gettime")));
    
    notrace int
    __vdso_clock_gettime_stick(clockid_t clock, struct __kernel_old_timespec *ts)
    {
        struct vvar_data *vvd = get_vvar_data();
    
    switch (clock) {
    case CLOCK_REALTIME:
        if (unlikely(vvd->vclock_mode == VCLOCK_NONE))
            break;
        return do_realtime_stick(vvd, ts);
    case CLOCK_MONOTONIC:
        if (unlikely(vvd->vclock_mode == VCLOCK_NONE))
            break;
        return do_monotonic_stick(vvd, ts);
    case CLOCK_REALTIME_COARSE:
        return do_realtime_coarse(vvd, ts);
    case CLOCK_MONOTONIC_COARSE:
        return do_monotonic_coarse(vvd, ts);
    }
    /*
     * Unknown clock ID ? Fall back to the syscall.
     */
        return vdso_fallback_gettime(clock, ts);
    }
    

    CLOCK_MONITONIC 更好(虽然我使用CLOCK_MONOTONIC_RAW),因为它不受 NTP 时间调整的影响。


    这就是do_monotonic_stick 在内核中的实现方式:

    notrace static __always_inline int do_monotonic_stick(struct vvar_data *vvar,
                                  struct __kernel_old_timespec *ts)
    {
        unsigned long seq;
        u64 ns;
    
        do {
            seq = vvar_read_begin(vvar);
            ts->tv_sec = vvar->monotonic_time_sec;
            ns = vvar->monotonic_time_snsec;
            ns += vgetsns_stick(vvar);
            ns >>= vvar->clock.shift;
        } while (unlikely(vvar_read_retry(vvar, seq)));
    
        ts->tv_sec += __iter_div_u64_rem(ns, NSEC_PER_SEC, &ns);
        ts->tv_nsec = ns;
    
        return 0;
    }
    

    而提供纳秒分辨率的vgetsns_stick()函数实现为:

    notrace static __always_inline u64 vgetsns(struct vvar_data *vvar)
    {
        u64 v;
        u64 cycles;
    
        cycles = vread_tick();
        v = (cycles - vvar->clock.cycle_last) & vvar->clock.mask;
        return v * vvar->clock.mult;
    }
    

    函数vread_tick()根据CPU从寄存器中读取周期:

    notrace static __always_inline u64 vread_tick(void)
    {
        register unsigned long long ret asm("o4");
    
        __asm__ __volatile__("rd %%tick, %L0\n\t"
                     "srlx %L0, 32, %H0"
                     : "=r" (ret));
        return ret;
    }
    

    clock_gettime() 的一次调用大约需要 20 到 100 纳秒。读取rdtsc 寄存器并将周期转换为时间总是更快。

    我在这里对CLOCK_MONOTONIC_RAW 做了一些实验:Unexpected periodic behaviour of an ultra low latency hard real time multi threaded x86 code

    【讨论】:

      猜你喜欢
      • 2021-06-19
      • 1970-01-01
      • 1970-01-01
      • 2014-04-24
      • 1970-01-01
      • 2021-12-24
      • 2018-04-11
      • 1970-01-01
      • 2012-05-22
      相关资源
      最近更新 更多