Java 微优化:要缓存还是不缓存 System.currentTimeMillis() return 值?

Java Micro-optimization: To cache or not to cache a System.currentTimeMillis() return value?

一个简单的问题,我一直想知道。以下两个版本的代码,哪个优化得更好?假设 System.currentTimeMillis() 调用产生的时间值只需要 漂亮 准确,那么缓存应该只从性能的角度考虑。

这个(带值缓存):

    long time = System.currentTimeMillis();
    for (long timestamp : times) {
        if (time - timestamp > 600000L) {
            // Do something
        }
    }

或者这个(无缓存):

    for (long timestamp : times) {
        if (System.currentTimeMillis() - timestamp > 600000L) {
            // Do something
        }
    }

我假设 System.currentTimeMillis() 已经是一个非常优化和轻量级的方法调用,但我们假设我将在短时间内多次调用它。

"times" collection/array 必须包含多少个值才能证明在其自己的变量中缓存 System.currentTimeMillis() 的 return 值是合理的?

从 CPU 或内存优化的角度来看,这样做更好吗?

一个long基本上是免费的。带有 JIT 编译器的 JVM 可以将其保存在寄存器中,并且由于它是循环不变量,甚至可以将循环条件优化为 -timestamp < 600000L - timetimestamp > time - 600000L。即循环条件成为迭代器和寄存器中循环不变常量之间的简单比较。

所以是的,将函数调用提升到循环之外并将结果保存在变量中显然更有效,尤其是当优化器无法为您执行此操作时,尤其是当结果是原始类型时,不是对象。


假设您的代码 运行 在 JIT 为 x86 机器代码的 JVM 上,System.currentTimeMillis() 可能至少包含一个 rdtsc 指令和该结果的一些缩放1。因此,它可以 可能 最便宜的(例如在 Skylake 上)是一个微编码的 20-uop 指令,每 25 个时钟周期吞吐量为一个(http://agner.org/optimize/)。

如果你的 // Do something 很简单,比如通常在缓存中命中的一些内存访问,或者一些更简单的计算,或者乱序执行可以很好地完成的任何其他事情,那可能是您循环的大部分成本。除非每个循环迭代通常需要几微秒(即 4GHz 超标量 CPU 上数千条指令的时间),否则将 System.currentTimeMillis() 提升到循环之外可能会产生可测量的差异。 小与大将取决于您的循环体的简单程度。

如果你能证明将它提升到你的循环之外不会导致正确性问题,那就去做吧。

即使它在你的循环中,你的线程仍然可以在调用它和完成该迭代的工作之间无限长度地休眠。但是将它提升到循环之外会使您更有可能在实践中实际观察到这种效果; 运行 次迭代 "too late".


脚注 1:在现代 x86 上,时间戳计数器以固定速率运行,因此它作为低开销时间源很有用,而对于周期精确的时间源则不太有用微基准测试。 (为此使用性能计数器,或禁用 turbo/节能,因此核心时钟 = 参考时钟。)

IDK 如果 JVM 实际上会去实现它自己的时间函数的麻烦。它可能只使用 OS 提供的时间函数。在Linux上,gettimeofdayclock_gettime是在user-space中实现的(内核将代码+比例因子数据导出到user-space内存中,in the VDSO region).所以 glibc 的包装器只是调用它,而不是制作一个实际的 syscall.

因此,与切换到内核模式并返回的实际系统调用相比,clock_gettime 可能非常便宜。在启用了 Spectre + Meltdown 缓解的内核上,这可能需要

所以是的,假设 System.currentTimeMillis() 是 "very optimized and lightweight" 是安全的,但与某些循环体相比,即使 rdtsc 本身也很昂贵。

在您的情况下,方法调用应始终提升到循环之外。


System.currentTimeMillis() 只是从 OS 内存中读取一个值,因此它非常便宜(几纳秒),而不是 System.nanoTime(),它涉及对硬件的调用,并且因此可以慢几个数量级。