Java中的时间测量开销

55

在低级别上测量经过的时间时,我可以选择使用以下任何一种:

System.currentTimeMillis();
System.nanoTime();

这两种方法都是使用native实现的。在深入了解任何C代码之前,是否有人知道调用其中一个方法是否存在实质性的开销?我的意思是,如果我不关心额外的精度,哪个方法预计会消耗更少的CPU时间?

注意:我正在使用标准的Java 1.6 JDK,但该问题可能适用于任何JRE...


2
请注意,nanoTime 不映射到实际时间,只应用于测量某事物所花费的时间。 - Powerlord
@R.Bemrose,谢谢,我知道。这是为了测量某件事情需要多长时间。 - Lukas Eder
8个回答

48
这个页面上标记为正确答案的回答实际上是不正确的。那种写法是无效的基准测试方法,因为会有JVM死代码消除(DCE)、栈替换(OSR)、循环展开等因素影响。只有像Oracle的JMH微基准测试框架这样的工具才能够适当地测量。如果你对此类微基准测试的有效性有任何疑问,请阅读 这篇文章。 这里是一个对System.currentTimeMillis()System.nanoTime()进行 JMH 基准测试的例子:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class NanoBench {
   @Benchmark
   public long currentTimeMillis() {
      return System.currentTimeMillis();
   }

   @Benchmark
   public long nanoTime() {
    return System.nanoTime();
   }
}

以下是在Intel Core i5上的测试结果:

Benchmark                            Mode  Samples      Mean   Mean err    Units
c.z.h.b.NanoBench.currentTimeMillis  avgt       16   122.976      1.748    ns/op
c.z.h.b.NanoBench.nanoTime           avgt       16   117.948      3.075    ns/op

这表明每次调用 System.nanoTime() 的速度比约为 123 纳秒的另一种方法快了约 118 纳秒。然而,考虑到平均误差后,两者之间的差异非常小。结果也可能因操作系统而异。但总的来说,在开销方面它们基本上是等效的。

更新于2015/08/25:尽管这个答案更接近正确,使用 JMH 进行测量仍然不正确。像 System.nanoTime() 这样的测量本身就是一种特殊的扭曲基准测试。答案和权威文章在这里


JMH 的使用非常出色!我希望这个网站上有更多的微基准测试能够利用它。 - Joe C
我想尝试你的示例,不知道为什么 @GenerateMicroBenchmark 注释无法工作。最近它似乎已经更名为 @Benchmark - dajood

26

我认为您不需要担心它们的开销。它们的开销非常小,几乎可以忽略不计。以下是两者的快速微基准测试:

for (int j = 0; j < 5; j++) {
    long time = System.nanoTime();
    for (int i = 0; i < 1000000; i++) {
        long x = System.currentTimeMillis();
    }
    System.out.println((System.nanoTime() - time) + "ns per million");

    time = System.nanoTime();
    for (int i = 0; i < 1000000; i++) {
        long x = System.nanoTime();
    }
    System.out.println((System.nanoTime() - time) + "ns per million");

    System.out.println();
}

最终结果为:

14297079ns per million
29206842ns per million

看起来System.currentTimeMillis()System.nanoTime()快两倍。但是29纳秒的时间短到你无论如何都会测不出其他东西了。我会选择使用System.nanoTime()来获得精确度和准确性,因为它没有关联时钟。


1
不错的基准测试。但是在我的电脑上,情况恰好相反。我的输出结果是:每百万次执行需要17258920ns和14974586ns。这意味着实际结果取决于JVM、处理器、操作系统等因素。除此之外,这种差异几乎可以忽略不计。感谢您给出这样好的答复! - Lukas Eder
没问题。是的,我确定有各种因素会导致它变化。无论如何,在现代计算机上每秒调用数百万次才会导致明显的时间开销。 - WhiteFang34
底层操作系统可能很有趣。我怀疑如果这个实现使用POSIX的 clock_gettime(),差异会是 ~0。 - ninjalj
@b1naryatr0phy是正确的,微基准测试几乎总是做错了。请查看我在本页面底部的答案。 - brettw
2
变量X未被使用,因此可以优化掉获取时间的系统调用。在后续阶段,可能会发现循环内没有任何工作可做,它们也可以被优化掉。留下很少的东西可以测量。微基准测试和JIT的危险性。 - UnixShadow
显示剩余5条评论

