Перейти к тексту

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

Профиль создается, когда вы в первый раз заходите в developerWorks. Выберите данные в своем профиле (имя, страна/регион, компания) которые будут общедоступными и будут отображаться, когда вы публикуете какую-либо информацию. Вы можете изменить данные вашего ИБМ аккаунта в любое время.

Вся введенная информация защищена.

  • Закрыть [x]

При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

Вся введенная информация защищена.

  • Закрыть [x]

Теория и практика Java: Городские легенды о производительности

Аллигаторы в мусоросборнике и другие мифы программистов

Брайан Гетц, главный консультант, Quiotix
Брайан Гетц (Brian Goetz) - консультант по ПО и последние 15 лет работал профессиональными разработчиком ПО. Сейчас он является главным консультантом в фирме Quiotix, занимающейся разработкой ПО и консалтингом и находящейся в Лос-Альтос, Калифорния. Следите за публикациями Брайана в популярных промышленных изданиях. Вы можете связаться с Брайаном по адресу brian@quiotix.com

Описание:  Городские легенды - те же вирусы, но они поражают мозг; даже если мы наверняка знаем, что все это неправда, мы не можем удержаться от того, чтобы их пересказать (и тем самым заразить другие доверчивые "хосты"), потому что эти байки так и просятся на язык. Большинство городских легенд имеет под собой основание, что еще больше препятствует их искоренению. К сожалению, многие ссылки и рекомендации, касающиеся настроек производительности Java, во многом похожи на городские легенды - кто-то где-то дает какую-то подсказку, которая имеет (или имела) под собой некое основание, но за время ее нескончаемого пересказа давно потеряла всякий смысл, который в ней возможно и был. В этой статье Брайан Гетц разбирает некоторые из этих городских легенд о производительности и очищает их от заблуждений.

Больше статей из этой серии

Дата:  08.02.2007
Уровень сложности:  простой
Активность:  4989 просмотров
Комментарии:  


Вы когда-нибудь слышали о престарелой даме, пытавшейся высушить свою вымокшую под дождем собаку в микроволновке (ложь)? Или о командире авианосца, который требовал, чтобы маяк уступил ему дорогу (ложь)? Или о том, что аллигаторы поселились в канализации Нью-Йорка (ложь)? Или что почтовый грузовик имеет на дороге преимущество перед местными спецмашинами - включая полицию, пожарных и скорую помощь - благодаря федеральному статусу (также неправда)? Теперь признайтесь - сколько раз вы выслушивали и пересказывали другим такие истории, даже если и были убеждены в обратном или весьма скептически относились к их достоверности?

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

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

Городской миф о производительности № 1: Синхронизация происходит очень медленно

Верно или неверно утверждение, что синхронные методы в пятьдесят раз медленнее, чем эквивалентные несинхронные методы? Этот небольшой перл появился в весьма приличной книге, которую написал Дов Балка (Dov Bulka) по настройкам производительности низкого уровня, и повторялся во многих других источниках. Как и все городские легенды, это и на самом деле имеет под собой некое основание. Будем справедливыми к Балке: когда-то в далеком прошлом, возможно, во времена JDK 1.0, это так и было, и если вы запускаете микротест в Листинге 1, то видно, что testSync занял в 50 раз больше времени, чем testUnsync.

Даже если это и было когда-то правдой, то теперь это совершенно не так. Виртуальные машины Java сделали огромный шаг вперед со времен JDK 1.0. Теперь синхронизация реализуется более эффективно, и JVM иногда может определить, что синхронизация на самом деле не защищает данные и может быть отключена. Но еще важнее то, что микротест в Листинге 1 в основе своей ошибочен. Прежде всего, микротесты редко измеряют то, что они должны измерять по вашему мнению. При динамической компиляции у вас нет ни малейшего понятия о том, когда или какой байт-код ваша JVM собирается конвертировать во внутренний (собственный) код, поэтому какой толк сравнивать яблоки с яблоками?

Кроме того, у вас нет представления о том, какой компилятор или JVM сейчас отключает при оптимизации - некоторые Java-компиляторы полностью отключат запросы к unsyncMethod, потому что он ничего не делает, а другие могут также оптимизировать с отключением syncMethod или синхронизацию syncMethod, потому что она также ничего не делает. Что отключает ваш компилятор, и при каких условиях? Этого вы не знаете, но это почти наверняка искажает измерения.

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

