Pull to refresh

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

Reading time 4 min
Views 34K
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 мысленным лучом


image
Видя, что БД загружена на 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
Выводы как всегда — классика жанра. Используйте инструменты при оценке производительности, не выдумывайте из головы. Ширше (или ширее) пользуйтесь готовыми библиотеками, вместо закатывания солнца вручную.

И да пребудет с вами святой Коннектий высокий перформанс!
Tags:
Hubs:
+100
Comments 65
Comments Comments 65

Articles