Может ли JVM пропустить создание объектов с коротким сроком службы, чтобы я мог выполнять рефакторинг без ущерба для производительности?
Иногда в ходе алгоритма нам нужно вычислить или просто сохранить несколько значений, которые зависят друг от друга или не имеют никакого смысла отдельно друг от друга.
Так же, как (довольно бессмысленный, но важный, простой) пример позволяет найти два различных значения в int[]
которые ближе всего к числу 3
:
int a = values[0];
int b = values[1];
for (int value : values) {
int distance = Math.abs(value-3);
if (value != b) {
if (distance < Math.abs(a-3) ) {
a = value;
}
}
if (value != a) {
if (distance < Math.abs(b-3) ) {
b = value;
}
}
}
takeSausageSliceBetween(a, b);
Мы не можем просто делать методы computeA()
а также computeB()
в том же классе, потому что вычисления для a
а также b
взаимозависимы. Итак, вычисления a
а также b
просто просит о рефакторинге в отдельный класс:
class DistinctNumbersNearestTo3 {
final int a;
final int b;
DistinctNumbersNearestTo3(int[] values) {
// same algorithm
}
}
поэтому код более высокого уровня становится красивым и понятным:
DistinctNumbersNearestTo3 nearest = new DistinctNumbersNearestTo3(values);
takeSausageSliceBetween(nearest.a, nearest.b);
// nearest never leaves the method it was declared in
Однако (пожалуйста, исправьте меня, если я ошибаюсь), он вводит, по крайней мере, для некоторых уровней оптимизации, создание экземпляров, а затем сборщик мусора (только из eden, но в любом случае) нового объекта только для того, чтобы разобраться с вашим код. А также поиск int в стеке превращается в поиск объекта в куче.
Вопрос здесь такой: достаточно ли умен JVM, чтобы в конечном итоге оптимизировать переработанный код, чтобы он работал так же, как и непрореагировавший код?
Другой случай, когда я хотел бы объединить несколько переменных в новый объект только для более чистого кода, это когда я хочу реорганизовать длинный метод a()
в несколько новых методов, вводя новый класс C
который содержит данные, специфичные для a()
Вызов и, следовательно, содержит несколько новых методов для работы с этими данными. Я наткнулся на такой случай, когда реализую класс, представляющий набор цепочек, к которым мы можем добавить новые ссылки (пары узлов), расширяя существующую цепочку, объединяя две цепочки в одну или создавая новую цепочку из двух узлов. Давайте обсудим этот конкретный класс только для некоторой перспективы:
public class SetOfChains {
List<List<Node>> chains;
public void addLink(Node a, Node b) {
// Very long method that mutates field this.chains. Needs refactoring.
}
}
Правильно разделить addLink
на несколько методов SetOfChains
каждый из этих новых методов должен иметь много параметров, потому что есть данные, специфичные для addLink()
вызов, который необходим на каждом этапе addLink()
, Сохранение этих данных в специальные поля SetOfChains
было бы также возможно, но вонючий и невыразительный (потому что опять же это имеет смысл только во время вызова метода), поэтому, очевидно, я создаю новый внутренний класс для хранения этих данных в своих полях и выполняю все шаги алгоритма, изменяя внешнее поле chains
как нерефакторированный addLink()
делает:
class SetOfChains{
List<List<Node>> chains;
void addLink(Node a, Node b) {
new LinkAddition(a,b).compute();
}
class LinkAddiditon { // Instances never leave addLink()'s scope
private final Something bunch, of, common, fields;
compute() {
this.properly();
this.isolated();
this.steps();
this.of();
this.the();
this.refactored();
this.method();
}
// There be code for the properly isolated steps...
}
Но так как по своей функциональности это равносильно тому, что мой старый давно не подвергался ремонту addLink()
Может ли JVM сделать полученные инструкции такими же оптимальными, как в нерефакторном случае, как если бы их не было LinkAddition
объекты?
Итак, подведем итоги под этой стеной текста: если я стремлюсь к простому для понимания коду, извлекая данные и методы в новые классы, а не в методы, обязательно ли это приводит к снижению производительности?
И правильно ли я понимаю, что описанные случаи - это как раз то, что представляет собой анализ побега?
1 ответ
Говорят, что "преждевременная оптимизация - корень всего зла".
В целом, лучше сделать код более понятным и сделать его более сложным и трудным для понимания после обнаружения узкого места в производительности.
Большая часть кода Java создает тонны недолговечных объектов, которые быстро становятся недоступными. По этой причине я считаю, что сборщик мусора в HotSpot - это "поколение", то есть он разделяет кучу на две отдельные области: молодое поколение и старое поколение.
Молодое поколение - содержит большинство вновь созданных объектов и, как ожидается, будет недолгим. Сбор мусора на этой территории очень эффективен
Старое поколение - долгоживущие объекты в конечном итоге "продвигаются" из молодого поколения в старое поколение. Сбор мусора на этой территории обходится дороже.
Поэтому я не думаю, что ваш страх создания слишком большого количества недолговечных объектов является проблемой, поскольку сборщик мусора разработан специально для поддержки этого варианта использования.
Кроме того, HotSpot проще анализировать и потенциально встроить / развернуть / оптимизировать, что также позволяет создавать множество простых объектов и методов.
Итак, подведем итоги, сделайте проще и проще понять и поддерживать код, который создает множество недолговечных объектов. Вы не должны видеть большую часть снижения производительности. Профиль, чтобы увидеть фактическую производительность. Если производительность вам не нравится, вы также можете реорганизовать узкие места, чтобы сделать их быстрее.