Но уж если вы начали сравнивать время запуска реальных синхронизированных методов с несинхронизированными в современных JVM, то вы обнаружите, что издержки и близко не стоят к нашумевшей цифре "50 раз", о которой все говорят. Прочитайте Часть 1 нашей серии, Осторожность при работе с потоками, "Синхронизация вам не враг" (Threading lightly, "Synchronization is not the enemy", см. Ресурсы), для ознакомления с некоторыми приблизительными и не совсем научными способами замера издержек синхронизации. Наверняка существуют некоторые издержки несостязательной (uncontended) синхронизации (они гораздо больше при состязательной (contended) синхронизации), но синхронизация - не аллигатор, живущий в канализации и съедающий производительность, чего многие так боятся.


Листинг 1. Дефектный микротест для измерения издержек синхронизации
                
    public static final int N_ITERATIONS = 10000000;

    public static synchronized void syncMethod() {
    }

    public static void unsyncMethod() {
    }

    public static void testSync() {
        for (int i=0; i<N_ITERATIONS; i++)
            syncMethod();
    }

    public static void testUnsync() {
        for (int i=0; i<N_ITERATIONS; i++)
            unsyncMethod();
    }

    public static void main(String[] args) {
        long tStart, tElapsed;

        tStart = System.currentTimeMillis();
        testSync();
        tElapsed = System.currentTimeMillis() - tStart;
        System.out.println("Synchronized took " + tElapsed + " ms");

        tStart = System.currentTimeMillis();
        testUnsync();
        tElapsed = System.currentTimeMillis() - tStart;
        System.out.println("Unsynchronized took " + tElapsed + " ms");
    }

Миф о том, что "синхронизация - это медленно", весьма опасен, потому что это заставляет программистов ставить под угрозу безопасность потоков своих программ во избежание субъективно воспринимаемых рисков для производительности. На самом деле они зачастую думают, что поступают очень разумно, делая это. Это всего лишь ложный страх, заставляющий разработчиков и авторов внедрять кажущуюся на первый взгляд умной, но чрезвычайно опасную "блокировку двойного контроля", которая на первый взгляд исключает синхронизацию из стандартного пути кода, но которая на самом деле угрожает потокобезопасности вашего кода. Проблемы с безопасностью использования потоков являются бомбами замедленного действия в вашем коде, лишь ждущими своего часа, и когда они пойдут в ход, то случится это в самое неподходящее время - когда ваша программа будет сильно загружена. Забота о производительности - недостаточный аргумент для того, чтобы подвергать риску безопасность использования потоков; а байка о производительности - еще менее уважительная причина для этого.


Городской миф о производительности № 2: Декларирование классов и методов как final заставляет их работать быстрее

Я рассматривал этот миф в октябрьской статье (см. Ресурсы), поэтому не буду вдаваться в подробности. Во многих статьях рекомендуется делать классы или методы final, потому что это облегчает компилятору их подключение и, следовательно, должно привести к лучшей производительности. Это весьма занятная теория. Но, к сожалению, неверная.

Этот миф даже интересней, чем миф о синхронизации, потому что он не подтвержден никакими данными - он только кажется правдоподобным (миф о синхронизации хотя бы подтверждался дефектным микротестом). Вероятно, кто-то решил, что это должно работать именно таким образом, уверенно написал об этом, и как только начало было положено, эта история распространилась повсюду.

Опасность данного мифа, так же как и мифа о синхронизации, - в том, что он заставляет разработчиков жертвовать хорошими объектно-ориентированными принципами разработки ради несуществующих преимуществ в производительности. Решение о том, делать ли класс final или нет, является проектным решением и должно мотивироваться анализом того, что класс выполняет, как и кем он будет использоваться, и можете ли вы предвидеть то, как этот класс может быть расширен. Если класс декларируют как final потому, что он неизменяемый, то это веская причина; создание комплексного класса как final потому, что он не предназначался для расширения, - тоже хороший довод. Создание класса как final только потому, что вы где-то прочитали о том, что он будет работать быстрее (даже если бы это и было правдой), веским доводом не является.


Городской миф о производительности № 3: Неизменяемые объекты плохо влияют на производительность

Весьма обычно использовать неизменяемые объекты (такие как String) для описания изменяющихся данных - когда данные изменяются, вы создаете новый объект вместо видоизменения старого. Рост или снижение производительности при использовании изменяемых и неизменяемых объектов - довольно запутанный вопрос. В зависимости от того, как объект используется в вашей программе, вы можете обнаружить, что неизменяемые объекты действительно создают некоторые преимущества для производительности (потому что вам не надо копировать их для защиты). С другой стороны, вы можете также обнаружить, что они привносят значительные потери производительности (потому что вы моделируете часто изменяемые данные и, соответственно, создаете новые объекты). Но, возможно, вы опять не сможете измерить разницу.

