- PVSM.RU - https://www.pvsm.ru -
По долгу службы мне периодически приходится пользоваться профайлером, так как требования к производительности серверов задокументированы и не могут опускаться ниже определенного уровня. Помимо некоторых очевидных архитектурных изменений и решений частенько находятся повторяющиеся места от модуля к модулю, от одного проекта к другому, которые создают дополнительную нагрузку на виртуальную машину, которыми и хочу поделится.
Так уж случилось, что на глаза чаще всего попадался код работы с Date потому с него и начнем:
Не один десяток раз я имел возможность наблюдать, как во время обработки одного запроса от пользователя в нескольких разных местах создается новый объект даты. Чаще всего цель одна и та же — получить текущее время. В простейшем случае это выглядит так:
public boolean isValid(Date start, Date end) {
Date now = new Date();
return start.before(now) && end.after(now);
}
Казалось бы — вполне очевидное и правильное решение. В принципе, да, за исключением двух моментов:
public boolean isValid(Date start, Date end) {
long now = System.currentTimeMillis();
return start.getTime() < now && now < end.getTIme();
}
Очень часто в веб проектах возникает задача перевести строку в дату или наоборот дату в строку. Задача довольно типичная и чаще всего выглядит так:
return new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z").parse(dateString);
Это правильное и быстрое решение, но если серверу приходится парсить строку на каждый пользовательский реквест в каждом из сотен потоков — это может ощутимо бить по производительности сервера в виду довольно тяжеловесного конструктора SimpleDateFormat, да и помимо самого форматера создается множество других объектов в том числе и не легкий Calendar (размер которого > 400 байт).
Ситуацию можно было бы легко решить, сделав SimpleDateFormat статическим полем, но он не является потокобезопасным. И в конкурентной среде легко можно словить NumberFormatException.
Вторая мысль — использовать синхронизацию. Но это таки довольно сомнительная вещь. В случае большой конкуренции между потоками, мы можем не просто не улучшить производительность но и ухудшить.
Но решения есть и их как минимум 2:
В моих проектах частенько возникает задача вернуть пользователю случайную сущность. Обычно подобного рода код выглядит так:
return items.get(new Random().nextInt(items.size()));
Отлично, просто, быстро. Но, если обращений к методу много — это означает постоянное создания новых объектов Random. Чего легко можно избежать:
private static final Random rand = new Random();
...
return items.get(rand.nextInt(items.size()));
Казалось бы, вот оно — идеальное решение, но и тут не все так просто. Не смотря на то, что Random является потокобезопасным, в многопоточной среде он может работать медленно. Но Sun Oracle об этом уже позаботились:
return items.get(ThreadLocalRandom.current().nextInt(items.size()));
Как заявлено в документации — это и есть самое оптимальное решение для нашей задачи. ThreadLocalRandom [5] гораздо эффективней Random в многопоточной среде. К сожалению, данный класс доступен только начиная с 7-й версии (после багофикса [6], привет TheShade [7]). По сути, это решение такое же, как и с SimpleDateFormat, только со своим персональным классом.
Многие разработчики избегая null значений, пишут нечто подобное:
public Item someMethod() {
Item item = new Item();
//some logic
if (something) {
fillItem(item);
}
return item;
}
В итоге, даже если something никогда не станет true, огромное количество объектов все равно будет создано (при условии что метод вызывается часто).
Если Вы пишете веб приложение, почти наверняка Вы используете регулярные выражения. Типичный код:
public Item isValid(String ip) {
Pattern pattern = Pattern.compile("xxx");
Matcher matcher = pattern.matcher(ip);
return matcher.matches();
}
Как и в первом случае, как только приехал новый IP адрес, мы должны делать валидацию. Опять на каждый вызов — паки новых объектов. В данном конкретном случае код можно немножко оптимизировать:
private final Pattern pattern = Pattern.compile("xxx");
public Item isValid(String ip) {
Matcher matcher = pattern.matcher(ip);
return matcher.matches();
}
Идеально было бы так же вынести создание матчера вне метода, но к сожалению он не является потокобезопасным и приходится его постоянно создавать. Что касается однопоточной среды, то тут решение есть:
private final Pattern pattern = Pattern.compile("xxx");
private final Matcher matcher = pattern.matcher("");
public Item isValid(String ip) {
matcher.reset(ip);
return matcher.matches();
}
Что идеально подходит для… правильно, ThreadLocal'a.
Еще одна довольно частая задача — урезание даты по часам, дням, неделям. Существует огромное множество способов это сделать, начиная от апачевских DateUtils, до собственных велосипедов:
public static Date truncateToHours(Date date) {
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
calendar.setTime(date);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
Например, совсем недавно, анализируя код map [8] фазы хадупа, наткнулся на такие 2 cтроки кода, которые потребляли 60% CPU:
key.setDeliveredDateContent(truncateToHours(byPeriodKey.getContentTimestamp()));
key.setDeliveredDateAd(truncateToHours(byPeriodKey.getAdTimestamp()));
Для меня самого это стало большой неожиданностью, но профайлер не врет. К счастью метод map оказался потокобезопасным, и создание объекта календаря удалось вынести вне метода truncateToHours(). Что увеличило скорость работы map метода в 2 раза.
Не знаю почему, но некоторые разработчики для генерации метода hashcode() и equals() используют апачевские вспомогательные классы. Вот например:
@Override
public boolean equals(Object obj) {
EqualsBuilder equalsBuilder = new EqualsBuilder();
equalsBuilder.append(id, otherKey.getId());
...
}
@Override
public int hashCode() {
HashCodeBuilder hashCodeBuilder = new HashCodeBuilder();
hashCodeBuilder.append(id);
...
}
В этом, конечно, нет ничего плохого если вы используете эти методы несколько раз за жизнь приложения. Но если они вызываются постоянно, например, для каждого ключа во время Sort фазы [8] hadoop джобы, то это вполне может повлиять на скорость выполнения.
К чему это я — нет, я ни в коем случае не призываю бежать и перелопатить код с целью сэкономить на создании пары объектов, это информация к размышлею, вполне вероятно, что кому-то это очень даже пригодится. Спасибо, что дочитали.
Автор: doom369
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/40391
Ссылки в тексте:
[1] ThreadLocal : http://docs.oracle.com/javase/6/docs/api/java/lang/ThreadLocal.html
[2] Joda: http://joda-time.sourceforge.net/
[3] DateTimeFormat: http://joda-time.sourceforge.net/apidocs/org/joda/time/format/DateTimeFormat.html
[4] тут: http://java-performance.info/joda-time-performance/
[5] ThreadLocalRandom: http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadLocalRandom.html
[6] багофикса: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7051516
[7] TheShade: http://habrahabr.ru/users/theshade/
[8] map: http://developer.yahoo.com/hadoop/tutorial/module4.html#dataflow
[9] Источник: http://habrahabr.ru/post/175049/
Нажмите здесь для печати.