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

7.1. Узкое место

Нам хотелось бы начать с описания того, как мы избавились от узкого места в важной программе нашей вычислительной системы.

Наша входящая почта поступала к нам через одну машину, называе­мую шлюзом (gateway), которая объединяла нашу внутреннюю сеть с внешним Интернетом. Электронная почта, приходящая извне, — в на­шу организацию, насчитывающую несколько тысяч человек, приходят десятки тысяч писем в день — поступает на шлюз и затем передается во внутреннюю сеть; такое разделение изолирует нашу локальную сеть от доступа из Интернета и позволяет указывать адрес только одной маши­ны (этого самого шлюза) для всех членов организации.

Одной из услуг, предоставляемых шлюзом, является защита от "спама" (spam — мясные консервы, содержащие в основном сало), незатребованной почты, рекламирующей услуги сомнительных достоинств. После первых успешных испытаний спам-фильтр был установлен на шлюз и включен для всех пользователей нашей внутренней сети — и немед­ленно возникла проблема. Машина, исполняющая роль шлюза, уже не­сколько устаревшая и без того достаточно загруженная, была буквально парализована: поскольку фильтрующая программа работала слишком медленно, она отнимала гораздо больше времени, чем вся остальная обра­ботка сообщений, и в результате доставка почты задерживалась на часы.

Это пример настоящей проблемы производительности: программа бы­ла не в состоянии уложиться во время, отводимое ей на работу, и пользо­ватели серьезно от этого страдали. Программа должна была работать го­раздо быстрее.

Несколько упрощая, можно сказать, что спам-фильтр работает при­мерно так: каждое входящее сообщение рассматривается как единая - строка, которая обрабатывается программой поиска образцов с целью обнаружить, не содержит ли она фраз из заведомого спама — таких, как "Make millions in your spare time" (сделайте миллион в свободное время) или "XXX-rated" (крутые порно). Подобные сообщения имеют тенден­цию появляться многократно, так что подобный подход достаточно эф­фективен, тем более что если какой-то спам проходил через фильтр, то характерные фразы из него добавлялись в список.

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

/* isspam: проверяет mesg на вхождение в него образцов pat */

int isspam(char *mesg)

{

int i;

for (i = 0; i < npat; i++)

if (strstr(mesg, pat[i]) != NULL) {

print.f("spam: совпадает с '%s'\n", pat[i]);

return 1;

}

return 0;

}

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

Благодаря профилированию — технологии, о которой мы поговорим в следующем параграфе, — мы выяснили, что реализация strstr такова, что использование ее в спам-фильтре неприемлемо. Изменив способ ра­боты st rst г, можно было сделать ее более эффективной для данной кон­кретной проблемы.

Существующая реализация strstr выглядела примерно так:

/* простая strstr: просматривает первый символ */

/* с помощью strchr */

char *strstr(const char *s1, const char *s2)

{

int n;

n = strlen(s2);

for (;;) {

s1 = strchr(s1, s2[0]);

if (s1 == NULL)

return NULL;

if (strncmp(s1, s2, n) == 0)

return (char *) s1;

s1++;

}

}

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

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

Во-вторых, strncmp содержит достаточно сложный внутренний цикл. В нем не только осуществляется сравнение байтов двух строк, но и про­изводится поиск символа окончания строки \0 в обеих строках, да еще при этом отсчитывается длина строки, переданной в качестве параметра. Поскольку длина всех строк известна заранее (хотя и не в функции strncmp), в этих сложностях нет необходимости, мы знаем, что подсчеты верны, поэтому проверка на \0 — пустая трата времени.

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

И наконец, даже если решить, что strncmp, strchr и strlen достаточно эффективны сами по себе, затраты на вызов этих функций сравнимы с за­тратами на вычисления, которые они осуществляют. Более эффектив­но выполнять все действия в отдельной аккуратно написанной версии st rst r, а не вызывать другие функции.

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

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

Наша новая strstr была добавлена в библиотеку, и в результате спам-фильтр стал работать примерно на 30 % быстрее, чем раньше, — хоро­ший результат для изменения одной функции.

К сожалению, и это было слишком медленно.

Чтобы решить задачу, важно задавать себе правильные вопросы. До сего момента мы искали самый быстрый способ поиска текстового об­разца в строке. Но на самом деле наша проблема заключается в поиске большого фиксированного набора текстовых образцов в большой строке переменной длины. Если исходить из этого, то наша strst r не представ­ляется, очевидно, лучшим решением.

