Pull to refresh

Оптимизируем, оптимизируем и еще раз оптимизируем

Reading time 5 min
Views 23K
По долгу службы мне периодически приходится пользоваться профайлером, так как требования к производительности серверов задокументированы и не могут опускаться ниже определенного уровня. Помимо некоторых очевидных архитектурных изменений и решений частенько находятся повторяющиеся места от модуля к модулю, от одного проекта к другому, которые создают дополнительную нагрузку на виртуальную машину, которыми и хочу поделиться.
Так уж случилось, что на глаза чаще всего попадался код работы с Date потому с него и начнем:

Date

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

    public boolean isValid(Date start, Date end) {
        Date now = new Date();
        return start.before(now) && end.after(now); 
    }

Казалось бы — вполне очевидное и правильное решение. В принципе, да, за исключением двух моментов:
  • Использовать Date сегодня в java — уже, пожалуй, моветон, учитывая тот факт, что почти все методы в нем уже Deprecated.
  • Нету смысла создавать новый объект даты, если вполне можно обойтись примитивом long:

    public boolean isValid(Date start, Date end) {
        long now = System.currentTimeMillis();
        return start.getTime() < now && now < end.getTIme(); 
    }


SimpleDateFormat

Очень часто в веб проектах возникает задача перевести строку в дату или наоборот дату в строку. Задача довольно типичная и чаще всего выглядит так:

    return new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z").parse(dateString);

Это правильное и быстрое решение, но если серверу приходится парсить строку на каждый пользовательский реквест в каждом из сотен потоков — это может ощутимо бить по производительности сервера в виду довольно тяжеловесного конструктора SimpleDateFormat, да и помимо самого форматера создается множество других объектов в том числе и не легкий Calendar (размер которого > 400 байт).

Ситуацию можно было бы легко решить, сделав SimpleDateFormat статическим полем, но он не является потокобезопасным. И в конкурентной среде легко можно словить NumberFormatException.

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

Но решения есть и их как минимум 2:
  • Старый, добрый ThreadLocal — cоздаем SimpleDateFormat для каждого потока 1 раз и переиспользуем для каждого последующего запроса. Данный подход поможет ускорить парсинг даты в 2-4 раза за счет избежания создания объектов SimpleDateFormat на каждый запрос.
  • Joda и ее потокобезопасный аналог SimpleDateFormat — DateTimeFormat. Хоть йода в целом и медленнее дефолтного Java Date API в парсинге дат они идут наравне. Несколько тестов можно глянуть тут.


Random

В моих проектах частенько возникает задача вернуть пользователю случайную сущность. Обычно подобного рода код выглядит так:

    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 гораздо эффективней Random в многопоточной среде. К сожалению, данный класс доступен только начиная с 7-й версии (после багофикса, привет TheShade). По сути, это решение такое же, как и с SimpleDateFormat, только со своим персональным классом.

Not null

Многие разработчики избегая null значений, пишут нечто подобное:

public Item someMethod() {
    Item item = new Item();
    //some logic
    if (something) {
        fillItem(item);
    }
    return item;
}

В итоге, даже если something никогда не станет true, огромное количество объектов все равно будет создано (при условии что метод вызывается часто).

Regexp

Если Вы пишете веб приложение, почти наверняка Вы используете регулярные выражения. Типичный код:

public Item isValid(String ip) {
    Pattern pattern = Pattern.compile("xxx");
    Matcher matcher = pattern.matcher(ip);
    return matcher.matches();
}

Как и в первом случае, как только приехал новый IP адрес, мы должны делать валидацию. Опять на каждый вызов — паки новых объектов. В данном конкретном случае код можно немножко оптимизировать:

private static final Pattern pattern = Pattern.compile("xxx");
public Item isValid(String ip) {
    Matcher matcher = pattern.matcher(ip);
    return matcher.matches();
}

Идеально было бы так же вынести создание матчера вне метода, но к сожалению он не является потокобезопасным и приходится его постоянно создавать. Что касается однопоточной среды, то тут решение есть:

private static 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.

Truncate Date

Еще одна довольно частая задача — урезание даты по часам, дням, неделям. Существует огромное множество способов это сделать, начиная от апачевских 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 фазы хадупа, наткнулся на такие 2 cтроки кода, которые потребляли 60% CPU:

key.setDeliveredDateContent(truncateToHours(byPeriodKey.getContentTimestamp()));
key.setDeliveredDateAd(truncateToHours(byPeriodKey.getAdTimestamp()));

Для меня самого это стало большой неожиданностью, но профайлер не врет. К счастью метод map оказался потокобезопасным, и создание объекта календаря удалось вынести вне метода truncateToHours(). Что увеличило скорость работы map метода в 2 раза.

HashCodeBuilder

Не знаю почему, но некоторые разработчики для генерации метода 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 фазы hadoop джобы, то это вполне может повлиять на скорость выполнения.

Заключение

К чему это я — нет, я ни в коем случае не призываю бежать и перелопатить код с целью сэкономить на создании пары объектов, это информация к размышлению, вполне вероятно, что кому-то это очень даже пригодится. Спасибо, что дочитали.
Tags:
Hubs:
+26
Comments 34
Comments Comments 34

Articles