Миф о том, что "неизменяемые объекты - это медленно", коренится в более общем принципе производительности: создание множества временных объектов плохо сказывается на производительности. Хотя создание временных объектов, конечно, влечет за собой дополнительную работу для распределителя ресурсов (allocator) и мусоросборщика, но сейчас JVM значительно продвинулись в уменьшении влияния на производительность при создании временных объектов. Влияние создания объектов на производительность, хотя и существует на самом деле, не является настолько значительным в большинстве программ, как это было раньше, или как об этом принято думать.

Рассмотрите разницу между неизменяемым классом StringHolder и изменяемым, как показано в Листинге 2. В одном случае, если бы вы хотели изменить содержащуюся здесь строку, вы бы создали новый экземпляр класса StringHolder; в другом случае, вы бы вызвали метод настройки для существующего класса StringHolder для задания свойств содержащейся в нем строки. Возьмем конкретный пример, предположим, вы упаковали строку в ограничитель (delimiter). Как эти два подхода отразятся на производительности?


Листинг 2. Сравнение изменяемого и неизменяемого класса StringHolder
                
	// mutable 
	stringHolder.setString("/" + stringHolder.getString() + "/"); 

	// immutable
	stringHolder = new StringHolder("/" + stringHolder.getString() + "/"); 

Если вы считали, что создание дополнительного объекта StringHolder сильно повлияет на производительность сети, то вы ошибались. В случае с изменяемым объектом также создаётся много объектов. Для выполнения конкатенации строки создается объект StringBuffer, который влечет за собой создание массива char, а затем создается объект String для описания окончательного массива символов. Если полученная строка окажется длиннее, чем размер буфера по умолчанию, используемого StringBuffer, то внутренний массив символов будет перераспределен, что приведет к созданию одного или нескольких дополнительных объектов. Таким образом, метод изменяемых объектов влечет за собой создание как минимум трех объектов, тогда как при использовании неизменяемого объекта требуется ещё один. Существует некая потенциальная разница в производительности, но она не такая серьезная как разница между ситуацией, когда вам вовсе не надо создавать объекты и случаем, когда вы вынуждены создавать их многократно.

Здесь также имеется значительная, но трудно измеримая зависимость от мусоросборщика, который тоже может повлиять на производительность. Современная сборка мусора разных поколений функционирует гораздо лучше, когда новые объекты ссылаются на старые, а не иначе. Создание нового неизменяемого объекта-контейнера даёт точно такую конфигурацию; изменение существующего контейнерного объекта для ссылки на вновь созданную строку делает обратное.

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


Извлеченные уроки

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

Оптимизируйте, имея представление о конкретных целях производительности

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

Срок действия рекомендаций по производительности невелик

Производительность любой указанной технологии не является ее неотъемлемой частью - на нее воздействует среда, в которой она работает, а среды выполнения программ постоянно меняются. Компиляторы становятся умнее; процессоры - быстрее; библиотеки обновляются; мусоросборка и алгоритмы диспетчеризации изменяются; средняя скорость и издержки процессоров, кэша, оперативной памяти и I/O устройств меняются с течением времени. Если технология A была в десять раз быстрее, чем технология B десять лет назад, то не слишком рассчитывайте на то, что это соотношение будет точно таким же и сегодня. Просто срок годности данных о производительности короток. В случае получения рекомендаций по производительности, прежде чем им последовать, подумайте о том, что они, возможно, устарели.

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

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


Ресурсы

Об авторе

Брайан Гетц (Brian Goetz) - консультант по ПО и последние 15 лет работал профессиональными разработчиком ПО. Сейчас он является главным консультантом в фирме Quiotix, занимающейся разработкой ПО и консалтингом и находящейся в Лос-Альтос, Калифорния. Следите за публикациями Брайана в популярных промышленных изданиях. Вы можете связаться с Брайаном по адресу brian@quiotix.com

Помощь по сообщениям о нарушениях

Сообщение о нарушениях

Спасибо. Эта запись была помечена для модератора.


Помощь по сообщениям о нарушениях

Сообщение о нарушениях

Сообщение о нарушении не было отправлено. Попробуйте, пожалуйста, позже.


developerWorks: вход


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


При первом входе в developerWorks для Вас будет создан профиль. Выберите информацию отображаемую в Вашем профиле — скрыть или отобразить поля можно в любой момент.

Выберите ваше отображаемое имя

При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

(Должно содержать от 3 до 31 символа.)


Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Оценить эту статью

Комментарии

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=194621
ArticleTitle=Теория и практика Java: Городские легенды о производительности
publish-date=02082007
author1-email=brian@quiotix.com
author1-email-cc=