3

我正在使用 TPTP 来分析一些运行缓慢的 Java 代码,我遇到了一些有趣的事情。我的一个私有属性 getter 在执行时间分析结果中具有很大的基准时间值。公平地说,这个属性被调用了很多次,但我从来没想过这样的属性会花费很长时间:

public class MyClass{
    private int m_myValue;    
    public int GetMyValue(){
        return m_myValue;
    }
}

好的,所以类中显然有更多的东西,但是正如您所看到的,当调用 getter 时没有其他任何事情发生(只是返回一个 int)。给你一些数字:

  • 大约 30% 的运行调用都在 getter 上(我正在努力减少这种情况)
  • 运行的大约 25% 的基本时间都花在了这个 getter 上
  • 平均基准时间为 0.000175s

为了比较,我在不同的类中有另一个方法使用这个getter:

private boolean FasterMethod(MyClass instance, int value){
    return instance.GetMyValue() > m_localInt - value;
}

它的平均基准时间要低得多,为 0.000018 秒(低一个数量级)。

这是怎么回事?我认为有些东西我不明白或我遗漏了一些东西:

  1. 返回本地原语真的比返回计算值需要更长的时间吗?
  2. 我应该查看基准时间以外的指标吗?
  3. 这些结果是否具有误导性,我需要考虑其他一些分析工具吗?

编辑1:根据下面的一些建议,我将该方法标记为最终并重新运行测试,但我得到了相同的结果。

编辑 2:我安装了 YourKit 的演示版来重新运行我的性能测试,YourKit 结果看起来更接近我的预期。我将继续测试 YourKit 并报告我的发现。

编辑 3:更改为 YourKit 似乎解决了我的问题。我能够使用 YourKit 来确定我的代码中的实际慢点。下面有一些很好的评论和帖子(适当地赞成),但我接受第一个建议 YourKit 为“正确”的人。(我不以任何方式隶属于 YourKit / YMMV)

4

4 回答 4

6

如果可能,请尝试使用另一个分析器(Netbeans 运行良好)。这可能很难做到,具体取决于您的代码是如何设置的。

就像许多其他工具一样,不同的分析器可能会产生不同的信息。

返回本地原语真的比返回计算值需要更长的时间吗?

返回实例变量比返回局部变量(取决于 VM)花费更长的时间。您拥有的 getter 很简单,因此它应该被内联,因此它变得与访问公共实例变量一样快(同样,它比访问局部变量慢)。

但是您没有本地值(方法中的本地含义,而不是类中的本地含义)。

“本地”到底是什么意思?

我应该查看基准时间以外的指标吗?

我没有使用 Eclipse 工具,所以我不确定它是如何工作的……如果它是跟踪或采样分析器,它可能会有所不同(两者可以为这样的事情提供不同的结果)。

这些结果是否具有误导性,我需要考虑其他一些分析工具吗?

我会考虑另一种工具,只是看看结果是否相同。

根据评论编辑:

如果它是一个采样分析器,那么本质上,每个“n 时间单位”都会对程序进行采样以查看程序的位置。如果您调用一种方法的方式多于另一种方法,它将显示为在被调用更多的方法中(很可能该方法正在运行)。

跟踪分析器将代码添加到您的程序(称为检测的过程),以从本质上记录正在发生的事情。

跟踪分析器速度较慢但更准确,它们还需要更改程序(检测过程),这可能会引入错误(我没有听说过它发生......但我相信至少在他们正在开发时会这样做探查器)。

采样分析器更快但不太准确(它们只是猜测一行代码的执行频率)。

因此,如果 Eclipse 使用采样分析器,您可能会看到您认为奇怪的行为。更改为跟踪分析器将显示更准确的结果。

如果 Eclipse 使用跟踪分析器,那么更改分析器应该显示相同的结果(但是它们新的分析器可能会让您更清楚地了解正在发生的事情)。

于 2009-03-27T16:12:48.727 回答
3

听起来确实有点误导 - 也许分析器正在删除一些优化?

只是为了好玩,尝试使方法最终,这将更容易内联。这很可能是属性和 FasterMethod 之间的区别。在实际使用中,HotSpot 甚至会内联虚拟方法,直到它们第一次被覆盖(IIRC)。

编辑:回应布赖恩的评论:是的,通常情况下,制作最终的东西不会帮助性能(尽管它在设计方面可能是一件好事:)因为热点通常会根据它是否可以内联来确定它是否可以内联无论它是否被覆盖。我的意思是这个分析器可能已经搞砸了。

编辑:我现在已经设法重现 HotSpot“撤消”尚未扩展的类(或尚未覆盖的方法)的优化的方式。对于服务器 VM,这比客户端更难做到,但我已经做到了 :)

public class Test
{
    public static void main(String[] args)
        throws Exception
    {
        final long iterations = 1000000000L;
        Base b = new Base();
        // Warm up Hotspot
        time(b, 1000);

        // Before we load Derived
        time(b, iterations);

        // Load Derived and use it quickly
        // (Just loading is enough to make the client VM
        // undo its optimizations; the server VM needs more effort)
        Base d = (Base) Class.forName("Derived").newInstance();
        time(d, 1);

        // Time it again with Base
        time(b, iterations);
    }

    private static void time(Base b, long iterations)
    {
        long total = 0;
        long start = System.currentTimeMillis();
        for (long i = 0; i < iterations; i++)
        {
            total += b.getValue();
        }
        long end = System.currentTimeMillis();
        System.out.println("Time: " + (end-start));
        System.out.println("Total: " + total);
    }
}

class Base
{
    public int getValue() { return 1; }
}

class Derived extends Base
{
    @Override
    public int getValue() { return 2; }
}
于 2009-03-27T16:08:08.813 回答
2

这听起来很奇特。您不会错误地调用覆盖方法,是吗?

我很想下载YourKit 的演示版本。设置起来很简单,它应该可以指示真正发生的事情。如果 TPTP 和 YourKit 都同意,那么就会发生一些奇怪的事情(我知道这并没有多大帮助!)

于 2009-03-27T16:10:39.073 回答
0

曾经对这类方法的性能产生很大影响的东西(尽管这可能在某种程度上是历史性的)是调用方法的大小可能是一个问题。HotSpot(和严重的竞争对手)会很乐意内联小方法(有些可能会在同步/最终尝试时窒息)。但是,如果调用方法很大,则可能不会。这对于旧版本的 HotSpot C1/客户端来说尤其是一个问题,它有一个非常糟糕的寄存器分配算法(它现在有一个非常好的和快速的算法)。

于 2009-03-27T18:17:55.617 回答