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



我创建了一个Java程序来以特定频率发出事件。我使用System.nanoTime()而不是Thread.sleep()因为根据这里和这里的许多参考,第一个在区间上提供了更高的精度。但是,我想当我尝试将其设置为发出 1M 记录/秒的数据速率时,它没有实现目标。这是我的代码:

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) ;
}

因此,当我在变量中等待 10000 纳秒delayInNanoSeconds时,我将获得 100K 的工作负载 记录/秒 (1_000_000_000/10_000 = 100K r/s(。当我在变量中等待 2000 纳秒delayInNanoSeconds时,我的工作负载为 500K 记录/秒 (1_000_000_000/2_000 = 500K r/s(。对于 1000 纳秒,我将获得 1M 的工作负载 记录/秒 (1_000_000_000/1000 = 1M r/s(。对于 500 纳秒,工作负载为 2M rec/秒 (1_000_000_000/500 = 2M r/s(。

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

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

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

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


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

(对于像Intel Tremont(下一代Goldmont(这样的未来CPU,LockSupport.parkNanos可以移植地公开tpause等功能,以在给定的TSC截止日期之前闲置CPU内核。 不是通过操作系统,只是一个超线程友好的截止时间暂停,适合在 SMT CPU 上短暂睡眠。

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

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


如果您的系统足够快,可以在每次迭代运行一次时间获取函数时跟上,这将起作用。 如果没有,那么您可以每 4 次迭代(循环展开(检查一次截止日期,以摊销nanoTime()的成本,以便您登录 4 次或其他内容的突发。

当然,如果您的系统速度不够快,即使根本没有延迟调用,您需要优化一些东西来解决这个问题。 你不能延迟负的时间,检查时钟本身需要时间。

最新更新