О тонкостях повышения performance на С++, или как делать не надо

Источник: habrahabr
viklequick

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

Вкратце, задача была такой - есть некий робот на С++, обдирающий HTML страницы, и собранное складывающий в БД (MySQL). С массой функционала и вебом на LAMP - но это к повествованию отношения не имеет.

Предыдущая команда умудрилась на 4-ядерном Xeon в облаке получить фантастическую скорость сбора аж в 2 страницы в секунду, при 100% утилизации CPU как сборщика, так и БД на отдельном таком же сервере. 

К слову, поняв что они не справляются - "команда крепких профессионалов" из г. Бангалор сдалась и разбежалась, так что кроме горки исходников - "ничего! помимо бус" (С).

О тонкостях наведения порядка в PHP и в схеме БД поговорим как-нибудь в другой раз, приведу только один пример приехавшего к нам мастерства.

Приступаем к вскрытию

image
Столь серьезная загрузка БД меня заинтересовала в первую очередь. Включаю детальное логирование - и начинаю вырывать на себе волосы во всех местах вот оно. 

Задачи из интерфейса разумеется складывались в БД, а робот 50 раз в секунду опрашивал - а не появилась ли новая задача? Причем данные естественно разложены так, как удобно интерфейсу, а не роботу. Итог - три inner join в запросе. 

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

select * from new_table where status = Pending

Новая картинка - сборщик по-прежнему занят на 100%, БД на 2%, теперь четыре страницы в секунду.

Берем в руки профилировщик

image
И внезапно выясняется, что 80% времени выполнения занимают чудные методыEnterCriticalSection и LeaveCriticalSection. А вызываются они (предсказуемо) из стандартного аллокатора одной известной компании.

Вечер перестает быть томным, а я понимаю что работы - много и переписывать придется от души.

И разумеется - парсить HTML быдлокодеры мои предшественники предпочитали пачкой регулярных выражений, ведь SAX - это так сложно.

Самое время ознакомиться - а что было улучшено до меня?

 

 

 

Об опасности premature optimizations мысленным лучом

Видя, что БД загружена на 100%, ребята были твердо уверены, что тормозит вставка в список новых URL для обработки. 

Я даже затрудняюсь понять - чем они руководствовались, оптимизируя именно этот кусок кода. Но сам подход! У нас по идее тормозит вот тут, давайте мы затормозим еще.

Для этого, они придумали такие трюки:

  1. Очередь асинхронных запросов на insert
  2. Огромная HashMap в памяти, самописная, с giant lock, которая запоминала все пройденные URL в памяти. А так как это был сервер - то его после таких оптимизаций приходилось регулярно перезапускать. Очистку своего кэша они не доделали.
  3. Масса магических констант, например - для обработки следующей партии URL из БД беретсмя не более 400 записей. Почему 400? А подобрали.
  4. Количество "писателей" в БД было велико, и каждый пытался свою часть впихнуть в цикле, вдруг повезет.

И конечно же много других перлов было в наличии.

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

void GodClass::PlaceToDB(const Foo* bar, ...) {
/* тут код с вариантом номер 1, закомментарен */
/* тут код с вариантом номер 2 - копипаст первого и немного изменений, закомментарен  */
/* тут код с вариантом номер 3 - еще изменили, не забыв скопировать вариант номер два, закомментарен  */
....
/* тут вариант номер N-1, уже ничего общего не имеет с первым вариантом, закомментарен  */
// а тут наконец-то вариант рабочий
}

Что делал я

image
Разумеется, все трюки были немедленно выброшены, я вернул синхронную вставку, а в БД был повешен constraint, чтобы отсекал дубли (вместо плясок с giant lock и самописным hashmap). 

Автоинкрементные поля также убрал, вместо них вставил UUID (для подсчета нового значения может приползать неявный lock table). Заодно серьезно уменьшил таблицу, а то по 20К на строчку - неудивительно что БД проседает.

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

Результат - 15 страниц в секунду.

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

Регулярные выражения для разбора мегабайтных структурированных файлов - это плохо

image
Продолжаю изучать то, что сделано до меня, наслаждаюсь подходом неизвестных мне авторов. 

Ме-то-ди-ка!  

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

  • Вырезали все комментарии в HTML
  • Вырезали комментарии в JavaScript
  • Вырезали теги script
  • Вырезали теги style
  • Вынули две цифры из head
  • Вырезали все кроме body
  • Теперь собрали все "a href" и вырезали их
  • В body вырезали все ненужные div и table, а также картинки
  • После чего убрали табличную разметку
  • В оставшемся убирали теги p, strong, em, i, b, г и т. д.
  • И наконец в оставшемся plain text достали еще три цифры

Удивительно с таким подходом, что оно хотя бы 2 страницы в секунду пережевывало.

Понятно, сами выражения после их тюнинга я не привожу - это огромная простыня нечитаемых закорючек.

Это еще не все - разумеется, была использована правильная библиотека boost, а все операции проводились над std::string ( правильно - а куда еще HTML складывать? char* не концептуально! Только хардкор! ). Вот отсюда и безумное количество реаллокаций памяти.

Беру char* и простенький парсер HTML в SAX-style, нужные цифры запоминаю, параллельно вытаскиваю URL. Два дня работы, и вот.

Результат - 200 страниц в секунду.

Уже приемлемо, но - мало. Всего в 100 раз.

Еще один подход к снаряду

image
Перехожу к результатам нового профилирования. Стало лучше, но аллокаций все еще много, и на первое место вылез почему-то бустовский to_lower().

Первое, что бросается в глаза - это могучий класс URL, цельнотянутый из Java.  Ну правильно - ведь это С++, он по любому быстрее будет, подумаешь что аллокаторы разные . Так что пачка копий и substring() - наше индусское все. И конечно же to_lower прямо к URL::host применять ни-ни - надо на каждом сравнении и упоминании и непременно boost-ом.

Убираю чрезмерное употребление to_lower(), переписываю URL на char* без переаллокаций вместо std::string. Заодно оптимизирую пару циклов.

Результат - 300 страниц в секунду.

На этом закончил, ускорение было достигнуто в 150 раз, хотя еще были резервы для ускорения. И так убил больше 2х недель.

Выводы

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

И да пребудет с вами святой Коннектий высокий перформанс!


Страница сайта http://www.interface.ru
Оригинал находится по адресу http://www.interface.ru/home.asp?artId=31042