为什么System.nanoTime()和System.currentTimeMillis()相差如此之快?

2022-09-02 03:57:38

出于诊断目的,我希望能够在长时间运行的服务器应用程序中检测系统时间时钟的变化。由于 它 基于挂钟时间,并且基于独立于挂钟时间的系统计时器,因此我认为可以使用这些值之间的差异变化来检测系统时间变化。System.currentTimeMillis()System.nanoTime()

我编写了一个快速测试应用程序,以查看这些值之间的差异有多稳定,令我惊讶的是,这些值立即以每秒几毫秒的水平发散。有几次我看到了更快的背离。这是在带有Java 6的Win7 64位桌面上。我还没有在Linux(或Solaris或MacOS)下尝试下面的测试程序,看看它的性能如何。对于此应用的某些运行,散度为正,对于某些运行,背离为负。这似乎取决于桌面正在做什么,但很难说。

public class TimeTest {
  private static final int ONE_MILLION  = 1000000;
  private static final int HALF_MILLION =  499999;

  public static void main(String[] args) {
    long start = System.nanoTime();
    long base = System.currentTimeMillis() - (start / ONE_MILLION);

    while (true) {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        // Don't care if we're interrupted
      }
      long now = System.nanoTime();
      long drift = System.currentTimeMillis() - (now / ONE_MILLION) - base;
      long interval = (now - start + HALF_MILLION) / ONE_MILLION;
      System.out.println("Clock drift " + drift + " ms after " + interval
                         + " ms = " + (drift * 1000 / interval) + " ms/s");
    }
  }
}

时间的不准确以及中断应该与计时器漂移完全无关。Thread.sleep()

这两个Java“系统”调用都旨在用作测量 - 一个用于测量挂钟时间的差异,另一个用于测量绝对间隔,因此当实时时钟未更改时,这些值应以非常接近相同的速度变化,对吗?这是Java中的错误,弱点还是失败?操作系统或硬件中是否有某些东西阻止了Java更准确?

我完全预计这些独立测量之间会有一些漂移和抖动(**),但我预计每天的漂移时间不到一分钟。每秒1毫秒的漂移,如果单调,几乎是90秒!我观察到的最坏情况漂移可能是这个数字的十倍。每次运行此程序时,我都会在第一次测量时看到漂移。到目前为止,我还没有运行该程序超过30分钟。

由于抖动,我期望在打印的值中看到一些小的随机性,但是在几乎所有的程序运行中,我都看到了差异的稳步增加,通常高达每秒3毫秒的增加,并且比这多几倍。

是否有任何版本的Windows具有类似于Linux的机制来调整系统时钟速度,以缓慢地使时间时钟与外部时钟源同步?这样的事情会影响两个计时器,还是只影响挂钟计时器?

(*)我理解,在某些体系结构上,必然会使用与 相同的机制。我也认为,假设任何现代Windows服务器都不是这样的硬件架构是公平的。这是一个糟糕的假设吗?System.nanoTime()System.currentTimeMillis()

(**)当然,通常具有比大多数系统上的粒度大得多的抖动,因为它的粒度不是1毫秒。System.currentTimeMillis()System.nanoTime()


答案 1

您可能会发现这篇关于 JVM 计时器的 Sun/Oracle 博客文章很有趣。

以下是那篇关于Windows下JVM计时器的文章中的几个段落:

System.currentTimeMillis()是使用该方法实现的,该方法实质上只是读取 Windows 维护的低分辨率时间值。读取此全局变量自然非常快 - 根据报告的信息,大约6个周期。无论计时器中断是如何编程的,此时间值都会以恒定的速率更新 - 根据平台的不同,这将是10ms或15ms(此值似乎与默认中断周期相关联)。GetSystemTimeAsFileTime

System.nanoTime()是使用 / API 实现的(如果可用,否则它将返回 )。(QPC) 以不同的方式实现,具体取决于其运行的硬件。通常,它将使用可编程间隔计时器 (PIT)、ACPI 电源管理计时器 (PMT) 或 CPU 级时间戳计数器 (TSC)。访问 PIT/PMT 需要执行慢速 I/O 端口指令,因此 QPC 的执行时间约为微秒。相比之下,读取TSC大约为100个时钟周期(从芯片读取TSC并根据工作频率将其转换为时间值)。您可以通过检查 QueryPerformanceFrequency 是否返回签名值 3,579,545 (即 3.57MHz) 来判断系统是否使用了 ACPI PMT。如果您看到一个大约1.19Mhz的值,则您的系统正在使用旧的8245 PIT芯片。否则,您应该会看到一个大约与 CPU 频率大致相同的值(将可能生效的任何速度限制或电源管理取模)。QueryPerformanceCounterQueryPerformanceFrequencycurrentTimeMillis*10^6QueryPerformanceCounter


答案 2

我不确定这实际上会有多大帮助。但这是Windows/Intel/AMD/Java世界中一个积极变化的领域。对准确和精确的时间测量的需求已经持续了数年(至少10年)。英特尔和AMD都通过改变TSC的工作方式来做出回应。两家公司现在都有一种叫做不变TSC和/或Constant-TSC的东西

查看跨 CPU 内核的 rdtsc 准确性。引用来自 osgx(指的是英特尔手册)。

“16.11.1 不变的海训方案

较新的处理器中的时间戳计数器可能支持增强功能,称为不变 TSC。处理器对不变 TSC 的支持由 PUID.80000007H:EDX[8] 表示。

不变的 TSC 将在所有 ACPI P-、C- 中以恒定速率运行。和 T 态。这是向前推进的体系结构行为。在具有固定 TSC 支持的处理器上,操作系统可以将 TSC 用于挂钟计时器服务(而不是 ACPI 或 HPET 计时器)。TSC 读取的效率要高得多,并且不会产生与环形转换或访问平台资源相关的开销。

另请参见 http://www.citihub.com/requesting-timestamp-in-applications/。引用作者的话

对于 AMD:

如果 CPUID 8000_0007.edx[8] = 1,则确保 TSC 速率在所有 P 状态、C 状态和停止授权转换(如 STPCLK 限制)中保持不变;因此,TSC适合用作时间来源。

对于英特尔:

处理器对不变 TSC 的支持由 CPUID.80000007H:EDX[8] 表示。不变的 TSC 将在所有 ACPI P-、C- 中以恒定速率运行。和 T 态。这是向前推进的体系结构行为。在具有固定 TSC 支持的处理器上,操作系统可以将 TSC 用于挂钟计时器服务(而不是 ACPI 或 HPET 计时器)。TSC 读取的效率要高得多,并且不会产生与环形转换或访问平台资源相关的开销。

现在真正重要的一点是,最新的JVM似乎利用了新近可靠的TSC机制。网上没有太多的东西可以证明这一点。但是,请看一下 http://code.google.com/p/disruptor/wiki/PerformanceResults

“为了测量延迟,我们采用三阶段管道,并在低于饱和度的情况下生成事件。这是通过在注入事件后等待1微秒,然后再注入下一个事件并重复5000万次来实现的。要达到此精度级别,必须使用来自 CPU 的时间戳计数器。我们选择具有固定TSC的CPU,因为较旧的处理器由于节能和睡眠状态而遭受频率变化的影响。Intel Nehalem 和更高版本的处理器使用固定 TSC,可由在 Ubuntu 11.04 上运行的最新 Oracle JVM 访问。此测试未使用 CPU 绑定”

请注意,“Disruptor”的作者与Azul和其他JVM工作的人有着密切的联系。

另请参阅“Java 飞行记录幕后”。本演示将介绍新的固定 TSC 指令。