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

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

    Заключение

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

    Comments 34

    • UFO just landed and posted this here
        +2
        Когда читал про рендом, случайно наткнулся этот баг и увидел в переписке хабраюзера, был приятно удивлен. Мир тесен.
        • UFO just landed and posted this here
        +1
        Как бы я хотел, чтобы для начала этот и подобные посты оптимизировались под RSS.
          +5
          Ну хоть не про сложение String'ов, и на том спасибо :)
            +4
            Простите что беспокою вас, но у вас в первом слове в заглавии одна буква пропущена.
              +2
              Оптимизировали, не иначе как.
                0
                Спасибо, подправил.
                0
                private final Pattern pattern = Pattern.compile(«xxx»);
                — А почему его нельзя сделать статичным он вроде потокобезопасный?
                  0
                  Потерял, подфиксал
                  0
                  Про класс Date не согласен. Строгая типизация для того и есть, что бы везде было написано Date, а не long (а в комментарии приписка //Date), а уже пользуетесь стандартным библиотекой, или нестандартной, или свой пишете — ваше дело.
                    +1
                    Если честно, то я не понял — при чем тут строгая типизация?
                      0
                      Ну тут просто смотря как читать ваш текст, если про одну конкретную функцию, то это и не советы оптимизации, а так вы пишите: «нету смысла создавать новый объект даты, если вполне можно обойтись примитивом long», что можно трактовать очень широко. То есть истина где-то посредине, и стоит это наверное выяснить.
                        0
                        В предложенном методе на две строчки локальная переменная типа long лучше типа Date.
                        +1
                        Просто то, что вы написали — это очень уж частный случай. В массе своей проще везде использовать Date now — это будет более поддерживаемо и расширяемо. А если поддерживать и расширять не предполагается, то скорее всего, и оптимизация особо не нужна (экономия на спичках уж точно).
                      0
                      public boolean isValid(Date start, Date end) {
                      long now = System.currentTimeMillis();
                      return start.getTime() < now && now < end.getTIme();
                      }


                      Оптимизация одного частного случая. Написать тест на разное текущее время или сделать проверку, были ли данные валидны на некий момент, вы уже не сможете. Экономия на спичках здесь может создать нехилую такую проблему. Плюс, чисто семантически странно проверять даты на основе timestamp'а. В любом случае, в итоге всегда получается, что кроме timestamp'а нужно узнать часовой пояс, каким-либо образом отформатировать дату и т.п. И в этом случае лучше везде таскать за собой зависимость на Date now, чем усложнять код хардкодами текущего времени.

                      Это всё моё имхо, конечно.
                        0
                        Абсолютно с Вами согласен. У нас система разделена на модули. Есть ХХХ модуль (веб сервис), который является высоконагруженным. Требования к одному серверу 500 рек/сек и время ответа меньше 50мс для 99% реквестов. Когда стоят такие требования — вопрос подготовки данных для ускорения работы сервера — это вовсе не проблема.
                        0
                        key.setDeliveredDateContent(truncateToHours(byPeriodKey.getContentTimestamp()));
                        key.setDeliveredDateAd(truncateToHours(byPeriodKey.getAdTimestamp()));
                        

                        Знакомые строки :)
                          0
                          Кстати Escape Analysis сам увидит что объект Date метода не покидает:
                              public boolean isValid(Date start, Date end) {
                                  Date now = new Date();
                                  return start.before(now) && end.after(now); 
                              }

                          И сам соптимизирует к System.currentTimeMillis();

                          А код останется чуточку более читаемым.

                          Хотя я сам, конечно, new Date() уже почти никогда не пишу.
                            0
                            Насколько я знаю (поправьте если не так), чтобы JIT начал анализировать код на убегание объектов и прочие продвинутые вещи, он должен быть действительно «горячим». Не уверен, что в условиях обычного веб-сервера это часто достигается. Так можно в куче мест понадеяться на JIT, он их все по-отдельности не посчитает достаточно горячими, и в итоге замедление набежит существенное.
                              +1
                              Чтобы JIT начал такой анализ, самое главное чтобы был выставлен флаг -XX+DoEscapeAnalysis. А насколько «горячим» должен быть код для работы JIT контролируется флагом -XX:CompileThreshold (по умолчанию 10 000 вызовов метода, если мне память не изменяет, по крайней мере с «серверным» компилятором).

                              Всё это относится только к HotSpot JVM, конечно. У других JVM другие настройки.
                                +1
                                насколько я помню, Escape Analysis по дефолту включён в JDK7
                                  0
                                  Дейстивительно. Спасибо за поправку!
                            0
                            Расскажите в двух словах, как вы профилируете Hadoop-задачи.
                              0
                              1. Запускается приложение с включенным profile-агентом
                              2. Подключается профилировщик
                              3. ??????
                              4. PROFIT
                                0
                                ну я бы не был так уверен =)

                                если кратко:
                                1) имеется процесс tasktracker
                                2) он периодически ломится на удаленную машинку за заданиями
                                3) на приходящий таск подымает отдельную jvm и выполняет там задание(таск)
                                4) грохает jvm от таска

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

                                Поэтому вопрос про то, как профайлили hadoop мне тоже был бы интересен.
                                0
                                Никак. Я профайлил отдельно мап и редюс методы в юнит тестах с продакшн логами. По ним смог определить узкие места нашего кода. Что касается более сложных задач — например, распределения ключей по редюсам, то для этого использовалась мониторилка самого хадупа. По ней четко можно понять где именно проблема и туда уже копать в каждом конкретном случае.
                                +2
                                1. Проблема Date не в deprecated-методах, а в том, что он mutable и дает соблазн изменить объект, передаваемый в аргументе. Здесь, как правильно заметил автор, пригодится joda. Если посмотреть исходники конструктора Date(), то там всего лишь вызов currentTimeMillis(). Если хотите оптимизировать такой код и вызовов действительно много — вынесите за скобки вызов currentTimeMillis (он по какой-то неведомой причине не такой уж и быстрый), сравнивайте long'и.
                                2. Про Not Null — сам по себе метод весьма странный, так вообще не стоит писать. А еще есть паттерны immutable и null object (не путать с null).
                                3. В блоке про Regexp, по всей видимости, после правки стоит убрать упоминание ThreadLocal'а.
                                4. Пример про truncateToHours по неясной причине просто дату заменяют на дату UTC. А вообще это делается проще: new Date(date.getMillis() / 3600000 * 3600000) (если есть уверенность, что мы работаем в часовом поясе, кратном одному часу, что характерно для большинства задач).
                                5. Про хеш-коды тоже не согласен. Опять же есть паттерн immutable, особенно важный для объектов, которые кладутся в Set (Map), там в качестве оптимизации можно сделать кеширование hashCode, как это сделано, например, в классе String. А использование HashCodeBuilder — личный выбор разработчика, лишь бы equals->hashCode контракт был сохранен.

                                P.S. Мне не хватает кармы, но, простите, я бы за статью влепил бы минус.
                                  +2
                                  я влепил за вас :) И вот почему. Я не понимаю, какие задачи решает статья и к чему призывает читателей. «Не создавайте лишних объектов»? Это можно было одной строчкой написать.

                                  Только вот создание объекта — это очень (ОЧЕНЬ!) быстрая операция. Поэтому хоть сколько заметного прироста перфоманса от замены в произвольном месте объекта на что-то — не будет с вероятностью 99,9%.

                                  Если ускорение работы приложения от убирания операции создания объекта и возникает, то, в основном, по двум причинам: локальность данных (совершенно отдельная тема, на которую в статье никаких намёков не было) и снижение нагрузки на GC (плодим меньше мусора). Так вот, если вы уверены, что приложение тормозит из-за GC, то есть если есть реальная нагрузка на GC, то надо включать профилятор и совершенно по месту смотреть, где у вас плодятся объекты. И именно в этом месте решать проблему. А Date это будет, Format, String или что-то ещё — вообще пофиг.
                                    0
                                    надо включать профилятор и совершенно по месту смотреть
                                    Не, ну человек пишет же, что "периодически приходится пользоваться профайлером", наверняка не просто так. А раз уж и написать об этом решил, то видимо и профит заметный был.

                                    Хотя, лично у меня, если честно, первой реакцией на статью было съязвить что-нибудь. Но, подумал и передумал. Хотя интересного в статье мало, а большая часть материала — просто здравый смысл и будни программиста, но все же это напоминание о том, что не следует забывать про оптимизацию, и самый простой и будничный кусок кода может с аппетитом откушать процессора. Лишь бы оптимизация не была преждевременной. А то я вот, например, не так давно увидел в старом, но добротном коде веб-сервиса блок synchronized и решил заменить его на что-нибудь более продвинутое из того, что появилось в Java 5/6. НО! Перед тем как сделать это, сделал замеры производительности. Оказалось, что работа в этом блоке длится 0.001% (одна тысячная процента) от общего времени вызова метода веб-сервиса. «Не все то золото, что блестит» — подумал я, и оставил все как есть.
                                  +1
                                  Про SimpleDateFormat и согласен, и несогласен одновременно.
                                  Действительно, это тяжелый метод, который на практике в нашем случае сжирал существенную долю CPU высоконагруженного веб-сервера.
                                  Но я не соглашусь с тем, что виной тому создание нового SimpleDateFormat каждый раз. Медленным здесь является сам процесс конвертации в строку заданного формата. Я эту проблему решил, написав свой конвертер HttpDate, заточенный под конкретный формат, фигурирующий в HTTP протоколе.
                                    0
                                    Кстати по поводу hash и equals — eclipse умеет генерировать очень симпотичные перекрытия
                                      0
                                      Not null:
                                      Здесь тоже можно использовать static :)
                                      Постоянные проверки на null сильно засоряют код.

                                      private static final Item ItemObj=new Item(); // =Item.getEmptyStaticObject();
                                      
                                      public Item someMethod() {
                                          //some logic
                                          if (something) {
                                              Item item = new Item();
                                              fillItem(item);
                                              return item;
                                          }else{
                                             return ItemObj;
                                          }
                                      }
                                      
                                        0
                                        Будто вы сейчас сделали его лучше…

                                      Only users with full accounts can post comments. Log in, please.