Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
kernigan_paik.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.91 Mб
Скачать

7.3. Стратегии ускорения

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

Улучшайте алгоритм и структуру данных. Наиболее важным фактором в уменьшении времени работы программы является выбор алгоритма или структуры данных — между эффективным алгоритмом и неэффек­тивным разница может быть просто огромной. Для нашего спам-фильт-ра мы изменили структуру данных и получили выигрыш в десять раз; еще более внушительным улучшением могло стать применение нового алгоритма, который бы сократил вычисления на порядок, скажем с О(n2) до O(n log n). Мы уже обсуждали эту тему в главе 2, так что теперь не бу­дем к ней возвращаться.

Убедитесь в том, что сложность такая, как надо; если она завышена, то именно в ней может крыться источник недостаточной производи­тельности. Этот, линейный на первый взгляд, алгоритм для сканирова­ния строки

? for (i = 0; i < strlen(s); i++)

? if (s[i] == c)

? ……

на самом деле вовсе не линейный, а квадратичный: если s содержит п символов, то цикл выполняется п раз и при каждом исполнении вызов strlen обходит п символов строки.

Используйте оптимизацию компилятора. Одно улучшение, часто приво­дящее к достаточно неплохим результатам, вы можете сделать совершен­но "бесплатно" — включить всю возможную оптимизацию компилятора. Современные компиляторы справляются с оптимизацией достаточно хорошо, так что необходимости во внесении мелких улучшений вруч­ную, как правило, не возникает.

По умолчанию большинство компиляторов С и C++ не используют все свои возможности по оптимизации, но у них есть опции, позволяю­щие включить оптимизатор (optimizer). Он не применяется по умолча­нию из-за того, что оптимизация обычно конфликтует с отладчиками исходного кода, поэтому включать оптимизатор явным образом можно только после того, как вы будете абсолютно уверены в том, что програм­ма как следует отлажена.

Оптимизация компилятора обычно увеличивает производительность программы в диапазоне от нескольких процентов до двух раз. Иногда, од­нако, она может даже замедлить программу, так что не забудьте произвес­ти новые замеры времени. Мы сравнили результаты неоптимизирован­ной и оптимизированной компиляции пары версий спам-фильтра. В тестовых примерах для окончательной версии алгоритма сравнения из­начальное время исполнения равнялось 8.1 секунды и упало до 5.9 секун­ды после включения оптимизации, так что улучшение составило бо­лее 25 %. В то же время версия, которая использовала исправленную strstr, после включения оптимизации не показала никаких видимых улучшений, поскольку библиотечная функция strstr уже была оптими­зирована при инсталляции; оптимизатор обращается только к исходному коду, компилируемому непосредственно в данный момент, но не к сис­темным библиотекам. Правда, некоторые компиляторы имеют глобаль­ный оптимизатор, который в поисках возможных улучшений анализиру­ет всю программу целиком. Если такой компилятор есть в вашей системе, попробуйте использовать его, — может, он ужмет еще несколько циклов.

Надо предупредить, что чем агрессивнее оптимизация компилятора, тем больше вероятность внесения им ошибок в скомпилированную программу. Поэтому после применения оптимизатора, как, собственно, и после внесе­ния любого изменения, не забудьте запустить набор возвратных тестов.

Тонкая настройка кода. Если объемы данных достаточно существенны, серьезное значение имеет правильный выбор алгоритма. Более того, улучшенный алгоритм будет работать на любых машинах, компиляторах и языках. Но если и при должном алгоритме вопрос быстродействия по-прежнему стоит на повестке дня, то можно попробовать тонко на­строить (tune) код — отполировать детали циклов и выражений, чтобы заставить их работать еще быстрее.

Версия isspam, которую мы рассмотрели в конце параграфа 7.1, не была тонко настроена. Теперь мы покажем, как можно ее улучшить, изменив цикл. Итак, последний вариант выглядел следующим образом:

for (j =0; (с = mesg[j]) != '\0'; j++) {

for (i = 0; i < nstartingfc]; i++) {

k = starting[c][i];

if (memcmp(mesg+j, pat[k], patlen[k]) == 0) {

printf("spam: совпадает с -%s'\n", pat[k]);

return 1;

}

}

}

На этой версии после компиляции с оптимизатором наш набор тестов исполнялся за 6.6 секунды. Внутренний цикл в своем условии содержал индекс массива (nstarting[c]); а его значение фиксировано для каждой итерации внешнего цикла. Мы можем избежать его многократного вы­числения, единожды сохранив значение в локальной переменной:

for (j =0; (с = mesg[j]) != ‘\0'; j++) {

n = nstarting[c];

for (i = 0; i < n; i++) {

k = starting[c][i];

После этого изменения время уменьшилось до 5.9 секунды, то есть при­мерно на 10 % — типичное значение для ускорения, которого можно до­стичь с помощью тонкой настройки. Есть и еще одна переменная, которую мы можем вытащить из тела цикла, — starting[c] также фиксировано. Вроде бы, если мы уберем ее вычисление из цикла, то это улучшит производительность, однако наши тесты не показали никакого изменения. Надо сказать, что это тоже типично при тонкой настройке, — некоторые вещи помогают, а некоторые нет, и это можно определить только заме­рами времени. Кроме того, результаты могут варьироваться для разных машин и компиляторов.

Есть и еще одно изменение, которое можно внести в спам-фильтр. Внутренний цикл сравнивает образец со строкой, однако алгоритм построен на том, что их первые символы совпадают. Соответственно, мы можем настроить код так, чтобы memcrnp начинала сравнение со второго байта. Мы попробовали так сделать, и результатом стал 3-процентный прирост производительности. Это, конечно, немного, но от нас требова­лось изменить всего три строчки кода, что не так уж сложно.

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

Сколько времени стоит тратить на увеличение скорости работы про­граммы? Главный критерий — будут ли изменения достаточно резуль­тативны, чтобы окупиться. В качестве простейшего принципа можно принять следующее требование: время, потраченное на увеличение про­изводительности программы, не должно быть больше, чем тот выигрыш во времени, который накопится благодаря внесенным изменениям за жизненный цикл программы. Исходя из этого правила, можно сказать, что изменения алгоритма в isspam были оправданы, — они потребовали дня работы, а сэкономили (и продолжают экономить) по нескольку ча­сов каждый день. Удаление индекса массива из внутреннего цикла не сыграло столь глобальной роли, однако все равно имело смысл, посколь­ку программа эксплуатируется большим количеством пользователе. Оптимизация программ, которые используются коллективно, — вроде спам-фильтра или библиотеки — выгодна практически всегда, а опти­мизация тестовых программ не выгодна почти никогда. Программу, ко­торая будет считать что-нибудь в течение года, стоит доводить до макси­мального совершенства; более того, если через месяц ее работы вы придумаете способ 10-процентного ее ускорения, то эту программу сто­ит запустить заново.

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

Важно производить замер времени после внесения каждого измене­ния, чтобы быть уверенными в том, что ситуация действительно улуч­шается. Иногда два изменения, каждое из которых порознь улучшает программу, аннулируют эти улучшения при совместном использовании. Кроме того, надо помнить о том, что механизмы, лежащие в основе изме­рений времени, могут быть настолько непостоянны в работе, что вынес­ти однозначное решение о пользе изменений весьма проблематично. Даже в однопользовательских системах время исполнения может изме­няться непредсказуемым образом. Если разброс внутреннего таймера (или, по крайней мере, тех результатов, которые он вам возвращает) со­ставляет 10 %, то изменения, которые увеличивают производительность менее чем на эти 10 %, будет очень трудно отличить от нормальной по­грешности результатов.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]