Профилирование Java: получатель частной собственности имеет большое базовое время

Я использую TPTP для профилирования медленно работающего Java-кода и наткнулся на кое-что интересное. Один из моих получателей частной собственности имеет большое значение базового времени в результатах анализа времени выполнения. Честно говоря, это свойство вызывается много-много раз, но я никогда бы не подумал, что такое свойство займет очень много времени:

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

Итак, очевидно, что в классе есть еще что-то, но, как вы можете видеть, при вызове геттера больше ничего не происходит (просто возвращает int). Несколько цифр для вас:

  • Около 30% вызовов прогона приходится на геттер (я работаю над его уменьшением)
  • В этом геттере тратится около 25% базового времени прогона
  • Среднее базовое время составляет 0,000175 с.

Для сравнения, у меня есть другой метод в другом классе, который использует этот геттер:

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)


person Mark    schedule 27.03.2009    source источник
comment
Звучит вводит в заблуждение - я бы попробовал второй профилировщик, просто чтобы посмотреть...   -  person matt b    schedule 27.03.2009
comment
На самом деле это не связано, но стандарт кодирования Java заключается в том, чтобы начинать имена методов с нижнего регистра и не добавлять m_ перед переменными.   -  person Steve Kuo    schedule 27.03.2009
comment
Вы также можете попробовать профилировать с помощью YourKit, что стоит денег, но есть бесплатная 14-дневная пробная версия.   -  person Steve Kuo    schedule 27.03.2009
comment
Если вы можете сделать это с помощью Netbeans, попробуйте, это бесплатно (и профилировщик хорош, как и YourKit, я использовал оба).   -  person TofuBeer    schedule 27.03.2009


Ответы (4)


Это звучит очень своеобразно. Вы не вызываете переопределяющий метод по ошибке, не так ли?

У меня возникло бы желание загрузить демонстрационную версию YourKit. Это тривиально настроить, и это должно дать представление о том, что происходит на самом деле. Если и TPTP, и YourKit согласны, то происходит что-то странное (и я знаю, что это не очень помогает!)

person Brian Agnew    schedule 27.03.2009
comment
Хороший вопрос, но нет метода переопределения. - person Mark; 27.03.2009

Если возможно, попробуйте использовать другой профилировщик (хорошо работает Netbeans). Это может быть сложно сделать в зависимости от того, как настроен ваш код.

Возможно, что, как и многие другие инструменты, другой профилировщик даст другую информацию.

Действительно ли возврат локального примитива занимает больше времени, чем возврат вычисленного значения?

Возврат переменной экземпляра занимает больше времени, чем возврат локальной переменной (зависит от виртуальной машины). Геттер, который у вас есть, прост, поэтому он должен быть встроен, поэтому он становится таким же быстрым, как доступ к общедоступной переменной экземпляра (что, опять же, медленнее, чем доступ к локальной переменной).

Но у вас нет локального значения (локальное значение в методе, а не в классе).

Что именно вы подразумеваете под «местным»?

Должен ли я смотреть на метрику, отличную от базового времени?

Я не использовал инструменты Eclipse, поэтому я не уверен, как они работают... может иметь значение, является ли это профилировщиком трассировки или выборки (два могут давать разные результаты для подобных вещей).

Вводят ли эти результаты в заблуждение, и мне нужно рассмотреть какой-либо другой инструмент профилирования?

Я бы рассмотрел другой инструмент, просто чтобы посмотреть, будет ли результат таким же.

Изменить на основе комментариев:

Если это профилировщик выборки, то, по сути, происходит то, что каждые «n-временные единицы» программа отбирается, чтобы увидеть, где находится программа. Если вы вызываете один метод чаще, чем другой, он будет отображаться как находящийся в методе, который вызывается больше (просто более вероятно, что этот метод выполняется).

Профилировщик трассировки добавляет код в вашу программу (процесс, известный как инструментирование), чтобы, по сути, регистрировать происходящее.

Профилировщики трассировки медленнее, но точнее, они также требуют, чтобы программа была изменена (процесс инструментирования), что потенциально может привести к ошибкам (не то, чтобы я слышал об этом... но я уверен, что это происходит, по крайней мере, пока они разрабатывают профайлер).

Профилировщики выборки быстрее, но менее точны (они просто угадывают, как часто выполняется строка кода).

