如何精确控制Java程序的执行时间?

21

据我所知,在启动Java应用程序之前,JVM会为其分配一些内存,并且在启动之前用户可以控制这段内存。但是当我启动一个应用程序时,每次执行所需的时间都是不同的,这也是另一个问题。

下面是一个非常简单的for循环示例:

package timespent;

public class Main {

    public static void main(String[] args) {

        long startTime = System.nanoTime();

        int j = 0;

        for (int i = 0; i < 1.E6; i++) {
            j = i + 1;
        }

        long endTime = System.nanoTime();

        long duration = (endTime - startTime);

        System.out.println("duration = " + duration);
    }
}

它输出不同的结果:

duration = 5720542
duration = 7331307
duration = 7737946
duration = 6899173

假设我想让它精确执行10,000,000纳秒或10毫秒。

我想要什么?

我希望Java应用程序能够在精确的时间内执行。

为什么需要这样做?

当我启动一个应用程序时,我希望在加载所有组件之前,在应用程序启动窗口上显示准确的执行时间。

我认为这是一种CPU操作,我想知道是否可能。

Q1:Java中是否可能实现?
Q2:如果Java中不可能实现,那么通过访问操作系统本地方法是否有任何方式实现此目标。例如通过将Java应用程序设置为优先级较高或其他方式?
Q3: 将应用程序的状态保存到文件中,然后将其加载到内存中如何?


18
它永远不可能是一样的,它取决于该特定时刻系统的状态。 - Saif Ahmad
13
请注意,7737946纳秒相当于0.007737946秒,因此您在结果中看到的差异大约是0.002秒-这在启动屏幕上并不明显。您最好发表一些更真实的演示以展示您所需要的内容。 - OldCurmudgeon
31
在我看来,这似乎是一个X/Y问题。如果你的目的是让你的应用程序“在精确的时间内执行”,那么你的解决方案就是使用定时器,而不是试图微调代码执行时间。 - ris8_allo_zen0
7
你的实际问题是什么? - Bergi
2
计时器不是解决方案。首先,你事先不知道程序执行需要多长时间,因为每次执行可能需要不同的时间;其次,计时器也不精确,即使 Thread.sleep(1000); 也不能准确地休眠1000毫秒。每次都可能有所差异,你可以自行检查。 - Asad Ganiev
显示剩余3条评论
7个回答

40

在时间测量中存在许多不确定性的来源。所有这些来源不仅会影响您的测量,而且运行时本身也是不确定的。其中不确定性的来源包括:

  • 缓存使用(CPU内存缓存了哪些部分)。您的数据可能会被CPU执行后台任务而从缓存中清除。

  • 内存位置(内存是否直接连接到执行CPU核心?)。这可能会随着时间的推移而发生变化,因为您的进程可能随时迁移到另一个核心。

  • 软件中断(操作系统抢占您的进程以运行其他进程)。可以通过在安静的机器上运行来减轻一些影响,但无法保证不会中断。

  • 热量限制(CPU决定它太热并降低时钟速度)。除非您准备在某个具有固定时钟速度的嵌入式处理器上工作,否则真的没有什么能做的。

  • 硬件中断(您的网络连接器从互联网上的另一台机器接收了一些数据)。您无法控制此类中断何时发生。

  • 不可预测的延迟(您正在从磁盘上读取某些数据,但首先,磁盘必须等待数据到达读头下方)。当您一遍又一遍地重复完全相同的操作时,这可能会遵循模式,但是一旦您收到一个不相关的硬件中断,这可能会导致意外延迟1 / 7200 rpm * 60 s / min = 8.3 ms

  • 垃圾回收(您正在询问Java,因此后台运行GC)。即使是最好的、最现代的垃圾收集器也无法完全避免停止世界的发生。而且即使它们不停止世界,它们仍然在后台运行,通过缓存、内存位置和软件中断引入运行时噪声。

这些可能是最重要的来源,但也可能有其他来源。关键是,您的进程永远不会独占机器。除非您在没有操作系统和禁用所有硬件中断的情况下运行,否则您必须接受每次执行的运行时间都会有所不同,而且没有办法解决这个问题。


一个实时操作系统(RTOS)可以让你对执行时间调度有一定的控制。 - JAB
1
@JAB 是的,但它不能保证某些执行速度(我的前两个观点,缓存和NUMA效应,这些都是硬件效应),因此仍然存在一些执行时间差异。您可以保证线程启动的时间以及它们实际获得的处理器时间量。这使得进程可以请求足够的时间,以便它们可以有信心在分配的时间范围内完成各自的工作。但这与在确定性时间完成任务是完全不同的事情。 - cmaster - reinstate monica
2
@cmaster:你没有提到一个重要的因素:_CPU速度_。对于像OP这样的紧密循环来说,CPU速度是至关重要的因素。而且,这本身又受到_温度_的影响,这是不可预测的。 - Mooing Duck
@MooingDuck 很好的观点。我已将其添加到列表中,命名为“热量调节”。谢谢。 - cmaster - reinstate monica

16

这是不可能的。首先,在纳秒级别上测量时间并不准确。我认为这篇文章很好地解释了这一点。

其次,你无法控制CPU如何调度执行。可能会有其他任务独占CPU时间,延迟程序的执行。


