Главная страница
Навигация по странице:

  • Оптимизацию следует выполнять по мере написания

  • Быстродействие программы не менее важно, чем ее

  • Когда выполнять оптимизацию

  • ЧАСТЬ V

  • Время выполне- Время выполне- ния кода, оптими- ния кода без зированного Экономия Соотношение Язык оптимизации компилятором

  • ГЛАВА 25

  • Частые причины снижения эффективности Операции ввода/вывода

  • Время обработки Время обработки Экономия Соотношение Язык внешнего файла данных «в памяти» времени быстродействия

  • Время обработки Время обработки Экономия Язык локального файла файла по сети времени

  • Пример цикла инициализации, вызывающего много страничных ошибок (Java)

  • Пример цикла инициализации, вызывающего немного страничных ошибок (Java)

  • Табл. 25-1. Относительное быстродействие кода, написанного на разных языках Время выполнения кода Язык Тип языка в сравнении с кодом C++

  • Относительное быстродействие распространенных операций

  • Табл. 25-2. Быстрота выполнения часто используемых операций Относительное время выполнения Операция Пример C++ Java

  • Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по


    Скачать 5.88 Mb.
    НазваниеРуководство по стилю программирования и конструированию по
    АнкорСовершенный код
    Дата31.03.2023
    Размер5.88 Mb.
    Формат файлаpdf
    Имя файлаСовершенный код. Мастер-класс. Стив Макконнелл.pdf
    ТипРуководство
    #1028502
    страница73 из 106
    1   ...   69   70   71   72   73   74   75   76   ...   106
    ГЛАВА 25 Стратегии оптимизации кода
    579
    Разумеется, это не значит, что увеличение числа строк высокоуровневого кода всегда приводит к повышению быстродействия или сокращению объема програм#
    мы. Это означает, что независимо от эстетической привлекательности компакт#
    ного кода ничего определенного о связи между числом строк кода на высокоуров#
    невом языке и объемом и быстродействием итоговой программы сказать нельзя.
    Одни операции, вероятно, выполняются быстрее или компактнее других
    — НЕВЕРНО! Если речь идет о производительности, не может быть никаких «ве#
    роятно». Без измерения производительности вы никак не сможете точно узнать,
    помогли ваши изменения программе или навредили. Правила игры изменяются при каждом изменении языка, компилятора, версии компилятора, библиотек, вер#
    сий библиотек, процессора, объема памяти, цвета рубашки, которую вы надели
    (ладно, это шутка), и т. д. Результаты, полученные на одном компьютере с одним набором инструментов, вполне могут оказаться противоположными на другом ком#
    пьютере с другим набором инструментов.
    Исходя из этого, можно назвать несколько причин, по которым производитель#
    ность не следует повышать путем оптимизации кода. Если программа должна быть портируемой, помните: методики, повышающие производительность в одной среде,
    могут снижать ее в других. Если вы решите изменить или модернизировать ком#
    пилятор, возможно, новый компилятор будет автоматически выполнять те виды оптимизации, что вы выполнили вручную, и все ваши усилия окажутся бесполез#
    ными. Хуже того: оптимизировав код, вы можете помешать компилятору выпол#
    нить более эффективные виды оптимизации, ориентированные на простой код.
    Оптимизируя код, вы обрекаете себя на перепрофилирование каждого оптими#
    зированного фрагмента при каждом изменении марки компилятора, его версии,
    версий библиотек и т. д. Если вы не будете перепрофилировать код, оптимизация,
    бывшая выгодной, после изменения среды сборки программы вполне может стать невыгодной.
    Оптимизацию следует выполнять по мере написания
    кода — НЕВЕРНО! Кое#кто утверждает, что если вы буде#
    те стремиться написать самый быстрый и компактный код при работе над каждым методом, то итоговая программа бу#
    дет быстрой и компактной. Однако на самом деле это ме#
    шает увидеть за деревьями лес, и программисты, чрезмер#
    но поглощенные микрооптимизацией, начинают упускать из виду по#настоящему важные глобальные виды оптимизации. Основные недо#
    статки этого подхода рассмотрены ниже.
    
    До создания полностью работоспособной программы найти узкие места в коде почти невозможно. Программисты очень плохо угадывают, на какие 4% кода приходятся 50% времени выполнения, поэтому, оптимизируя код по мере его написания, они будут тратить примерно 96% времени на оптимизацию кода,
    который не нуждается в оптимизации. На оптимизацию по#настоящему важ#
    ных 4% кода времени у них уже не останется.
    
    В тех редких случаях, когда узкие места определяются правильно, разработ#
    чики уделяют им слишком большое внимание, и критически важными стано#
    вятся уже другие узкие места. Результат очевиден: все то же снижение произ#
    Возможности небольшого повы- шения эффективности следует игнорировать, скажем, в 97%
    случаев: необдуманная оптими- зация — корень всего зла.
    Дональд Кнут (Donald Knuth)

    580
    ЧАСТЬ V Усовершенствование кода водительности. Если оптимизация выполняется после создания полной систе#
    мы, разработчики могут определить все проблемные области и их относитель#
    ную важность, что способствует эффективному распределению времени.
    
    Концентрация на оптимизации во время первоначальной разработки отвле#
    кает от достижения других целей. Разработчики погружаются в анализ алго#
    ритмов и сокровенные дискуссии, от которых пользователям ни тепло, ни хо#
    лодно. Корректность, сокрытие информации, удобочитаемость и т. д. стано#
    вятся вторичными целями, хотя потом улучшить их сложнее, чем производи#
    тельность. Работа над повышением производительности после создания пол#
    ной программы обычно затрагивает менее 5% кода. Что легче: повысить про#
    изводительность 5% кода или улучшить удобочитаемость всего кода?
    Короче говоря, главный недостаток преждевременной оптимизации — отсутствие перспективы. Это сказывается на быстродействии итогового кода, других, еще более важных атрибутах производительности и качестве программы, ну а расплачиваться за это в итоге приходится пользователям. Если время, сэкономленное благодаря реализации наиболее простой программы, посвятить ее последующей оптимиза#
    ции, итоговая программа непременно будет работать быстрее, чем программа,
    разработанная с использованием неорганизованного подхода к оптимизации
    (Stevens, 1981).
    Иногда оптимизация программы после ее написания не позволяет достичь нуж#
    ных показателей производительности, из#за чего приходится вносить крупные изменения в завершенный код. Можете утешить себя тем, что в этих случаях оп#
    тимизация небольших фрагментов все равно не привела бы к нужным результа#
    там. Проблема в таких ситуациях объясняется не низким качеством кода, а неадек#
    ватной архитектурой программы.
    Если оптимизацию нужно выполнять до создания полной программы, сведите риск к минимуму, интегрировав в процесс оптимизации перспективу. Один из спосо#
    бов сделать это — задать целевые показатели объема и быстродействия отдель#
    ных функций и провести оптимизацию кода по мере его написания, направлен#
    ную на достижение этих показателей. Определив такие цели в спецификации, вы сможете следить сразу и за лесом, и за конкретными деревьями.
    Быстродействие программы не менее важно, чем ее
    корректность — НЕВЕРНО! Едва ли можно представить ситуацию, когда программу прежде всего нужно сделать быстрой или компактной и только потом корректной. Дже#
    ральд Вайнберг рассказывает историю о программисте, ко#
    торого вызвали в Детройт, чтобы он помог отладить нера#
    ботоспособную программу. Через несколько дней разработчики пришли к выво#
    ду, что ситуация безнадежна.
    На пути домой он обдумывал проблему и внезапно понял ее суть. К концу полета у него уже был набросок нового кода. В течение нескольких дней программист тестировал код и уже собирался вернуться в Детройт, но тут получил телеграмму,
    в которой утверждалось, что работа над проектом прекращена из#за невозмож#
    ности написания программы. И все же он снова прилетел в Детройт и убедил руководителей в том, что проект можно было довести до конца.
    Дополнительные сведения Опи- сания других занимательных и поучительных случаев можно найти в книге Джеральда Вай- нберга «Psychology of Computer
    Programming» (Weinberg, 1998).

    ГЛАВА 25 Стратегии оптимизации кода
    581
    Далее он должен был убедить в этом участников проекта. Они выслушали его, и когда он закончил, создатель старой системы спросил:
    — И как быстро выполняется ваша программа?
    — Ну, в среднем она обрабатывает каждый набор введенных данных примерно за
    10 секунд.
    — Ага! Но моей программе для этого требуется только 1 секунда.
    Ветеран откинулся назад, удовлетворенный тем, что он приструнил выскочку.
    Другие программисты, похоже, согласились с ним, но новичок не смутился.
    — Да, но ваша программа
    не работает. Если бы моя не обязана была работать, я мог бы сделать так, чтобы она обрабатывала ввод почти мгновенно.
    В некоторых проектах быстродействие или компактность кода действительно имеет большое значение. Однако таких проектов немного — гораздо меньше, чем ка#
    жется большинству людей, — и их число постоянно сокращается. В этих проек#
    тах проблемы с производительностью нужно решать путем предварительного проектирования. В остальных случаях ранняя оптимизация представляет серьез#
    ную угрозу для общего качества ПО,
    включая производительность.
    Когда выполнять оптимизацию?
    Создайте высококачественный проект. Следите за правиль#
    ностью программы. Сделайте ее модульной и изменяемой,
    чтобы позднее над ней было легко работать. Написав кор#
    ректную программу, оцените ее производительность. Если программа громоздка, сделайте ее быстрой и компактной.
    Не оптимизируйте ее, пока не убедитесь, что это на самом деле нужно.
    Несколько лет назад я работал над программой на C++, ко#
    торая должна была генерировать графики, помогающие ана#
    лизировать данные об инвестициях. Написав код расчета первого графика, мы провели тестирование, показавшее, что программа отображает график пример#
    но за 45 минут, что, конечно, было неприемлемо. Чтобы решить, что с этим де#
    лать, мы провели собрание группы. На собрании один из разработчиков выкрик#
    нул в сердцах: «Если мы хотим иметь хоть какой#то шанс выпустить приемлемый продукт, мы должны начать переписывать весь код на ассемблере
    прямо сейчас».
    Я ответил, что мне так не кажется — что около 50% времени выполнения скорее всего приходятся на 4% кода. Было бы лучше исправить эти 4% в конце работы над проектом. После еще некоторых споров наш руководитель поручил мне по#
    работать над производительностью программы (что мне и было нужно: «О, нет!
    Только не бросай меня в тот терновый куст!»
    1
    ).
    Как часто бывает, я очень быстро нашел в коде пару ослепительных узких мест.
    Внеся несколько изменений, я снизил время рисования с 45 минут до менее чем
    30 секунд. Гораздо меньше 1% кода соответствовало 90% времени выполнения.
    Правила оптимизации Джексо- на: Правило 1. Не делайте это- го. Правило 2 (только для экс- пертов). Не делайте этого пока
    — до тех пор, пока вы не по- лучите совершенно ясное не- оптимизированное решение.
    М. А. Джексон
    (M. A. Jackson)
    1
    Часто цитируемая фраза из негритянской сказки про Братца Кролика и Братца Волка в изло#
    жении У. Фолкнера. —
    Прим. перев.

    582
    ЧАСТЬ V Усовершенствование кода
    Ну, а к моменту выпуска ПО нам удалось сократить время рисования почти до 1
    секунды.
    Оптимизация кода компилятором
    Современные компиляторы могут оптимизировать код куда эффективнее, чем вам кажется. В случае, который я описал выше, мой компилятор выполнил оптимиза#
    цию вложенного цикла так эффективно, что я едва ли получил бы лучшие резуль#
    таты, переписав код. Покупая компилятор, сравните производительность каждо#
    го компилятора с использованием своей программы. Каждый компилятор имеет свои плюсы и минусы, и одни компиляторы лучше подходят для вашей програм#
    мы, чем другие.
    Оптимизирующие компиляторы лучше оптимизируют простой код. Если вы жон#
    глируете индексами циклов и делаете другие «хитрые» вещи, компилятору будет труднее выполнить свою работу, от чего пострадает ваша программа. В подразде#
    ле «Размещение одного оператора на строке» раздела 31.5 вы найдете пример про#
    стого кода, который после оптимизации компилятором оказался на 11% быстрее,
    чем аналогичный «хитрый» код.
    Хороший оптимизирующий компилятор может повысить быстродействие кода на
    40 и более процентов, тогда как многие из методик, описанных в следующей гла#
    ве, — только на 15–30%. Так почему ж просто не написать ясный код и не позво#
    лить компилятору выполнить свою работу? Вот результаты нескольких тестов,
    показывающие, насколько успешно компиляторы оптимизировали метод встав#
    ки#сортировки:
    Время выполне-
    Время выполне- ния кода, оптими-
    ния кода без
    зированного
    Экономия Соотношение
    Язык
    оптимизации
    компилятором
    времени
    быстродействия
    Компилятор C++ 1 2,21 1,05 52%
    2:1
    Компилятор C++ 2 2,78 1,15 59%
    2,5:1
    Компилятор C++ 3 2,43 1,25 49%
    2:1
    Компилятор C#
    1,55 1,55 0%
    1:1
    Visual Basic
    1,78 1,78 0%
    1:1
    Java VM 1 2,77 2,77 0%
    1:1
    Java VM 2 1,39 1,38
    <1%
    1:1
    Java VM 3 2,63 2,63 0%
    1:1
    Единственное различие между версиями метода заключалось в том, что при пер#
    вой компиляции оптимизация была отключена, а при второй включена. Очевид#
    но, что одни компиляторы выполняют оптимизацию лучше, чем другие, а неко#
    торые изначально генерируют максимально эффективный код без его оптимиза#
    ции. Некоторые виртуальные машины Java (Java Virtual Machine, JVM) также бо#
    лее эффективны, чем другие. Эффективность вашего компилятора или вашей JVM
    может быть другой; оцените ее сами.

    ГЛАВА 25 Стратегии оптимизации кода
    583
    25.3. Где искать жир и патоку?
    При оптимизации кода вы находите части программы, медленные, как патока зи#
    мой, и огромные, как Годзилла, и изменяете их так, чтобы они были быстры, как молния, и могли скрываться в расщелинах между байтами в оперативной памяти.
    Без профилирования программы вы никогда не сможете с уверенностью сказать,
    какие фрагменты медленны и огромны, но некоторые операции давно славятся ленью и ожирением, так что вы можете начать исследование именно с них.
    Частые причины снижения эффективности
    Операции ввода/вывода Один из самых главных источников неэффективно#
    сти — ненужные операции ввода/вывода. Если объем используемой памяти не иг#
    рает особой роли, работайте с данными в памяти, а не обращайтесь к диску, БД
    или сетевому ресурсу.
    Вот результаты сравнения эффективности случайного доступа к элементам 100#
    элементного массива «в памяти» и записям аналогичного файла, хранящегося на диске:
    Время обработки
    Время обработки
    Экономия
    Соотношение
    Язык
    внешнего файла
    данных «в памяти»
    времени
    быстродействия
    C++
    6,04 0,000 100%

    C#
    12,8 0,010 100%
    1000:1
    Судя по этим результатам, доступ к данным «в памяти» выполняется в 1000 раз быстрее, чем доступ к данным, хранящимся во внешнем файле. В случае моего компилятора C++ время доступа к данным «в памяти» не удалось даже измерить.
    Результаты аналогичного тестирования последовательного доступа к данным по#
    хожи:
    Время обработки
    Время обработки
    Экономия
    Соотношение
    Язык
    внешнего файла
    данных «в памяти»
    времени
    быстродействия
    C++
    3,29 0,021 99%
    150:1
    C#
    2,60 0,030 99%
    85:1
    Примечание: при тестировании последовательного доступа данные были в 13 раз бо#
    лее объемными, чем при тестировании случайного доступа, поэтому результаты двух видов тестов сравнивать нельзя.
    Если для доступа к внешним данным используется более медленная среда (напри#
    мер, сетевое соединение), разница только увеличивается. При тестировании слу#
    чайного доступа к данным по сети результаты выглядят так:
    Время обработки
    Время обработки
    Экономия
    Язык
    локального файла
    файла по сети
    времени
    C++
    6,04 6,64
    #10%
    C#
    12,8 14,1
    #10%

    584
    ЧАСТЬ V Усовершенствование кода
    Конечно, эти результаты сильно зависят от скорости сети, объема трафика, рас#
    стояния между компьютером и сетевым диском, производительности сетевого и локального дисков, фазы Луны и других факторов.
    В целом доступ к данным «в памяти» выполняется гораздо быстрее, так что дваж#
    ды подумайте, прежде чем включать операции ввода/вывода в фрагменты, к быс#
    тродействию которых предъявляются повышенные требования.
    Замещение страниц Операция, заставляющая ОС заменять страницы памяти,
    выполняется гораздо медленнее, чем операция, ограниченная одной страницей памяти. Иногда самое простое изменение может принести огромную пользу. На#
    пример, один программист обнаружил, что в системе, использующей страницы объемом по 4 кб, следующий цикл инициализации вызывает массу страничных ошибок:
    Пример цикла инициализации, вызывающего много страничных ошибок (Java)
    for ( column = 0; column < MAX_COLUMNS; column++ ) {
    for ( row = 0; row < MAX_ROWS; row++ ) {
    table[ row ][ column ] = BlankTableElement();
    }
    }
    Это хорошо отформатированный цикл с удачными именами переменных, так в чем же проблема? Проблема в том, что каждая строка (row) массива
    table содер#
    жит около 4000 байт. Если массив включает слишком много строк, то при каж#
    дом обращении к новой строке ОС должна будет заменить страницы памяти.
    В предыдущем фрагменте изменение номера строки, а значит, и подкачка новой страницы с диска выполняются при каждой итерации внутреннего цикла.
    Программист реорганизовал цикл:
    Пример цикла инициализации, вызывающего немного страничных ошибок (Java)
    for ( row = 0; row < MAX_ROWS; row++ ) {
    for ( column = 0; column < MAX_COLUMNS; column++ ) {
    table[ row ][ column ] = BlankTableElement();
    }
    }
    Этот код также вызывает страничную ошибку при каждом изменении номера стро#
    ки, но это происходит только
    MAX_ROWS раз, а не MAX_ROWS * MAX_COLUMNS раз.
    Степень снижения быстродействия кода из#за замещения страниц во многом за#
    висит от объема памяти. На компьютере с небольшим объемом памяти второй фрагмент кода выполнялся примерно в 1000 раз быстрее, чем первый. При нали#
    чии большего объема памяти различие было всего лишь двукратным и было за#
    метно лишь при очень больших значениях
    MAX_ROWS и MAX_COLUMNS.
    Системные вызовы Вызовы системных методов часто дороги. Они нередко вклю#
    чают переключение контекста — сохранение состояния программы, восстановле#
    ние состояния ядра ОС и наоборот. В число системных методов входят методы,
    служащие для работы с диском, клавиатурой, монитором, принтером и другими

    ГЛАВА 25 Стратегии оптимизации кода
    585
    устройствами, методы управления памятью и некоторые вспомогательные методы.
    Если вас беспокоит производительность, узнайте, насколько дороги системные вызовы в вашей системе. Если они дороги, рассмотрите следующие варианты.
    
    Напишите собственные методы. Иногда функциональность системных мето#
    дов оказывается избыточной для решения конкретных задач. Заменив низко#
    уровневые системные методы собственными, вы получите более быстрый и компактный код, лучше соответствующий вашим потребностям.
    
    Избегайте вызовов системных методов.
    
    Обратитесь к производителю системы и укажите ему на низкую эффективность тех или иных методов. Обычно производители хотят улучшить свою продук#
    цию и охотно принимают все замечания (поначалу они могут показаться не#
    много недовольными, но они на самом деле в этом заинтересованы).
    В программе, про оптимизацию которой я рассказал в подразделе «Когда выпол#
    нять оптимизацию?» раздела 25.2, использовался класс
    AppTime, производный от коммерческого класса
    BaseTime (имена изменены). Объекты AppTime использо#
    вались в программе на каждом шагу и исчислялись десятками тысяч. Через несколь#
    ко месяцев мы обнаружили, что объекты
    BaseTime инициализировались в кон#
    структоре значением системного времени. В нашей программе системное время не играло никакой роли, а это означало, что мы без надобности генерировали ты#
    сячи системных вызовов. Простое переопределение конструктора класса
    BaseTime
    так, чтобы поле
    time инициализировалось нулем, дало нам такое же повышение производительности, что и все остальные изменения, вместе взятые.
    Интерпретируемые языки При выполнении интерпретируемого кода каждая команда должна быть обработана и преобразована в машинный код, поэтому интерпретируемые языки обычно гораздо медленней компилируемых. Вот при#
    мерные результаты сравнения разных языков, полученные мной при работе над этой главой и главой 26 (табл. 25#1):
    Табл. 25-1. Относительное быстродействие кода,
    написанного на разных языках
    Время выполнения кода
    Язык
    Тип языка
    в сравнении с кодом C++
    C++
    Компилируемый
    1:1
    Visual Basic
    Компилируемый
    1:1
    C#
    Компилируемый
    1:1
    Java
    Байт#код
    1,5:1
    PHP
    Интерпретируемый
    >100:1
    Python
    Интерпретируемый
    >100:1
    Как видите, в плане быстродействия языки C++, Visual Basic и C# примерно одина#
    ковы. Код на Java выполняется несколько медленнее. PHP и Python — интерпрети#
    руемые языки, и код, написанный на них, обычно выполняется в 100 и более раз медленнее, чем написанный на C++, Visual Basic, C# или Java. Однако к общим ре#
    зультатам, указанным в этой таблице, следует относиться с осторожностью. Отно#
    сительная эффективность C++, Visual Basic, C#, Java и других языков во многом за#
    висит от конкретного кода (читая главу 26, вы сами в этом убедитесь).

    586
    ЧАСТЬ V Усовершенствование кода
    Ошибки Наконец, еще одним источником проблем с производительностью яв#
    ляются некоторые виды ошибок. Какие? Вы можете оставить в итоговой версии про#
    граммы отладочный код (например, записывающий трассировочную информацию в файл), забыть про освобождение памяти, неграмотно спроектировать таблицы БД,
    опрашивать несуществующие устройства до истечения лимита времени и т. д.
    При работе над первой версией одного приложения мы столкнулись с операци#
    ей, выполнявшейся гораздо медленнее других похожих операций. Сделав массу попыток объяснить этот факт, мы выпустили версию 1.0, так и не поняв полнос#
    тью, в чем дело. Однако, работая над версией 1.1, я обнаружил, что таблица БД,
    используемая в этой операции, не была проиндексирована! Простая индексация таблицы повысила скорость некоторых операций в 30 раз. Определение индекса для часто используемой таблицы нельзя считать оптимизацией — это просто хорошая практика программирования.
    Относительное быстродействие
    распространенных операций
    Хотя нельзя с полной уверенностью утверждать, что одни операции медленнее других, не оценив их, определенные операции все же обычно дороже. Отыскивая патоку в своей программе, используйте табл. 25#2, которая поможет вам выдвинуть первоначальные предположения о том, какие фрагменты кода неэффективны.
    Табл. 25-2. Быстрота выполнения часто используемых операций
    Относительное время выполнения
    Операция
    Пример
    C++
    Java
    Исходный показатель
    i = j
    1 1
    (целочисленное присваивание)
    Вызовы методов
    Вызов метода без параметров
    foo()
    1

    Вызов закрытого метода
    this.foo()
    1 0,5
    без параметров
    Вызов закрытого метода
    this.foo( i )
    1,5 0,5
    с одним параметром
    Вызов закрытого метода
    this.foo( i, j )
    2 0,5
    с двумя параметрами
    Вызов метода объекта
    bar.foo()
    2 1
    Вызов метода производ#
    derivedBar.foo()
    2 1
    ного объекта
    Вызов полиморфного метода
    abstractBar.foo()
    2,5 2
    Обращения к объектам
    Обращение к объекту
    i = obj.num
    1 1
    1#го уровня
    Обращение к объекту
    i = obj1.obj2. num
    1 1
    2#го уровня
    Стоимость каждого
    i = obj1.obj2.obj3...
    неизмеряема неизмеряема дополнительного уровня

    1   ...   69   70   71   72   73   74   75   76   ...   106


    написать администратору сайта