Наиболее эффективный способ ускорить программу — применить ал­горитм получше. Теперь, когда у нас есть четкое определение проблемы, можно подумать и об алгоритме.

Основной цикл

for (i = 0; i < npat; i++)

if (strstr(mesg, pat[i]) != NULL)

return 1;

сканирует сообщение в точности npat раз; таким образом, в случае, если совпадений нет, он просматривает каждый байт сообщения npat раз, вы­полняя strlen(mesg)*npat сравнений.

Разумнее было бы поменять циклы местами, обходя сообщение еди­ножды — во внешнем цикле, а сравнения со всеми образцами осуществ­лять в параллельном или вложенном цикле:

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

if (совпадение с каким-либо образцом начиная с mesg[j])

return 1;

Повышение производительности достигнуто на основании простейшего наблюдения. Для того чтобы выяснить, не совпадает ли какой-нибудь образец с текстом сообщения, начиная с позиции j, нам не надо просмат­ривать все образцы — интересовать нас будут только те, что начинаются с того же символа, что и mesg[ j ]. В первом приближении, имея 52 буквы верхнего и нижнего регистров, мы можем ожидать выполнения только strlen(mesg)*npat/52 сравнений. Поскольку буквы распределены не одинаково — слова гораздо чаще начинаются с s, чем с х, — мы, конечно, не добьемся увеличения производительности в 52 раза, но все же кое-что у нас получится. Так что фактически мы создали хэш-таблицу, в ко­торой в качестве ключей используются первые буквы образцов.

Благодаря выполнению предварительных действий по созданию таб­лицы, определяющей, какой образец с какой буквы начинается, код isspam по-прежнему остался достаточно лаконичным:

int patlen[NPAT]; /* длина образца *./

int starting[UCHAR_MAX+1][NSTART]; /* образец с данным началом */

int nstnrting[UCHAR_MAX+1 ]; /* количество образцов для поиска*/

………

/* isspam: проверяет mesg на вхождение любого pat */

int isspam(char *mesg)

{

int i, j, k;

unsigned char c;

for (j =0; (c = mesg[j]) != ‘\0’; j++) {

for (i = 0; i < nstarting[c]; i++) {

k = starting[c][i];

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

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

return 1;

}

}

}

return 0;

}

Двумерный массив starting[c][ ] хранит для каждого символа с индек­сы образцов, которые начинаются с этого символа, а его напарник nstarting[c] фиксирует, сколько образцов начинается с этого с. Без этих таблиц внутренний цикл выполнялся бы от 0 до npat, то есть около тыся­чи раз; в нашем варианте он выполняется от 0 до примерно 20. Наконец, элемент массива patlen[k] содержит вычисленный заранее результат st rlen(pat [k]), то есть длину k-ro образца.

На приводимом ниже рисунке показаны эти структуры данных для трех образцов, начинающихся с буквы b.

Код для построения этих таблиц весьма прост:

int i;

unsigned char с;

for (1=0; i < npat; i++) {

с = pat[i][0];

if (nstarting[c] >= NSTART)

eprintf("слишком много образцов!(>=%d)"

"начинается на '%с'", NSTART, с); starting[c][nstarting[c]++] = i;

patlen[i]= strlen(pat[i]);

}

В теперешнем варианте — в зависимости от ввода — спам-фильтр стал работать от пяти до десяти раз быстрее, чем в версии с улучшенной st rst г, и от семи до пятнадцати раз быстрее, чем в исходной реализации. Мы не достигли показателя в 52 раза — отчасти из-за неравномерного распреде­ления букв, отчасти из-за того, что цикл в новом варианте стал более сложным, и отчасти из-за неизбежного выполнения бессмысленных срав­нений строк, но все же спам-фильтр перестал быть слабым звеном в обес­печении доставки почты. Проблема производительности решена.

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

Упражнение 7-1

Таблица, которая соотносит отдельный символ с набором образцов, начинающихся с него, стала основой существенного повышения производительности. Напишите версию isspam, которая использует в качестве дандекса два символа. Насколько это будет лучше? Это — простейший особый случай структуры данных, которая называется "бор" (trie)12. Большинство подобных структур данных основаны на затратах места

ради экономии времени.

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