在Linux 中测量延迟

原文地址:http://btorpey.github.io/blog/2014/02/18/clock-sources-in-linux/

他山之石,可以攻玉,该文章将详细地介绍了在Linux 系统中使用TSC 时钟精确地计算一段代码过程(中断和函数等)执行时间需要注意的内容,可以配置Intel 官方文档《How to Benchmark Code Execution Times on Intel® IA-32 and IA-64 Instruction Set Architectures》一起阅读。下面是译文。

为了在现代(操作)系统中的测量(一段过程的)延迟,我们需要(测量时间间隔的时钟)至少能够以微秒为单位,最好以纳秒为单位。好消息是,使用相对现代的硬件和软件,可以准确地测量小到几个纳秒的时间间隔。

但是,为确保您的(测量)结果是准确的,更重要的是要了解您要测量什么,以及不同情况下测量过程的边界是什么。

概要

为了(在Linux 中测量延迟)获得最佳效果,您应该使用:

  • (使用)Linux 内核 2.6.18 或更高版本 —— 这是第一个包含 hrtimers (高精度时钟)包的版本。最好的是 2.6.32 或更高版本,包括了对大多数不同时钟源的支持。
  • 具有恒定不变的 TSC(constant, invariant TSC (time-stamp counter))的 CPU。这意味着 TSC 在所有插槽和CPU核心上都以恒定速率运行,而不管电源管理(代码)对 CPU 的频率进行如何更改。如果 CPU 支持 RDTSCP 指令那就更好了(RDTSCP 会使得读取的时间更准确和稳定)。
  • TSC 应配置为Linux 内核的时钟源。
  • 您应该测量发生在同一台机器上的两个事件之间的间隔(机器内计时,即intra-machine timing)。
  • 对于机器内计时,您最好的选择使用汇编语言直接读取 TSC。在我的测试机器上,软件读取 TSC 大约需要 100ns,这是该方法准确性的(边界)限制(要测量100ns 以内的时间间隔是不实际的)。但不同机器读取TSC 开销不尽相同,这就是为什么我提供了源代码,您可以用来进行测量自己的机器。
    • 请注意,上面提到的100ns 主要是因为我的Linux 机器不支持RDTSCP 指令。所以为了获取合理准确的计时,在RDTSC 之前还执行了CPUID 指令以序列化指令执行过程。而在另一台支持RDTSCP 指令的机器(新 MacBook Air)上,开销下降了大约 14ns。

下面将讨论时钟在Linux 上的工作原理,如何从软件(角度)访问各种时钟,以及如何测量访问它们的开销。

机器内与机器间计时

在深入讨论上面(概要)建议的细节之前,首先谈谈机器内(Intra-machine)与机器间(Inter-machine)时间测量的不同。机内计时是最简单的情况,因为保证使用相同的时钟源计时一般来说非常容易。

机器间计时的问题在于,您面临着(至少)两个不同系统的时钟源。(当然,除非您是在一个机器上测量一个Ping-Pong 来回过程的间隔)。有个俗语有趣地描述了拥有两个时钟源的问题:

一个戴表的人知道现在几点了。一个有两块手表的人永远不会确定。

——西格尔定律

对于机器间计时,CLOCK_REALTIME 时钟源(gettimeofday() 的来源)几乎毫无用处,而您真正需要的是一个在两台(或更多台)机器之间同步的时钟。这样,计时准确性显然取决于您的同步时钟,并在一般情况下,(同步时钟)很难获取比微秒更精确的测量。

我们不会在本文中更多地讨论机器间计时。

Linux 如何计时

抛开(机器间计时),让我们来看看 Linux 是如何维持时间的。系统启动后, Linux 从 RTC(Real Time Clock)获取当前时间时。RTC是一个由主板纽扣电池供电的硬件时钟,因此即使机器断电,它也能继续运行。在大多数情况下,RTC 不是特别准确,因为它是由廉价的晶体振荡器驱动的,其频率会根据温度和其他环境因素变化。 Linux 系统从RTC 查询得到的启动时间,并存储在内核内存中,稍后将启动时间用作偏移量,合并TSC 保留的滴答计数推导出(当前的)挂钟时间(Wall-clock time)。

系统启动的同时, TSC(Time-Stamp Counter 时间戳计数器)开始运行。TSC 是一个寄存器计数器,它由生成CPU 的时钟脉冲的晶体振荡器驱动,所以它和 CPU 的频率相同,例如 2GHz (CPU的 TSC)时钟每纳秒会滴答两次。