@JeromeReinlancer,你对于纳秒的观点是正确的,但是关于秒呢? - Asad Ganiev
3
@EroriCube 无论你如何衡量,最终你需要一个具备硬实时能力的操作系统才能达到这个目标。你可以用秒来测量,通常情况下会很准确,但是在某些时刻,当你的系统开始进行碎片整理等操作时,你可能会错过截止时间超过1秒的情况。 - rtur

9
任意代码的精确执行时间是不确定的,因为它取决于物理机器同时正在执行的其他事情。即使您计划通过跟踪启动时间戳和计划结束时间戳并在主线程上睡眠以便在两个时间戳之间持续一段时间然后退出程序来使执行时间“恒定”,仍会有相当大的变化。线程何时执行或等待多久超出了程序员的控制范围。

6
[TL;DR] 非常困难/不可能。
更详细的答案请参见Planarity Testing by Path Addition博士论文-基准测试方法章节,其中包括一些问题。
  1. 其他应用程序具有资源。即不具备足够内存,操作系统必须对应用程序进行分页;或者由于另一个应用程序共享 CPU 而导致代码运行缓慢。
  2. 时钟分辨率-您的代码只能以CPU所能承受的速度运行。将其移植到另一台计算机上,基准测试可能会给出截然不同的结果,因此不要仅针对自己的系统进行优化。
  3. 类加载-当JVM首次运行代码时,它必须将类加载到内存中(通常是从磁盘)并解析字节码,因此第一次运行比后续运行要慢得多。
  4. 即时编译-JVM在加载字节码时,将以纯解释模式运行它。一旦它运行了一块字节码(即您代码中的一个函数)多次(比如10,000次),它就可以将该函数编译成本地代码。编译将会使执行变慢,但随后的调用将更快,因为它运行的是本地代码而不是解释的代码。然而,这并不是一次性编译,如果JVM发现块内某些执行路径被偏爱,则可能重新编译字节码以尝试优化这些路径,并且这可能导致字节码的多个本地版本,直到JVM根据其统计数据稳定代码。
  5. 垃圾收集-有时Java调用垃圾收集器时会中断您的代码。

如果您想要对应用程序进行基准测试,以了解其最佳运行状态,则:

  • 尽可能停止其他应用程序;
  • 运行代码数万次;
  • 忽略前1万至2万次执行(以缓解类加载和JIT编译的影响);以及
  • 忽略发生垃圾回收时的迭代(这比听起来更难确定)。

这将为您提供最佳性能的想法,但最佳和实际情况是两个非常不同的事情。


5
唯一接近这种要求的方法是使用专门支持实时执行的操作系统上的实时Java。请参考实时Java

即使如此,您的执行时间也会受到电源供应器中噪音、房间环境温度、中国的蝴蝶等因素的影响。(我认为这就是为什么Matt小心地说“接近”的原因) - A C

3
正如其他人所说,由于其他影响程序速度的因素,无法准确知道剩余时间。但是,您可以将里程碑放入并参考过去的运行,从此次运行到目前为止实际时间与之前的差异中得出一个半准确的时间,例如在Windows上复制大型文件夹或在Chrome中下载大型文件时所做的操作。
因此,您没有说明您的确切问题,但让我们假设它类似于处理需要与第三方系统联系的100,000个操作,并且通常需要约15分钟才能完成。您可以跟踪1)开始时间,2)预计结束时间和3)部分完成情况。例如,当您完成其中一半时,可以测量经过的时间并认为这就是剩余的时间。基本上获取每秒操作的速率并将剩余操作数除以该速率即可得到剩余秒数。
double speed = completedOperations / elapsedSeconds;
double remainingSeconds = remainingOperations / speed;

这种情况是无法避免的,但你可以采取一些措施来减轻影响。你可能想要在最后30秒内计算速度以获得更准确的数据,或者记录历史上每天的平均速度。如果速度波动很大,你可以将总运行速度和最后一分钟的速度取平均值。
另一个可能会影响数据精度的因素是数据本身的变化。例如,如果你根据客户成为客户的日期对其进行处理,那么前10000个操作可能是针对已经与你合作多年、有大量数据需要处理的忠实客户,而后10000个操作可能是针对新客户且数据较少,处理速度较快。因此,你可以使用底层数据量而不是客户数量来计算进度。
然而,如果你希望结果精确(大多数情况下),你可以牺牲时间来伪造数据。找到最长的正常运行时间,然后只使用从开始到现在所用的时间来提供进度和剩余时间。然后当所有实际工作完成后,使用sleep()命令等待剩余时间。虽然仍然存在系统负载等原因导致执行时间异常长的风险,但你可以将最大时间改为这个新值。
你的运行时间可能遵循某种曲线规律,你可以将时间加长以提高单个运行完成的概率,但这样会导致更多的任务等待无用时间。
         #            ^
         #            |
        ###           |
        ###           |
       #####          R
      #######         U
    ###########       N
###################   S

Time-------------->

尽管这看起来很傻,但由于您无法控制的变量,您的应用程序将以不同的速度运行。如果近乎恒定的运行时间对您很重要,那么这是唯一的方法。


1
这样做行不通。它取决于系统状态,比如有多少系统资源正在运行你的程序。
我理解你想显示打开应用程序的剩余时间。在这种情况下,假设两个用户在不同的机器上运行你的程序,这些机器具有不同的核心结构和时钟频率...
但我可以给出一个建议,你可以读取程序的数据并根据此调整时间,就像其他应用程序一样显示已加载..%或类似于下载管理器功能显示已下载..%。

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