在忙碌等待中调用System.nanoTime()需要比平时更长的时间随机

时间:2017-07-21 12:46:01

标签: java performance

我需要重复向服务器发送tcp包。发送速度为每秒800 msgs,每个msg应以1.25ms的均匀速度发送。我计算发送使用的时间,并将线程休眠1.25ms - 使用Thread.sleep()发送时间。但我发现Thread.sleep()在我的机器上至少需要1ms,即使我通过1ns也是如此。在网上进行一些搜索后,我将睡眠方法更改为忙等待:

long delayNanoSec = 1250000;

long endTime = System.nanoTime() + delayNanoSec;
while (System.nanoTime() < endTime);

当delayNanoSec&lt; 12500或&gt; 12500000,它在delayNanoSec周围花了一些时间,但是当设置为1250000时,它会比delayNanoSec随机花费更长的时间。我将代码更改为记录每个循环中System.nanoTime()使用的时间:

long[] timeUsed = new long[50000];

long delayNanoSec = 1250000;

long endTime = System.nanoTime() + delayNanoSec;
long lastTime = 0;
int index = 0;
do {
    lastTime = System.nanoTime();
    timeUsed[index++] = lastTime;
}while(lastTime < endTime);

for(int i = 0; i < times-1; i++) {
    System.out.println("INDEX: " + i + " TIME USED: " + (nanos[i+1] - nanos[i]));
}

我有这样的事情:

INDEX: 1729 TIME USED: 226
INDEX: 1730 TIME USED: 387
INDEX: 1731 TIME USED: 240
INDEX: 1732 TIME USED: 240
INDEX: 1733 TIME USED: 219
INDEX: 1734 TIME USED: 246
INDEX: 1735 TIME USED: 289
INDEX: 1736 TIME USED: 283
INDEX: 1737 TIME USED: 245
INDEX: 1738 TIME USED: 244
INDEX: 1739 TIME USED: 4254974

多次测试此代码后,看起来System.nanoTime()会花费很长时间随机返回。在大多数情况下,它花费大约250ns,但有时花费超过3-4ms。

有谁知道这个的原因?或者有更好的方法来完成这项工作吗? (控制每次发送之间的时间)

顺便说一句,对不起我的英语,希望你明白我的意思。

1 个答案:

答案 0 :(得分:0)

摘要

您的代码示例构成了JVM微基准测试,但您没有控制外部因素。特别是,我的测试表明在测试运行期间JIT编译仍然非常繁忙。这个特殊问题可以通过在进行任何测量之前运行足够的预热阶段来解决。

详细

微型基准测试代码在JVM上运行是非常棘手的,因为在测试代码的控制之外,有许多混淆因素可以非确定性地发生。这些因素包括垃圾收集,类加载,类的静态初始化和JIT编译。这些都是在没有被测量代码直接调用的情况下发生的,并且它们都可能导致执行被测代码所花费的额外时间。

关于JVM上的微基准测试,有一个很好的先前问题。

How do I write a correct micro-benchmark in Java?

特别注意关于预热阶段的规则1,推荐&#34; ... [经验]经验是数万次内循环迭代。&#34;

我决定将这些技术应用于您的代码示例。首先,我将测试代码简化为:

class TestNano {

    public static void main(String[] args) throws Exception {
        long[] nano = new long[10000];

        for (int i = 0; i < nano.length; ++i) {
            nano[i] = System.nanoTime();
        }

        for (int i = 0; i < nano.length; ++i) {
            System.err.println("INDEX: " + i
                    + " NANO: " + nano[i]
                    + " TIME USED: " + (i == 0 ? 0 : (nano[i] - nano[i - 1])));
        }
    }
}

我使用这个命令执行它:

java -XX:+UnlockDiagnosticVMOptions \
        -XX:CompileCommand=print,*.* \
        -XX:+PrintAssembly \
        -Xbatch \
        -XX:CICompilerCount=1 \
        -XX:+PrintGCDetails \
        -XX:+PrintGCTimeStamps \
        -verbose:gc \
        TestNano 2>&1 | tee TestNano.out