(系统也)还有许多其他时钟源,我们将在后面讨论。但在大多数情况下,TSC 是系统首选时钟源,原因有两个:它非常准确,且查询其值的开销非常小(因为对于代码来说TSC 只是一个寄存器)。但是使用TSC 作为时序源时,需要牢记几点:

  • 在较老旧的CPU 中,每个CPU 核心都有自己(独立运行)的 TSC,因此为了确保两次测量都相对彼此准确,有必要将测量代码固定到某一个CPU 核心(防止代码所在的任务被调度至其他CPU 核心)。
  • 同样在较老旧的CPU 中,TSC 将以CPU 本身的频率运行,如果该频率发生变化(例如,如果频率动态降低,或者 CPU 完全停止以进行电源管理),则该CPU 上的TSC 也会变慢下降或停止。(有时可以通过在BIOS 中禁用电源管理来解决此问题,那么所有CPU 始终以100% 频率运行,不升高也不降低)。

这两个问题在最近的(大约2010 年后) CPU 中都得到了解决:不管 CPU 频率如何变化,恒定的TSC (constant TSC)使系统中所有CPU 核心上TSC 保持同步,而不变(或不间断)的TSC (nonstop_tsc)使TSC 保持在固定速率下运行。想要看看您的CPU 是否支持以上一种或两种(特性),请执行以下操作并检查标志中输出的值:

$ cat /proc/cpuinfo | grep -i tsc
flags:... tsc rdtscp constant_tsc nonstop_tsc ...

这些(关于TSC 时钟的)标志具有以下含义:

tsc系统具有TSC 时钟。
rdtscpRDTSCP 指令可用。
constant_tscTSC 在所有CPU槽和核心之间同步。
nonstop_tscTSC 不受电源管理的影响。

其他时钟源

TSC 通常是首选时钟源,其精度高,(读取)开销相对较低。(但Linux 系统也)还可以使用其他时钟源:

  • HPET(High Precision Event Timer,高精度事件时钟)是微软和英特尔在2005年左右推出的,其精度约为100 ns,不如TSC(提供亚纳秒级精度)。查询 HPET 的成本比TSC 也高得多。
  • 由于acpi_pm 运行(频率)在 3.58MHz(每 279 ns 一个滴答),它优点是频率不会根据系统电源管理改变,acpi_pm 也没有前面的定时器那么准确。
  • jiffies 与用于调度(操作系统上进程)定时器采用相同的时钟源,因此其分辨率通常很差(大多数 Linux 发行版中的默认调度间隔是 1 ms 或 10 ms)。

查看系统上可用的时钟源:

$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
tsc hpet acpi_pm

并查看正在使用哪个:

$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
tsc

通常,内核在引导时自动设置时钟源,但您可以通过在Linux 内核启动命令行上添加参数(例如,在 /boot/grub/grub.conf 或 /etc/default/grub 中)来强制使用特定时钟源:

ro root=/dev/... clocksource=tsc 

您还可以在系统运行时更改时钟源,例如强制使用 HPET:

$ echo hpet > /sys/devices/system/clocksource/clocksource0/current_clocksource

上面所述时钟虽然都被我们平时称为硬件时钟,但严格来说它们是一种硬件和软件的混合体。在底层有某种硬件设备可以生成周期性的定时脉冲,然后对其进行计数以创建时钟。有一些(例如TSC)计数是在硬件中完成的,也有一些(例如jiffies)计数是在软件中完成的。

壁钟时间

刚刚讨论的硬件(或硬件/软件混合)时钟都有一个共同点:它们只是计数器,与我们大多数人认为的时间(通常称为壁钟时间,Wall-clock time)没有直接关联。

如果壁钟时间要相当准确,那么需要对这些计数器进行相当复杂的软件计算。当然,采用何种合理且准确的方法取决于所需壁钟时间的精准程度。

同步多个分布式时钟的过程极其复杂,这里不做赘述。有许多不同的机制来同步分布式时钟,从相对简单的(例如网络时间协议,Network Time Protocal,NTP) 到不太简单的(例如精准时间协议,Precise Time Protocal,PTP),直至专门的专有解决方案。

将一个系统的(壁钟)时间与其他系统的时间保持同步的关键是,需用一种手段来调整当前时钟以使其与其对等系统保持同步。有两种方法:

  • 对系统时间进行(一次或多次)不连续修改(该过程也称作Stepping)。这可能会导致壁钟时间出现大幅跳跃,包括向后跳跃,尽管时间调整软件通常可以限制单次(时间)跳跃的大小。一个常见的例子就是系统在启动时从 NTP 服务器初始化其本身的系统时钟。
  • 修改驱动硬件计数器(如 TSC)的振荡器的频率(或倍频器)(该过程也称作Slewing)。这会导致时钟运行得相对更快或更慢,但(时间)不会跳跃,也不能倒退。

