如何使用 System.nanoTime() 准确地延迟循环迭代以达到每秒 1M 的频率?

How can I delay a loop iteration accurately to hit frequencies like 1M per second, with System.nanoTime()?

我创建了一个 Java 程序来以特定频率发出事件。我使用 System.nanoTime() 而不是 Thread.sleep(),因为根据许多参考 here and here,第一个给出了更高的区间精度。但是,我想当我尝试将它设置为发出 1M records/second 的数据速率时,它没有达到目标。这是我的代码:

long delayInNanoSeconds = 1000000;

private void generateTaxiRideEvent(SourceContext<TaxiRide> sourceContext) throws Exception {
    gzipStream = new GZIPInputStream(new FileInputStream(dataFilePath));
    reader = new BufferedReader(new InputStreamReader(gzipStream, StandardCharsets.UTF_8));
    String line;
    TaxiRide taxiRide;
    while (reader.ready() && (line = reader.readLine()) != null) {
        taxiRide = TaxiRide.fromString(line);
        sourceContext.collectWithTimestamp(taxiRide, getEventTime(taxiRide));
        // sleep in nanoseconds to have a reproducible data rate for the data source
        this.dataRateListener.busySleep();
    }
}

public void busySleep() {
    final long startTime = System.nanoTime();
    while ((System.nanoTime() - startTime) < this.delayInNanoSeconds) ;
}

所以,当我在 delayInNanoSeconds 变量中等待 10000 纳秒时,我将获得 100K 的工作负载 rec/sec (1_000_000_000 / 10_000 = 100K r/s)。当我在 delayInNanoSeconds 变量中等待 2000 纳秒时,我将得到一个工作负载 500K rec/sec(1_000_000_000 / 2_000 = 500K r/s)。对于 1000 纳秒,我将获得 1M 的工作负载 rec/sec (1_000_000_000 / 1000 = 1M r/s)。对于 500 纳秒,2M rec/sec 的工作负载(1_000_000_000 / 500 = 2M r/s)。

我看到 here 使用 double 代替 long 可以更好地提高精度。它有某种关系吗?或者问题只是一个 OS 限制(我正在使用 Linux Ubuntu 18)?或者也许是因为我正在使用 readLine() 方法并且有更快的方法来发出这些事件?我认为当我使用 GZIPInputStream class 时我正在将整个文件加载到内存中并且 readLine() 不再访问磁盘。如何提高应用程序的数据速率?

@TobiasGeiselmann 提出了一个很好的观点:您的延迟计算没有考虑 调用 busySleep[=49 之间花费的时间=]

您应该计算相对于上一个截止日期 的截止日期,而不是 日志记录之后的当前时间。也不要使用之前 System.nanoTime() 的结果;那将是 >= 实际截止日期的某个时间(因为 nanoTime 本身需要时间,至少几纳秒,所以它不可避免地睡过头了)。你会这样累积错误。

在第一次迭代之前,找到当前时间并设置long deadline = System.nanoTime();。在每次迭代结束时,执行 deadline += 1000; 并使用您的忙等待循环旋转直到现在 >= deadline.


如果 deadline - now 足够大,使用一些可以将 CPU 产生给其他线程的东西,直到接近唤醒截止日期 。根据评论,LockSupport.parkNanos(…) 是现代 Java 的不错选择,实际上可能会忙等待足够短的睡眠时间(?)。我真的不知道Java。如果是这样,您应该只检查当前时间,计算到截止日期的时间,然后调用一次。

(对于像 Intel Tremont(下一代 Goldmont)这样的未来 CPU,LockSupport.parkNanos 可以便携地公开像 tpause 这样的功能以空闲 CPU 核心直到给定 TSC 截止日期。不是通过 OS,只是超线程友好的截止日期暂停,适合在 SMT CPUs 上短暂休眠。)

忙等待通常不好,但适用于高精度非常短的延迟。在具有当前 OSes 的当前硬件上,1 微秒不足以有效地让 OS 上下文切换到其他内容并返回。但是更长的睡眠间隔(当你选择了较低的频率时)应该睡眠让 OS 在这个核心上做一些有用的事情,而不是忙着等待这么久。

理想情况下,当你进行时间检查时,你会在延迟循环中执行像 x86 的 pause 这样的指令,以便对共享相同物理内核的其他逻辑内核更友好(超线程) /贴片机)。 Java 9 Thread.onSpinWait(); 应该在自旋等待循环中调用(尤其是在等待内存时),这让 JVM 以可移植的方式公开了这个概念。 (我想这就是它的用途。)


如果您的系统足够快,可以跟上 运行 每个迭代一次的时间获取功能,这将起作用。如果没有,那么您可以每 4 次迭代(循环展开)检查一次截止日期,以分摊 nanoTime() 的成本,这样您就可以连续登录 4 次或类似的东西。

当然,如果您的系统在 没有 延迟调用的情况下仍然不够快,您将需要优化一些东西来解决这个问题。你不能延迟负数的时间,检查时钟本身也需要时间。