请注意,在代码中使用System.err非常重要。额外的JVM诊断信息打印到stderr,我想要输出和我们自己的&#34; INDEX:...&#34;输出在同一个文件中,这样我们就可以看到JVM活动如何与循环迭代重合。

这产生了一些类似于你的结果:

INDEX: 9976 NANO: 1500876274305377000 TIME USED: 0
INDEX: 9977 NANO: 1500876274305377000 TIME USED: 0
INDEX: 9978 NANO: 1500876274305377000 TIME USED: 0
INDEX: 9979 NANO: 1500876274307294000 TIME USED: 1917000
INDEX: 9980 NANO: 1500876274307295000 TIME USED: 1000
INDEX: 9981 NANO: 1500876274307295000 TIME USED: 0
INDEX: 9982 NANO: 1500876274307295000 TIME USED: 0

INDEX: 14560 NANO: 1500876274307599000 TIME USED: 0
INDEX: 14561 NANO: 1500876274307599000 TIME USED: 0
INDEX: 14562 NANO: 1500876274307599000 TIME USED: 0
INDEX: 14563 NANO: 1500876274312344000 TIME USED: 4745000
INDEX: 14564 NANO: 1500876274312358000 TIME USED: 14000
INDEX: 14565 NANO: 1500876274312358000 TIME USED: 0
INDEX: 14566 NANO: 1500876274312358000 TIME USED: 0

当我看到这种模式时,我总是怀疑垃圾收集,但在这种情况下,我没有看到我启用的详细GC日志记录中的垃圾收集的证据。

相反,每当我有一个多毫秒的光点时,它大致与编辑信息的打印相对应,看起来类似于:

Compiled method (c2)     647   64             java.lang.StringBuilder::toString (17 bytes)
 total in heap  [0x000000010aae8610,0x000000010aae8d80] = 1904
 relocation     [0x000000010aae8730,0x000000010aae8760] = 48
 main code      [0x000000010aae8760,0x000000010aae8a40] = 736
 stub code      [0x000000010aae8a40,0x000000010aae8a58] = 24
 oops           [0x000000010aae8a58,0x000000010aae8a80] = 40
 scopes data    [0x000000010aae8a80,0x000000010aae8b60] = 224
 scopes pcs     [0x000000010aae8b60,0x000000010aae8d20] = 448
 dependencies   [0x000000010aae8d20,0x000000010aae8d28] = 8
 handler table  [0x000000010aae8d28,0x000000010aae8d70] = 72
 nul chk table  [0x000000010aae8d70,0x000000010aae8d80] = 16
Decoding compiled method 0x000000010aae8610:
Code:
[Entry Point]
[Constants]
  # {method} 'toString' '()Ljava/lang/String;' in 'java/lang/StringBuilder'
  #           [sp+0x50]  (sp of caller)
  0x000000010aae8760: mov    0x8(%rsi),%r10d
  0x000000010aae8764: shl    $0x3,%r10
  0x000000010aae8768: cmp    %r10,%rax

我看到在前20,000次循环迭代中间歇性地发生了重要的JIT编译活动。之后,我没有看到编译活动,TIME USED一直是亚毫秒级。

以上所有内容均附有警告,您的结果可能会有所不同。性能分析通常特定于执行测试的环境。就我而言,我在使用此JVM版本的Mac上运行测试:

java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)

尽管如此,我认为我们在这里有充分的证据表明,根本原因是由于JIT编译而导致停顿不足的预热时间。

您的程序的一个关键结论是,您不能依赖JVM在截止日期前精确执行,尤其是以纳秒为单位的紧张期限。正如评论者所说,你不会得到实时行为。它可能在大多数时候都可以正常工作,但是像这样会出现更长延迟的异常值。 (根据您的应用,偶尔可以接受这样的停顿。)