(Linux 中)可用时钟源

在Linux 中获取时间的最常见方法是调用gettimeofday() 系统调用,它以微秒精度(尽管不一定是微秒的准度)返回当前壁钟时间。由于gettimeofday() 调用 clock_gettime(CLOCK_REALTIME, **),因此以下讨论也适用于它。

Linux 还实现了POSIX clock_gettime() 系列函数,可让您查询不同的时钟源,包括:

CLOCK_REALTIME壁钟时间。该时间可能被时间同步协议(例如NTP 和PTP)而发生跳跃。
CLOCK_REALTIME_COARSECLOCK_REALTIME 的低分辨率版本。
CLOCK_REALTIME_HRCLOCK_REALTIME 的高分辨率版本,仅存在于实时内核。
CLOCK_MONOTONIC单调时钟。代表时间的计数器频率可能发生变化,但时间不会跳跃,即只会前移,不能后退。
CLOCK_MONOTONIC_COARSECLOCK_MONOTONIC 的低分辨率版本。
CLOCK_MONOTONIC_RAWCLOCK_MONOTONIC 的一个版本,时间的计数器频率不会变化,时间也不会跳跃 。
CLOCK_BOOTTIMECLOCK_MONOTONIC 的一个版本,它反应了自机器启动后至当前的时间。仅在较新的内核(2.6.39+)中可用。

以上可用时钟的分辨率和准确性取决于硬件以及特定的 Linux 实现。(我们实现了)一个小测试程序 (clocks.c),作为本文随附源代码的一部分,将打印有关系统上时钟的相关信息。在我的测试机上的运行结果如下:

clocks.c
                    clock	       res (ns)	           secs	          nsecs
             gettimeofday	          1,000	  1,391,886,268	    904,379,000
           CLOCK_REALTIME	              1	  1,391,886,268	    904,393,224
    CLOCK_REALTIME_COARSE	        999,848	  1,391,886,268	    903,142,905
          CLOCK_MONOTONIC	              1	        136,612	    254,536,227
      CLOCK_MONOTONIC_RAW	    870,001,632	        136,612	    381,306,122
   CLOCK_MONOTONIC_COARSE	        999,848	        136,612	    253,271,977

请注意函数clock_getres() 的返回结果,一个特定的时钟源可能返回比其clock_getres() 给出精度更高的结果(比如COARSE 时钟精度只有毫秒,但其结果可能包含微秒和微秒的信息),但超过该精度的数值可能都是没用的(唯一的例外是函数gettimeofday(),因为它返回一个以微秒为单位的时间值,低位数字全为零 )。

此外,从clock_getres() 为CLOCK_MONOTONIC_RAW 返回的值显然是不准确的,我在好几台机器上都看到过类似的结果。

最后请注意,为CLOCK_REALTIME 列出的分辨率接近但不完全是百万分之一秒——这是晶体振荡器无法产生恰好1000 Hz 频率的这一事实的产物——实际上是 1000.15 Hz。

从软件中获取时钟值

接下来简要讨论如何从软件(和程序)的角度,读取这些不同的时钟值。

汇编器

在汇编语言中,执行RDTSC 指令后,TSC 的值作为结果保存在在edx 和eax 寄存器中。但是由于现代CPU 支持乱序执行,一般要在 RDTSC 指令之前插入序列化指令(例如CPUID),以确保 RDTSC 指令的执行不会被处理器重新排序。

较新的CPU 包含了RDTSCP 指令,它将进行任何必要的序列化,从而避免了CPUID 指令的开销(CPUID 的开销可能相当大,且可变)。如果您的CPU 支持RDTSCP,请使用它而不是CPUID 和RDTSC 组合。

C/C++

显然,RDTSC 指令也可以直接从C 或C++ 调用,可以使用编译器支持的汇编语言方法来执行RDTSC,也可以调用C/C++封装后的接口(可以在Agner Fog 的优秀网站上找到一个示例)。

调用 gettimeofday() 或 clock_gettime() 方法也非常简单直接——请参阅随附的clocks.c 源文件以获取示例。

Java

Java 只有两种相关获取时间的方法:

  • System.currentTimeMillis() 返回自公元纪元以来至当前挂钟时间的毫秒数。它调用 gettimeofday(),后者又调用 clock_gettime(CLOCK_REALTIME, …)。
  • System.nanoTime 返回自某个未指定起点至今以来的纳秒数。它根据系统能力,要么调用 gettimeofday(),要么调用 clock_gettime(CLOCK_MONOTONIC, )。

