Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 24 Рефакторинг 571 Дополнительные ресурсы Процесс рефакторинга имеет много общего с процессом устранения дефектов (см. раздел 23.3). Факторы риска, свя# занные с рефакторингом, похожи на факторы риска, каса# ющиеся оптимизации кода. Об управлении факторами риска при оптимизации кода см. раздел 25.6. Fowler, Martin. Refactoring: Improving the Design of Existing Code. Reading, MA: Add# ison Wesley, 1999. Это самое полное и подробное руководство по рефакторингу содержит детальное обсуждение многих конкретных видов рефакторинга, упомя# нутых в этой главе, а также других видов, которых я не касался. Фаулер привел много примеров пошагового выполнения каждого вида рефакторинга. Ключевые моменты Изменения программы неизбежны как во время первоначальной разработки, так и после выпуска первой версии. Изменения могут приводить как к улучшению, так и к ухудшению ПО. Главное Правило Эволюции ПО заключается в том, что при эволюции кода внутрен# нее качество программы должно повышаться. Одним из условий успешности рефакторинга является пристальное внимание к многочисленным предупреждающим знакам — «запахам», указывающим на необходимость рефакторинга. Другое условие — изучение многих конкретных видов рефакторинга. Заключительным условием успешности рефакторинга является следование стратегии безопасного рефакторинга. Одни подходы к рефакторингу лучше, а другие хуже. Рефакторинг во время разработки — самая благоприятная возможность улуч# шения программы и внесения в нее всех изменений, которые вам так или иначе захочется внести позднее. Используйте эту возможность! http://cc2e.com/2464 572 ЧАСТЬ V Усовершенствование кода Г Л А В А 2 5 Стратегии оптимизации кода Содержание 25.1. Общее обсуждение производительности ПО 25.2. Введение в оптимизацию кода 25.3. Где искать жир и патоку? 25.4. Оценка производительности 25.5. Итерация 25.6. Подход к оптимизации кода: резюме Связанные темы Методики оптимизации кода: глава 26 Архитектура ПО: раздел 3.5 В этой главе обсуждается исторически противоречивая проблема — повышение производительности кода. В 1960#х годах ресурсы компьютеров были крайне ограниченны, поэтому эффективность их использования была вопросом перво# степенной важности. По мере роста производительности компьютеров в 70#х программисты начали понимать, насколько упор на производительность вредит удобочитаемости и легкости сопровождения кода, и оптимизация кода отошла на задний план. Вместе с микрокомпьютерной революцией, свидетелями которой мы стали в 80#х, проблема эффективного использования ресурсов вернулась, но в 90#х ее важность постепенно уменьшилась. В 2000#х мы опять столкнулись с этой про# блемой, только теперь она связана с ограниченной памятью мобильных телефо# нов, карманных ПК и подобных устройств, а также со временем выполнения ин# терпретируемого кода. Проблемы с производительностью можно решать на двух уровнях: стратегическом и тактическом. В этой главе рассматриваются стратегические вопросы произво# дительности: мы выясним, что такое производительность, насколько она важна, и обсудим общий подход к ее повышению. Если стратегии достижения нужной производительности вам уже хорошо известны и вы хотите узнать конкретные http://cc2e.com/2578 ГЛАВА 25 Стратегии оптимизации кода 573 методики ее повышения на уровне кода, можете перейти к главе 26. Однако прежде чем приступать к серьезной работе над повышением производительности, хотя бы просмотрите эту главу, чтобы не потратить время на оптимизацию кода тог# да, когда следовало бы заняться чем#то другим. 25.1. Общее обсуждение производительности ПО Оптимизация кода — лишь один из способов повышения производительности программы. Часто можно найти другие способы, обеспечивающие большее повы# шение производительности за меньшее время и с меньшим вредом для кода. Характеристики качества и производительность Некоторые люди смотрят на мир через розовые очки. Про# граммисты склонны воспринимать мир через кодовые очки. Мы полагаем, что чем лучше будет наш код, тем сильнее наше ПО понравится клиентам. Эта точка зрения верна лишь отчасти. Пользователей боль# ше интересуют явные характеристики программы, а не ка# чество кода. Производительность обычно привлекает их внимание, только когда она сказывается на их работе. Боль# шее значение для пользователей имеет не грубая производительность приложе# ния, а объем информации, который оно позволяет обработать за конкретный срок, а также такие факторы, как своевременное получение программы, ясный пользо# вательский интерфейс и предотвращение простоев. Приведу пример. Я делаю цифровой камерой минимум 50 снимков в неделю. Чтобы скопировать снимки на компьютер при помощи ПО, поставляемого с камерой, я должен выбрать каждый снимок по очереди, причем в окне программы отобра# жаются только 6 снимков сразу. В результате копирование 50 изображений пре# вращается в долгий и нудный процесс, требующий десятков щелчков и массы переключений между окнами. Устав от всего этого, я купил карту памяти, подклю# чаемую прямо к компьютеру и воспринимаемую им как диск. Теперь для копиро# вания изображений на диск компьютера я могу использовать Проводник Windows. Я делаю пару щелчков, нажимаю Ctrl+A и перетаскиваю все файлы в нужное мес# то. Меня не волнует, передает ли карта памяти каждый файл вдвое медленнее или быстрее, чем другое ПО, потому что сейчас я могу обработать больший объем информации за меньшее время. Каким бы быстрым или медленным ни был код драйвера карты памяти, его производительность выше. Производительность только косвенно связана с быстродействием кода. Чем больше вы работаете над скоростью кода, тем меньше внимания уделяете другим характеристикам его качества. Не приносите их в жер# тву быстродействию. Стремление к повышению быстродействия может снизить общую производительность программы, а не повысить ее. Во имя эффективности — при- чем достигается она далеко не всегда — совершается больше компьютерных грехов, чем по любой другой причине, включая банальную глупость. У. А. Вульф (W. A. Wulf) 574 ЧАСТЬ V Усовершенствование кода Производительность и оптимизация кода Решив, что эффективность кода — будь то его быстродействие или объем — дол# жна быть приоритетом, не торопитесь улучшать быстродействие или объем на уровне кода, а рассмотрите несколько вариантов. Подумайте об эффективности в контексте: требований к программе; проекта программы; проектов классов и методов; взаимодействия программы с ОС; компиляции кода; оборудования; оптимизации кода. Требования к программе Высокая производительность считается требованием гораздо чаще, чем на самом деле им является. Барри Бом рассказывает, что при разработке одной системы в компании TRW сначала решили, что время реакции системы не должно превы# шать 1 секунды. Это требование привело к разработке очень сложного проекта, на реализацию которого пришлось бы потратить примерно 100 млн долларов. Даль# нейший анализ показал, что в 90% случаев пользователей устроит время реакции, равное 4 секундам. Изменение требования позволило снизить общую стоимость системы примерно на 70 млн долларов (Boehm, 2000b). Прежде чем тратить время на решение проблемы с производительностью, убеди# тесь, что она действительно требует решения. Проект программы Проект программы определяет ее основные черты — глав# ным образом, способ ее разделения на классы. Проект мо# жет как облегчить, так и затруднить создание высокопро# изводительной системы. Например, при высокоуровневом проектировании одной реальной программы сбора и обработки данных в качестве ключевого атрибута была определена пропускная способность обработки резуль# татов измерений. Каждое измерение включало определение значения электриче# ской величины, калибровку значения, масштабирование значения и преобразо# вание исходных единиц измерения (таких как милливольты) в единицы прикладной области (такие как градусы Цельсия). Если бы при высокоуровневом проектировании программисты не оценили все факторы риска, им в итоге пришлось бы оптимизировать алгоритмы вычисления многочленов 13 степени, т. е. многочленов, содержащих 14 переменных с макси# мальной степенью 13. Вместо этого они решили проблему, выбрав другое обору# дование и разработав высокоуровневый проект, предусматривающий использо# вание десятков многочленов 3 степени. Оптимизировав код, они вряд ли получи# ли бы нужные результаты. Это пример проблемы, которую нужно было решать на уровне проектирования. Перекрестная ссылка Проекти- рование высокопроизводитель- ных программ рассматривается в работах, указанных в разде- ле «Дополнительные ресурсы» в конце главы. ГЛАВА 25 Стратегии оптимизации кода 575 Если объем и быстродействие программы действительно важны, разработайте архитектуру так, чтобы она позволя# ла добиться намеченных показателей, а затем задайте для отдельных подсистем, функций и классов целевые показа# тели использования ресурсов. Преимущества такого подхода таковы. Задание отдельных целевых показателей использования ресурсов делает про# изводительность системы предсказуемой. Если каждая функция соответствует целевым показателям, вся система будет обладать нужной производительнос# тью. Вы можете уже на ранних этапах определить проблемные подсистемы и перепроектировать их или оптимизировать их код. Простое задание явных целей повышает вероятность их достижения. Програм# мисты стремятся к достижению целей, если знают, каковы они; чем определен# нее цели, тем легче к ним стремиться. Вы можете поставить цели, которые непосредственно не направлены на повышение эффективности, но способствуют этому в долгосрочной перспективе. Эффективность часто лучше всего рассматривать в контексте других аспектов. Так, простота внесения изменений может создать лучшие условия для достижения высокой эффективности, чем явное определение эф# фективности как одной из целей. Если проект отличается высокой степенью модульности и простотой изменения, менее эффективные компоненты мож# но с легкостью заменить на более эффективные. Проекты классов и методов Проектирование классов и методов предоставляет еще одну возможность повышения производительности ПО. Одним из способов повышения производительности на этом уровне является выбор типа данных или алгоритма, от чего обычно зависит и объем используемой памяти, и быстрота выполне# ния кода. Именно на этом уровне вы можете выбрать быст# рую сортировку вместо пузырьковой или двоичный поиск вместо линейного. Взаимодействие программы с ОС Если ваша программа работает с внешними файлами, дина# мической памятью или устройствами вывода, она скорее всего взаимодействует с ОС. Низкая производительность в этом случае может объясняться большим объемом или мед# ленным выполнением методов ОС. Вы можете даже не знать, что программа взаимодействует с ОС: иногда вызовы сис# темных методов генерируются компилятором или содержатся в коде библиотек. Компиляция кода Хорошие компиляторы преобразуют ясный высокоуровневый код в оптимизиро# ванный машинный код. Иногда правильный выбор компилятора позволяет вооб# ще забыть о дальнейшей оптимизации кода. Перекрестная ссылка О том, как программисты стремятся к до- стижению поставленных целей, см. подраздел «Задание целей» раздела 20.2. Перекрестная ссылка О типах данных и алгоритмах см. кни- ги, указанные в разделе «Допол- нительные ресурсы» в конце главы. Перекрестная ссылка О страте- гиях борьбы со слишком мед- ленными или объемными мето- дами ОС на уровне кода см. главу 26. 576 ЧАСТЬ V Усовершенствование кода В главе 26 вы найдете многочисленные примеры ситуаций, когда код, сгенерирован# ный компилятором, оказывается эффективнее кода, оптимизированного вручную. Оборудование Иногда самым выгодным и эффективным способом повышения производитель# ности программы является покупка нового оборудования. Конечно, если программа предназначен для сотнен или тысяч компьютеров по всей стране, этот вариант нереалистичен. Но если вы разрабатываете специализированное ПО для нескольких пользователей, обновление оборудования на самом деле может оказаться самым дешевым вариантом. Это позволит сократить расходы, связанные с улучшением производительности ПО и проблемами, которые могут из#за этого улучшения воз# никнуть при сопровождении. Кроме того, это повысит производительность всех программ, выполняемых на новых системах. Оптимизация кода Оптимизацией кода (code tuning), которой посвящена оставшаяся часть этой главы, называют изменение корректного кода, направленное на повышение его эффек# тивности. «Оптимизация» подразумевает внесение небольших изменений, затра# гивающих один класс, один метод, а чаще всего — несколько строк кода. Крупно# масштабные изменения проекта или другие высокоуровневые способы повыше# ния производительности оптимизацией не считаются. Каждый из уровней от проектирования системы до оптимизации кода допускает существенное повышение производительности ПО. Джон Бентли утверждает, что в некоторых случаях формула общего повышения производительности системы имеет мультипликативный характер (Bentley, 1982). Так как каждый из шести уровней допускает десятикратный рост производительности, значит, что теоретически про# изводительность программы может быть повышена в миллион раз. Хотя для этого необходимо, чтобы результаты, получаемые на каждом из уровней, были незави# симы от других уровней (что наблюдается редко), этот потенциал вдохновляет. 25.2. Введение в оптимизацию кода Чем соблазняет оптимизация кода? Это не самый эффективный способ повыше# ния производительности: улучшение архитектуры программы, перепроектирование классов и выбор более эффективного алгоритма обычно приводят к более впе# чатляющим результатам. Кроме того, это не самый легкий способ повысить про# изводительность: легче купить новое оборудование или компилятор с улучшен# ным модулем оптимизации. Наконец, это не самый дешевый способ повысить производительность: на оптимизацию кода вручную изначально уходит много вре# мени, а потом оптимизированный код труднее сопровождать. Оптимизация кода привлекательна по ряду причин. Прежде всего она бросает вызов законам природы. Нет ничего радостнее, чем ускорить выполнение метода в 10 раз путем изменения всего лишь нескольких его строк. Кроме того, овладение мастерством написания эффективного кода — признак превращения в серьезного программиста. В теннисе вы не получаете очков за то, как берете мяч, но все же должны освоить правильный способ делать это. Вы не ГЛАВА 25 Стратегии оптимизации кода 577 можете просто наклониться и взять его рукой. Если вы не хотите показаться но# вичком, вы ударяете по нему ракеткой, пока он не подпрыгнет до пояса, и тогда вы его ловите. Более трех ударов мяча о землю — серьезная оплошность. Несмот# ря на кажущуюся незначительность, способ подбора мяча считается в культуре теннисистов отличительным признаком. Компактность вашего кода также обыч# но не волнует никого, кроме вас и других программистов. Тем не менее в про# граммисткой культуре способность создавать компактный и эффективный код слу# жит подтверждением вашего класса. Увы, эффективный код не всегда является «лучшим». Этот вопрос мы и обсудим ниже. Принцип Парето Принцип Парето, известный также как «правило 80/20», гласит, что 80% резуль# тата можно получить, приложив 20% усилий. Относящийся не только к програм# мированию, этот принцип очень точно характеризует оптимизацию программ. Барри Бом сообщает, что на 20% методов программы приходятся 80% времени ее выполнения (Boehm, 1987b). В классической работе «An Empi# rical Study of Fortran Programs» Дональд Кнут указал, что менее 4% кода обыч# но соответствуют более чем 50% времени выполнения программы (Knuth, 1971). Кнут обнаружил это неожиданное отношение при помощи инструмента профи# лирования, поддерживающего подсчет строк. Следствие очевидно: вам нужно найти в коде «горячие точки» и сосредоточиться на оптимизации процентов, использу# емых более всего. Профилируя свою программу подсчета строк, Кнут обнаружил, что половину времени она проводила в двух циклах. Он изменил несколько строк кода и удвоил скорость профайлера менее чем за час. Джон Бентли описывает случай, когда программа из 1000 строк проводила 80% времени в 5#строчном методе вычисления квадратного корня. Утроив быстродей# ствие этого метода, он удвоил быстродействие программы (Bentley, 1988). Опи# раясь на принцип Парето, можно дать еще один совет: напишите большую часть кода на интерпретируемом языке (скажем, на Python), а потом перепишите про# блемные фрагменты на более быстром компилируемом языке, таком как C. Бентли также сообщает о случае, когда группа обнаружила, что ОС половину вре# мени проводит в одном небольшом цикле. Переписав цикл на микрокоде, разра# ботчики ускорили его выполнение в 10 раз, но производительность системы ос# талась прежней — они переписали цикл бездействия системы! Разработчики языка ALGOL — прародителя большинства современных языков, сыгравшего одну из самых главных ролей в истории программирования, — руко# водствовались принципом «Лучшее — враг хорошего». Стремление к совершен# ству может мешать завершению работы. Доведите работу до конца и только по# том совершенствуйтесь. Часть, которую нужно довести до совершенства, обычно невелика. 578 ЧАСТЬ V Усовершенствование кода Бабушкины сказки С оптимизацией кода связано множество заблуждений. Сокращение числа строк высокоуровневого кода повышает быстродей' ствие или уменьшает объем итогового машинного кода — НЕВЕРНО! Многие убеждены в том, что, если сократить какой#то фрагмент до одной или двух строк, он будет максимально эффективным. Рассмотрим код инициализации мас# сива из 10 элементов: for i = 1 to 10 a[ i ] = i end for Как вы думаете, он выполнится быстрее или медленнее, чем эти 10 строк, решаю# щих ту же задачу? a[ 1 ] = 1 a[ 2 ] = 2 a[ 3 ] = 3 a[ 4 ] = 4 a[ 5 ] = 5 a[ 6 ] = 6 a[ 7 ] = 7 a[ 8 ] = 8 a[ 9 ] = 9 a[ 10 ] = 10 Если вы придерживаетесь старой догмы «меньшее число строк выполняется быс# трее», вы скажете, что первый фрагмент быстрее. Однако тесты на Microsoft Visual Basic и Java показали, что второй фрагмент минимум на 60% быстрее первого. Время Время выполнения выполнения последовательного Экономия Соотношение Язык цикла for кода времени быстродействия Visual Basic 8,47 3,16 63% 2,5:1 Java 12,6 3,23 74% 4:1 Примечания: (1) Временные показатели в этой и следующих таблицах данной главы ука# зываются в секундах, а их сравнение имеет смысл только в пределах конкретных строк каждой из таблиц. Действительные показатели будут зависеть от компилятора, парамет# ров компилятора и среды, в которой выполняется тестирование. (2) Большинство резуль# татов сравнительного тестирования основано на выполнении фрагментов кода от несколь# ких тысяч до многих миллионов раз, что призвано устранить колебания результатов. (3) Конкретные марки и версии компиляторов не указываются. Показатели производитель# ности во многом зависят от марки и версии компилятора. (4) Сравнение результатов те# стирования фрагментов, написанных на разных языках, имеет смысл не всегда, так как компиляторы разных языков не всегда позволяют задать одинаковые параметры генери# рования кода. (5) Фрагменты, написанные на интерпретируемых языках (PHP и Python), в большинстве случаев тестировались с использованием более чем в 100 раз меньшего числа тестов, чем фрагменты, написанные на других языках. (6) Некоторые из показателей «эко# номии времени» не совсем точны из#за округления «времени выполнения кода до опти# мизации» и «времени выполнения оптимизированного кода». |