为什么执行Mockito嘲讽的表现如此不稳定?

时间:2014-07-27 17:29:03

标签: java performance mockito

为什么执行Mockito嘲笑所花费的时间如此不稳定,是否有人会有解释,甚至更好的建议解决方案?我能想到的最简单的SSCCE如下:

import static org.mockito.Mockito.mock;

public class TestSimpleMockTiming
{
    public static final void main (final String args [])
    {
        final Runnable theMock = mock (Runnable.class);

        int tookShort = 0;
        int tookMedium = 0;
        int tookLong = 0;
        int tookRidiculouslyLong = 0;
        long longest = 0;

        for (int n = 0; n < 2000000; n++)
        {
            final long startTime = System.nanoTime ();
            theMock.run ();
            final long duration = System.nanoTime () - startTime;

            if (duration < 1000000)                 // 0.001 seconds
                tookShort++;
            else if (duration < 100000000)      // 0.1 seconds
                tookMedium++;
            else if (duration < 1000000000)     // 1 second !!!
                tookLong++;
            else
                tookRidiculouslyLong++;

            longest = Math.max (longest, duration);
        }

        System.out.println (tookShort + ", " + tookMedium + ", " + tookLong + ", " + tookRidiculouslyLong);
        System.out.println ("Longest duration was " + longest + " ns");
    }
}

如果我运行它(在Eclipse中,在Win 7 x64上使用JDK 1.7.45),典型输出如下:

1999983, 4, 9, 4
Longest duration was 5227445252 ns

因此,虽然在大多数情况下模拟执行得非常快,但是有几次执行甚至超过1秒。对于一种什么都不做的方法来说,这是永恒的。从我的实验中,我不相信问题是System.nanoTime()的准确性,我认为模拟确实需要花费很长时间才能执行。我能做些什么来改善这一点并使时间表现更一致吗?

(仅供参考,为什么这是一个问题是我有一个包含各种框架的Swing应用程序,我尝试为框架编写JUnit测试,以便我可以测试layoutManagers的行为是否正常而无需启动整个框架应用程序并导航到正确的屏幕。在一个这样的测试中,屏幕使用javax.swing.Timer来实现滚动,因此当鼠标靠近帧的末端时,显示将围绕一个区域平移。我注意到了这一行为这是非常不稳定的,滚动虽然通常很好,会周期性地冻结一秒钟而且看起来很可怕。我在这周围写了一个SSCCE,认为问题是Swing Timers不能依赖于射击一致的比率,在SSCCE中它运作得很好。

经过几个小时的撕裂我的头发然后试图发现我的真实代码和滚动演示SSCCE之间的差异,我开始将纳米计时器放在重复运行的代码块周围,注意到我的paintComponent方法所花费的时间非常不稳定并最终将其缩小为模拟电话。通过运行真实应用程序来测试屏幕,滚动行为很顺利,因为模拟调用,它只是JUnit测试中的一个问题,这导致我使用上面发布的SSCCE单独测试一个简单的模拟。)

非常感谢!

2 个答案:

答案 0 :(得分:4)

这项测试有多种方式存在缺陷。如果你想要正确地进行基准测试,我强烈建议使用JMH,这是由某人 Alexey Shipilev 完成的,这比我们聪明得多,而且对JVM的了解比大多数做Java的人都要多。我们敬爱的星球。

这是测试存在缺陷的最显着方式。

  1. 测试忽略了JVM正在做的事情,比如预热阶段,编译C1和C2线程,GC,线程问题(即使这段代码不是多线程的,JVM / OS可能需要做一些事情其他)等......

  2. 如果实际的OS / JVM / CPU组合提供高达纳秒的正确分辨率,测试似乎会忽略。

    即使有System.nanoTime(),您确定JVM和操作系统具有适当的分辨率。例如,在Windows上,JVM无法访问真正的纳秒,而是访问某些计数器,而不是挂钟时间。 javadoc说明了这一点,这里是代码段:

      

    此方法只能用于测量经过的时间,与系统或挂钟时间的任何其他概念无关。返回的值表示纳秒,因为某些固定但任意的原始时间(可能在将来,因此值可能为负)。在Java虚拟机的实例中,此方法的所有调用都使用相同的原点;其他虚拟机实例可能使用不同的来源。

         

    此方法提供纳秒精度,但不一定是纳秒级分辨率(即,值的变化频率) - 不保证除了分辨率至少与currentTimeMillis()

  3. 测试也忽略了Mockito的工作原理。

    Mockito 将每次调用存储在自己的模型中,以便能够在执行方案后验证这些调用。因此,在循环的每次迭代中,Mockito都会存储另一个最多2M调用的调用,这将影响JVM(也许模拟实例将持有几代并升级到终端,这对于GC来说肯定更昂贵)。这意味着迭代越多,这个代码就越强调JVM而不是Mockito。

    我相信它没有发布(然而在jcentral上有dev二进制文件),但是Mockito将提供一个设置,允许mockito只存储,因此它不会存储调用,这可能允许Mockito在这样的场景中很好地适应。

  4. 测试缺乏适当的统计分析。

    有趣的是,测试代码具有伪百分位方法。哪个好!虽然它不能像那样工作,但在这种情况下它无法解决大问题。相反,它应该记录每一个度量,以便提取随着迭代计数的推进而模拟时间的演变趋势。

    如果您愿意,最好存储每个记录的度量,因此可以将它们提供给适当的统计分析工具,如 R ,以便提取图表,百分位数据等

    关于统计问题,使用HDRHistogram肯定会很有趣。在微基准测试之外,当然它会影响存储器并改变微基准测试的结果。让我们为JMH保留。

  5. 如果您将代码更改为使用JMH,则可以解决第1点和第2点。

    希望有所帮助。

答案 1 :(得分:1)

JVM是一个非常复杂的事情,它在运行时进行了大量优化(包括缓存和字节码优化)。因此,测量Java程序的执行时间,首先应该在进行实际基准测试之前进行预热阶段。

我希望您的前四​​次运行时间最长,然后执行时间越来越好。

在实际开始分析之前,执行几百或几千次基准测试。之后,我希望您的测量结果会更稳定。