但是如果您在Java 中需要上述时钟以外的时钟,则需要你自己动手实现,比如通过JNI 调用C。但好消息是,这样做并不比访问nanoTime 开销大多少(至少在我的测试中是这样)。

时钟查询的开销

海森堡测不准原理,简而言之就是,观察现象的行为会改变现象本身。为测量延迟而获取系统时间戳也存在类似问题,因为读取任何时钟源都需要有限的(有时是可变的)时间。换句话说,2GHz 机器上的TSC 每纳秒滴答两次,并不意味着我们可以测量出一纳秒的间隔——因为我们还需要考虑软件读取TSC 所需的时间。

那么,执行这些不同的时钟查询的成本有多大?您可以使用C/C++ 和Java(使用 JNI 调用 C 代码)测试查询各种时钟源所需的时间,或者参考一些示例代码,。

C++ 和Java 版本都采用相同的方法:在连续的循环中调用特定的(获取)时钟函数,并存储结果。循环的次数应该足够多,确保代码指令和数据在处理器中的都能够缓存命中,以保证结果的稳定和准确。

在我的机器上运行测试的结果如下(所有结果都以纳秒为单位):

ClockBench.cpp
                   Method       samples     min     max     avg  median   stdev
           CLOCK_REALTIME       255       54.00   58.00   55.65   56.00    1.55
    CLOCK_REALTIME_COARSE       255        0.00    0.00    0.00    0.00    0.00
          CLOCK_MONOTONIC       255       54.00   58.00   56.20   56.00    1.46
      CLOCK_MONOTONIC_RAW       255      650.00 1029.00  690.35  839.50   47.34
   CLOCK_MONOTONIC_COARSE       255        0.00    0.00    0.00    0.00    0.00
              cpuid+rdtsc       255       93.00   94.00   93.23   93.50    0.42
                    rdtsc       255       24.00   28.00   25.19   26.00    1.50
Using CPU frequency = 2.660000

ClockBench.java
                   Method       samples     min     max     avg  median   stdev
          System.nanoTime       255       54.00   60.00   55.31   57.00    1.55
           CLOCK_REALTIME       255       60.00   84.00   62.50   72.00    1.92
              cpuid+rdtsc       255      108.00  112.00  109.03  110.00    1.39
                    rdtsc       255       39.00   43.00   39.85   41.00    1.37
Using CPU frequency = 2.660000

关于这些结果有几点需要注意:

  • 获取两个COARSE 时钟的延迟都显示为零,这告诉我们获取该类时钟值所需的时间小于时钟的分辨率。(我们之前的测试显示 COARSE 时钟的分辨率为 1ms)。
  • 出于某种原因,CLOCK_MONOTONIC_RAW 时钟的查询成本非常高。我也无法解释这一点——你可能会认为它不会往前或往后调整时间的特性会使其获取时间更快,而不是更慢。要不是它查询开销太大,它(优良的特性)将(使得它)成为机器内计时的最佳选择。
  • CPUID 和RDTSC 的组合比RDTSCP 慢,后者比单独的RDTSC 慢。这说明一般情况下RDTSCP 只要是可用,就应该是首选,如果没有(RDTSCP),则回退到 CPUID+RDTSC。(虽然RDTSC 本身是最快的,但由于乱序执行它可能不准确,也就意味着它只对操作时间相对较长的场景合适)。
  • Java 版本比C++ 版本稍慢,大概率是JNI 的开销。

结论

我原以为这是一个简短且琐碎的研究,而结果表明, 它比我预期的要复杂得多(相关文档记录也不多)。

总之,我希望以上总结是有帮助的。

(如有问题)请随时直接与我联系,提出意见、建议、更正等。

其他资源

以下是我在研究本文时参考的内容。

http://elinux.org/Kernel_Timer_Systems

http://elinux.org/High_Resolution_Timers

http://juliusdavies.ca/posix_clocks/clock_realtime_linux_faq.html

http://en.wikipedia.org/wiki/Time_Stamp_Counter

http://stackoverflow.com/questions/10921210/cpu-tsc-fetch-operation-special-in-multicore-multi-processor-environment

http://www.citihub.com/requesting-timestamp-in-applications/

http://www.intel.com/content/www/us/en/intelligent-systems/embedded-systems-training/ia-32-ia-64-benchmark-code-execution-paper.html

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据