Итак, если Eclipse использует профилировщик выборки, вы можете увидеть то, что вы считаете странным поведением. Переход на профилировщик трассировки покажет более точные результаты.

Если Eclipse использует профилировщик трассировки, то изменение профайлеров должно показать тот же результат (однако новый профилировщик может сделать более очевидным для вас, что происходит).

person TofuBeer    schedule 27.03.2009
comment
Вы правы: я написал не то. Вместо локальной переменной я должен был сказать переменную экземпляра. Часть моего замешательства связана с методом FasterMethod(). Я использую как локальные (передаваемые), так и переменные экземпляра, но это быстрее (согласно TPTP), чем использование переменной экземпляра. - person Mark; 27.03.2009
comment
Копаясь вокруг, я думаю, что Eclipse использует профилировщик выборки. Также похоже, что можно заставить его выполнять трассировку либо с помощью плагинов, либо с помощью опций (извините, я не пользователь Eclipse). - person TofuBeer; 27.03.2009

Звучит слегка обманчиво — возможно, профайлер убирает какие-то оптимизации?

Ради интереса попробуйте сделать метод final, что облегчит его встраивание. Это вполне может быть различием между свойством и FasterMethod. При реальном использовании HotSpot будет встраивать даже виртуальные методы до тех пор, пока они не будут переопределены в первый раз (IIRC).

РЕДАКТИРОВАТЬ: Отвечая на комментарий Брайана: Да, обычно это тот случай, когда создание чего-то окончательного не улучшит производительность (хотя это может быть хорошо с точки зрения дизайна :), потому что Hotspot обычно решает, будет ли он может быть встроен или нет в зависимости от того, переопределен он или нет. Я предположил, что этот профилировщик, возможно, испортил это.

РЕДАКТИРОВАТЬ: теперь мне удалось воспроизвести способ, которым HotSpot «отменяет» оптимизацию классов, которые еще не были расширены (или методы, которые не были переопределены). Это было сложнее сделать для серверной ВМ, чем для клиентской, но я это сделал :)

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; }
}
person Jon Skeet    schedule 27.03.2009
comment
Мне было бы интересно, если final имеет значение. Теоретически JVM может безопасно кэшировать это значение и не читать (медленную) основную память. - person Steve Kuo; 27.03.2009
comment
Смотрите информацию Брайана Гетца здесь re. завершение — ibm.com/developerworks/java/library/j-jtp04223. html - person Brian Agnew; 27.03.2009
comment
К сведению: я отметил метод как окончательный и повторно провел тест, но получил те же результаты. - person Mark; 27.03.2009
comment
Правильно. Ах хорошо. Я все равно редактирую свой ответ с примером переопределения, просто потому, что это интересно :) - person Jon Skeet; 27.03.2009
comment
Да, Hotspot может встраивать вещи, а затем отменять встраивание, если загружается класс, который нарушает встраивание. С++ не может (легко) встраивать виртуальные методы (есть компиляторы, которые, по-видимому, могут это сделать, но используют время выполнения). Хороший пример для будущего Java — медленные дебаты :-) - person TofuBeer; 27.03.2009
comment
@TofuBeer: Да, это одна из областей, в которой Java также может победить .NET, поскольку .NET только один раз выполняет JIT. - person Jon Skeet; 27.03.2009
comment
упс, стеллаж = отслеживание. Полезно знать о .NET. - person TofuBeer; 27.03.2009
comment
Если вы посмотрите на исходный код, HotSpot использует final для поиска того, переопределен ли метод, - он вообще не используется вне компиляции и проверки (это только дополнительная работа для валидатора). - person Tom Hawtin - tackline; 27.03.2009

Что-то, что имело большое значение для производительности такого рода методов (хотя это может быть в некоторой степени историческим), заключается в том, что размер вызывающего метода может быть проблемой. HotSpot (и серьезные конкуренты) с радостью встраивают небольшие методы (некоторые могут задохнуться от synchronized/try-finally). Однако если вызывающий метод большой, то может и не быть. Особенно это было проблемой со старыми версиями HotSpot C1/client, которые имели очень плохой алгоритм распределения регистров (теперь он имеет довольно хороший и быстрый алгоритм).

person Tom Hawtin - tackline    schedule 27.03.2009