11

你应该只使用System.nanoTime()来测量某个操作的运行时间。这不仅是因为纳秒级别的精度,System.currentTimeMillis()是"墙上钟表时间",而System.nanoTime()则旨在用于计时,没有另一个方法那样具有"现实世界时间"的怪癖。从System.nanoTime()的Javadoc中可以看到:

此方法仅可用于测量经过时间,与任何其他系统或墙上钟表时间概念无关。


是的,我知道Javadoc提到了这一点。但是如果你多次测量1-10毫秒之间的事物,两者可以等效使用。你所说的“现实世界时间怪癖”是什么意思? - Lukas Eder
3
问题在于System.currentTimeMillis()可能会插入小的时钟调整,从而影响计时。虽然这种情况很少见,但是nanoTime()旨在进行不受此类问题影响的计时测量。 - ColinD
@Lukas:实际上,它基本上只反映了计算机显示的系统时间。在使用currentTimeMillis()进行定时时将计算机日期更改为一周前,您将得到一个不错的负数!=)但对于nanoTime()则不是这样。 - ColinD
不会发生的,但是想法很好 :-) - Lukas Eder
1
你需要担心的不仅是手动更改时间。还有夏令时以及通过与互联网服务器进行检查后自动调整的时钟。 - Jonathan

8

System.currentTimeMillis()通常非常快(据我所知大约需要5-6个CPU周期,但是我已经不记得在哪里读到过了),但其分辨率因不同平台而异。

因此,如果您需要高精度,请使用nanoTime(),如果担心开销,请使用currentTimeMillis()


好的观点。看看WhiteFang34的回答。根据用于基准测试的系统,它们似乎同样快。 - Lukas Eder

6

如果你有时间,可以观看Cliff Click的这个演讲,他谈到了System.currentTimeMillis的代价以及其他相关内容。


4
这个问题的被接受答案确实是错误的。@brettw提供的替代答案不错,但细节方面还是有所欠缺。
要全面了解这个主题以及这些调用的真正成本,请参见https://shipilev.net/blog/2014/nanotrusting-nanotime/ 回答所问的问题:
调用其中一个函数是否会有任何实质性开销?
System#nanoTime的开销在每次调用时为15到30纳秒之间。
nanoTime报告的值只有在每30纳秒更改一次其分辨率。
这意味着,如果你试图每秒处理数百万个请求,调用nanoTime会导致你在大量时间内失去一个巨大的时间块。对于这种情况,请考虑从客户端测量请求,从而确保您不会陷入协调省略,同时测量队列深度也是一个很好的指标。
如果您不试图在单个秒钟内尽可能多地工作,那么nanoTime并不重要,但协调省略仍然是一个因素。
最后,为了完整起见,无论其成本如何,都不能使用currentTimeMillis。这是因为它不能保证在两次调用之间向前移动。特别是在具有NTP的服务器上,currentTimeMillis不断在变化。更不用说计算机测量的大多数事物都不需要整个毫秒。

感谢您的反馈。同时,我也偶然发现了Aleksey Shipilëv的那篇文章。您能否在我的具体Stack Overflow问题的答案中概括一下那篇文章的要点,以符合Stack Overflow的精神(即提供一个完整的答案,并可能在Stack Overflow上提供进一步链接的详细信息)?然后我会接受您的答案。 - Lukas Eder
我这样做不是为了得到一个被接受的答案。问题的根本在于被接受的答案会引导人们走向错误的方向,这是不幸的。 - pjulien
我已经根据你的要求进行了更新,但同时,我总觉得像这样简短的 StackOverflow 回答往往会引起麻烦,因为它们总是留下太多解释的空间。 - pjulien

2

在理论层面上,对于使用本地线程的虚拟机,并且运行在现代抢占式操作系统上,currentTimeMillis 可以实现为每个时间片仅读取一次。可以预想,nanoTime 实现不会牺牲精度。


关于精度,这是一个很好的观点。如果你是对的,那么 currentTimeMillis() 就不能更精确了。或者因为它不需要很精确,所以可以这样实现。但另一方面,这并不说明哪个更快... - Lukas Eder

0

currentTimeMillis() 的唯一问题在于当您的虚拟机调整时间时(通常会自动发生),currentTimeMillis() 也会随之变化,从而导致不准确的结果,尤其是在基准测试中。


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