elizarov


Блог Романа Елизарова


Previous Entry Share Next Entry
Escape Analysis и шаблон Defensive Copy в современных JVM
elizarov

В современных версиях JVM (начиная с Oracle Java 7) присутствует весьма продвинутый Escape Analysis. Это весьма мощный и революционный инструмент, который может в разы увеличить скорость работы кода без каких либо изменений в нем, а так же позволяет использовать некоторые специфические шаблоны Java кода без серьёзных последствий для производительности.

Escape Analysis включается автоматически при использовании Server VM (когда JVM запускается с ключиком "-server"), который при компиляции байт-кода Java в нативный код агрессивно делает inlining большого количества методов. Во время компиляции производится анализ выделяемых объектов. При условии, что выделенный объект не "убегает" из компилируемого куска кода, его выделение в куче может быть полностью устранено из кода, а его поля будут просто храниться на стеке и/или в регистрах.

Рассмотрим один из часто используемых в Java коде шаблонов под названием "защитное копирование" (defensive copy), подробно описанной в ставшей уже классической книге Effective Java. Я продемонстрирую его на тривиальном примере. Пусть у нас есть какие-нибудь изменяемые объекты, например экземпляры тривиального класса ModifiableInt, которые имеют одно свойство value типа int:

public class ModifiableInt { 
    private int value; 
 
    public ModifiableInt(int value) { 
        this.value = value; 
    } 
 
    public ModifiableInt(ModifiableInt other) { 
        this.value = other.value; 
    } 
 
    public int getValue() { 
        return value; 
    }
    // и т.д.

Если экземпляры этого класса используются в некоем другом классе, например, ModifiableIntContainer и они доступны через некие публичные getXXX и setXXX методы, то возникает проблема, описанная в вышеупомянутой книге. Клиентский код может получить указатель на экземпляр изменяемого класса ModifiableInt, который храниться внутри объекта ModifiableIntContainer и изменять его без ведома класса-контейнера, тем самым создавая проблемы с пониманием происходящего в коде, а так же с его безопасностью. В таком случае хорошим тоном считается создавать так называемую защитную копию объекта при его сохранении внутри контейнера и возврате его экземпляра оттуда.

Однако, использование защитной копии означает, что на куче будет каждый раз создаваться новый экземпляр объекта ModifiableContainer при каждом доступе к нему:

public class ModifiableIntContainer { 
    private ModifiableInt c; 
 
    public ModifiableIntContainer(ModifiableInt c) { 
        this.c = new ModifiableInt(c); 
    } 
 
    public ModifiableInt getC() { 
        return new ModifiableInt(c); 
    } 
    // и т.д.

Запустим простую тестовую программу ModifiableIntSum, которая проходится по списку объектов-контейнеров и суммирует хранящиеся там значения:

    private int runIteration() { 
        int sum = 0; 
        for (ModifiableIntContainer container : list) 
            sum += container.getC().getValue(); 
        return sum; 
    } 

При запуске этой программы на Java 1.7.0_09 под Client VM (build 23.5-b02), которая используется по умолчанию на клиентских машинах Windows, можно убедиться с помощью ключика "-verbose:gc" в том, что происходит активная сборка мусора, которая отнимает существенно время работы программы. А при запуске с ключом "-server", который включает Server VM (build 23.5-b02) или на машинах серверного класса, где он используется по умолчанию, сборки мусора не происходит из-за того, что код был оптимизирован и постоянное выделение памяти в нем устранено. При этом, общая скорость работы этого кода возрастает более чем в 4 раза. Однако надо заметить, что, в данном случае, такой большой выигрыш в производительности получается от всего комплекса оптимизаций, которые делает Server VM. Если в ней отключить анализ убегающих объектов ключом "-XX:-DoEscapeAnalysis", то производительность все равно будет почти в 2 раза выше, чем при использовании Client VM за счет других оптимизаций.

В целом можно сказать, что такая оптимизация кода на уровне JVM позволяет без потерь производительности использовать многие шаблоны кода, при использовании которых раньше всегда приходилось оглядываться на потенциальные проблемы производительности и избегать их в местах, где производительность является узким местом. В дополнения к шаблону защитного копирования можно также вспомнить и о возвращении из метода нескольких результатов, для чего метод возвращает выделенный на куче объект, инкапсулирующий всё множество возвращаемых значений. Естественно, оптимизация кода это всегда прерогатива в первую очередь программиста, который выявляет и оптимизирует узкие места кода, но помощь от инструментов в этом нелегком деле всегда приходится очень и очень кстати.


  • 1
Видимо, и в 6-й Java есть с какого-то апдейта. У меня на Java 6 и 7 работает одинаково (~300ms per pass) с ключом -server.

Все верно. Есть.

Не имеет ли смысл, в частности, по этой причине использовать -server на олимпиадах?

При ограничении в 2 сек на тест не всегда получается выигрыш. То есть выигрыш по wall clock time почти всегда есть, а по общеиу потребленному времени часто бывает хуже, ибо компиляция происходит в фоновых потоках и достаточно много cpu времени на это жрет.

При агрессивном использовании функциональных концепций скорее всего будет заметно.

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

Ну вот я постоянно использую самодельные функциональные примитивы, такие как мап фильтр и т.д. плюс иммутабельность данных где возможно. В итоге при обработке данных создается куча объектов с практически нулевым временем жизни. Код надежен и лаконичен, но с точки зрения эффективности есть явные излишества.

В guava есть эффективные map, filter и пр

Естественно. Массовое повышение уровня абстракции кода становится возможным когда появляются соответсвующие технологие компиляции, способные нивелировать эффект потери производительности. Такая же история была и с переходом на языки высокого уровня с ассемблера, и повсеместная победа структурного программирования.

непонятно в чем был смысл невключения этой фичи в -client. Я имею ввиду применение этого "мега паттерна" в awt/swing (getSize(), getFont(), getColor()) которые тучями гадят при каждой отрисовке...

Просто client очень примитивен и в нем эту фичу и нельзя включить. Запускайся под server и будет тебе счастье. Насколько я понимаю, есть план вообще уйти от деления на server/client и всегда делать tiered compilation.

у меня ява на десктопе только в виде идеи и райфайзен коннекта, так что я не преживаю :))

Купил новый ноут 64бит, скачал java 64bit win - там банально нет client ;-)
Только server, только hardcore ;-)

Всё верно. Под 64bit только server. Если у вас Mac, то других опций вообще по сути нет. Если же у вас Windows, то можно спокойно пользоваться 32bit версией (которая пока еще бывает и client и server) в тех случаях, когда вам важно быстрое время старта для коротко-живущих приложений или утилит (в этом случае client обгоняет server по общему потребленному времени работы).

Да. Но не всегда. Если внутри ModifiableInt сделать массив, например boolean[] myarr (и использовать его как-нибудь, чтоб не заоптимайзился), то все станет гораздо хуже, несмотря на то, что он тоже не покидает областей видимости. Просто, для массивов данная фича не работает :((.

Правда тестил давно на 1.7_07, надо будет снова на новой потестить.

  • 1
?

Log in

No account? Create an account