Подходы к кодовой базе становятся все медленнее

11

Мы работаем над базой кода C ++ среднего размера (10Mloc), которая благодаря нашим усилиям по оптимизации становится все более медленной .

Эта кодовая база представляет собой набор библиотек, которые мы объединяем, чтобы заставить их работать. Когда была разработана общая структура взаимодействия этих библиотек, был сделан акцент на производительность, а затем, когда было добавлено больше частей, общая структура не сильно изменилась. Оптимизация проводилась по мере необходимости и по мере развития нашего оборудования. Это сделало дорогое раннее решение очевидным только намного позже. Сейчас мы находимся в точке, где дальнейшая оптимизация обходится намного дороже, так как она потребует переписывания больших частей базы кода. Мы приближаемся к нежелательному локальному минимуму, поскольку знаем, что в принципе код должен работать намного быстрее.

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

РЕДАКТИРОВАТЬ

Чтобы ответить на вопрос, как мы в настоящее время профиль:

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

Бенджамин Банье
источник
10
10Mloc на самом деле огромный проект
BЈовић
1
Это 10 миллионов лок (префикс SI), как считается sloc. Я назвал это "умеренным размером", потому что я понятия не имею, что здесь считают "большим".
Бенджамин Баннье
5
почти 10 миллионов, по крайней мере, велики везде и, вероятно, огромны в большинстве мест.
Ryathal
1
Круто, спасибо @honk Для 10M LOC кажется, что вы оптимизируете на очень низком уровне, почти на аппаратном уровне? Традиционный ООП («массив структур» AOS) ужасно неэффективен для кешей. Вы пытались переставить свои классы в SOA (структуру массивов), чтобы точки данных, над которыми работает ваш код, были согласованы с памятью? С таким количеством машин вы сталкиваетесь с блокировками связи или с синхронизацией? Последний вопрос: вы имеете дело с большими объемами потоковых данных или это в основном проблема сложных операций с вашими наборами данных?
Патрик Хьюз
1
Когда у вас так много кода, шансы превратятся из отличного в пари, что есть большие потенциальные ускорения нелокального типа, о котором я упоминал. Не имеет значения, если есть тысячи потоков / процессов. Какая-то случайная пауза или зацепит их за вас, или докажет, что я неправ
Майк Данлавей

Ответы:

9

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

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

примерПосле профилирования одной особенно медленной операции нашего визуального редактора правил мы не обнаружили «низко висящих плодов»: не было ни одной операции, которая занимала бы более 2% времени выполнения, но операция в целом была вялой. Однако мы обнаружили, что редактор отправляет большое количество небольших запросов на сервер. Несмотря на то, что редактор быстро обрабатывал отдельные ответы, количество взаимодействий запрос / ответ имело мультипликативный эффект, поэтому общее время, затраченное на операцию, составило несколько секунд. После тщательной каталогизации взаимодействий редактора во время этой длительной операции мы добавили новую команду в интерфейс сервера. Дополнительная команда была более специализированной, так как она принимала данные, необходимые для выполнения подмножества коротких операций, исследовал зависимости данных, чтобы выяснить окончательный набор возвращаемых данных, и предоставил ответ, содержащий информацию, необходимую для выполнения всех отдельных небольших операций за одну поездку на сервер. Это не уменьшило время обработки в нашем коде, но сократило очень большую задержку из-за удаления нескольких дорогостоящих циклических обращений клиент-сервер.

Горизонтальная оптимизация является связанной техникой, когда вы устраняете «медлительность», которая тонко распределяется между несколькими компонентами вашей системы с использованием определенной функции среды выполнения.

примерПосле профилирования длительной операции мы обнаружили, что мы делаем много вызовов через границу домена приложения (это очень характерно для .NET). Мы не могли удалить ни один из вызовов, и мы не могли связать их вместе: они приходили в разное время из самых разных разделов нашей системы, и то, что они запрашивали, зависело от результатов, полученных из предыдущих запросов. Каждый вызов требовал сериализации и десериализации относительно небольшого количества данных. Опять же, отдельные звонки были короткими по продолжительности, но очень большими по количеству. В итоге мы разработали схему, которая почти полностью избегала сериализации, заменив ее наведением указателя через границу домена приложения. Это была большая победа, потому что многие запросы от совершенно не связанных классов мгновенно стали намного быстрее в результате применения одногогоризонтальное решение.

dasblinkenlight
источник
Спасибо, что поделились своим опытом, имейте в виду, что это полезная оптимизация. Кроме того, поскольку они поднимают проблемные детали в другое место, их будет намного легче контролировать в будущем. В некотором смысле они ставят на место то, что должно было произойти в первую очередь, теперь только на основе достоверных данных.
Бенджамин Баннье
3

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

Когда вы начинаете это переписывание, вы должны сделать несколько вещей по-разному.

Первый. И самое главное. Хватит "оптимизировать". «Оптимизация» не имеет большого значения вообще. Как вы видели, только оптовые переписывают вопросы.

Следовательно.

Во-вторых. Понимать последствия каждой структуры данных и выбора алгоритма.

В третьих. Сделать фактический выбор структуры данных и алгоритма вопросом «позднего связывания». Разработка интерфейсов, которые могут иметь любую из нескольких реализаций, используемых за интерфейсом.

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

С. Лотт
источник
1
Спасибо за Ваш ответ. Хотя нам все еще нужно оптимизировать (что для меня подпадает под 1. и 2.), мне действительно нравится организованное мышление 3. Делая структуру данных, алгоритм и доступ определены поздно и явно, нужно уметь справляться со многими проблемы, с которыми мы сталкиваемся. Спасибо, что выложили это на связный язык.
Бенджамин Баннье
Вам на самом деле не нужно оптимизировать. Как только вы получите правильную структуру данных, оптимизация будет показана как пустая трата усилий. Профилирование покажет, где у вас неправильная структура данных и неправильный алгоритм. Дурачиться с разницей в производительности ++и +=1будет неактуально и почти неизмеримо. Это то, что ты надолго .
С.Лотт
1
Не все плохие алгоритмы могут быть найдены чистыми рассуждениями. Время от времени нужно сесть и профиль. Это единственный способ узнать, была ли первоначальная догадка верной. Это единственный способ оценить реальную стоимость (BigO + const).
Бенджамин Баннье
Профилирование покажет плохие алгоритмы. Совершенно верно. Это все еще не «оптимизация». Это все еще исправление фундаментального недостатка дизайна, который я вносил в дизайн. Оптимизация (настройка, тонкая настройка и т. Д.) Редко будет видна для профилирования.
С.Лотт
3

Хорошим практическим приемом является использование вашего модульного набора тестов в качестве набора тестов производительности .

Следующий подход хорошо работал в моих базах кода:

  1. Убедитесь, что у вас хороший юнит тест (вы уже сделали это, верно?)
  2. Убедитесь, что ваш фреймворк для выполнения теста сообщает о времени выполнения каждого отдельного теста . Это важно, потому что вы хотите найти, где происходит медленная производительность.
  3. Если тест выполняется медленно, используйте его как способ погрузиться и нацелиться на оптимизацию в этой области . Оптимизация до выявления проблемы с производительностью может считаться преждевременной, поэтому замечательным в этом подходе является то, что вы сначала получаете конкретные доказательства низкой производительности. При необходимости разбейте тест на более мелкие тесты, которые сравнивают различные аспекты, чтобы вы могли определить, в чем заключается основная проблема.
  4. Если тест выполняется очень быстро, это, как правило, хорошо, хотя вы можете рассмотреть его в цикле с другими параметрами. Это делает его лучшим тестом производительности, а также увеличивает охват тестового пространства параметров.
  5. Напишите несколько дополнительных тестов, которые конкретно нацелены на производительность, например, сквозное время транзакций или время для завершения 1000 приложений правил. Если у вас есть определенные нефункциональные требования к производительности (например, <300 мс времени отклика), то сделайте тест неудачным, если он займет слишком много времени.

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

Также было бы возможно отслеживать исторические времена испытаний и составлять графики производительности и спрогнозировать регрессии с течением времени в средней производительности. Я никогда не беспокоился об этом, главным образом, потому что немного сложно убедиться, что вы сравниваете подобное с изменением и добавляете новые тесты, но это может быть интересным упражнением, если производительность достаточно важна для вас.

mikera
источник
мне нравится техника - умная - однако, это микрооптимизация и она улучшится до локального минимума - она ​​не решит архитектурные проблемы, которые позволят вам достичь глобальных минимумов
jasonk
@jasonk - ты абсолютно прав. Хотя я бы добавил, что иногда он может дать вам доказательства того, что вы должны продемонстрировать, почему оправданы определенные архитектурные изменения .....
mikera
1

Ответ @dasblinkenlight указывает на очень распространенную проблему, особенно с большими базами кода (по моему опыту). Могут быть серьезные проблемы с производительностью, но они не локализованы . Если вы запускаете профилировщик, то нет никаких процедур, отнимающих достаточно времени, чтобы привлечь внимание. (Предполагая, что вы смотрите на процент времени, включающий вызываемых абонентов. Даже не думайте о «самостоятельном времени».)

На самом деле, в этом случае настоящая проблема была найдена не по профилированию, а по счастливой случайности.

У меня есть тематическое исследование, для которого есть краткое слайд-шоу в формате PDF , подробно иллюстрирующее эту проблему и способы ее решения. Суть в том, что, поскольку код работает намного медленнее, чем может быть, это означает (по определению), что избыточное время тратится на выполнение чего-то, что может быть удалено.

Если бы вы посмотрели на некоторые случайные выборки состояния программы, вы бы просто увидели, что она выполняет съемную деятельность, потому что это занимает процент времени. Вполне возможно, что съемная деятельность не ограничивается одной функцией или даже многими функциями. Это не локализуется таким образом.

Это не «горячая точка».

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

(Этот подход часто критикуют за то, что количество выборок слишком мало для статистической достоверности. Ответ дан на слайде 13 PDF. Вкратце - да, существует большая неопределенность в «измерении» потенциальной экономии, но 1) ожидаемое значение этой потенциальной экономии по существу не изменяется, и 2) когда потенциальная экономия $ x $ переводится в коэффициент ускорения на 1 / (1-x) $, она сильно отклоняется от высоких (полезных) факторов.)

Майк Данлавей
источник
Спасибо за Ваш ответ. Мы не верим в статистическую выборку и используем инструментарий с Valgrind. Это дает нам хорошие оценки как «собственной», так и «инклюзивной» стоимости большинства вещей, которые мы делаем.
Бенджамин Банье
@honk: Верно. Но, к сожалению, инструментарий все еще несет идею о том, что проблемы с производительностью локализованы и поэтому могут быть найдены путем измерения доли времени, затрачиваемого на подпрограммы и т. Д. Вы, безусловно, можете запустить valgrind и т. Д., Но если вы хотите получить реальное представление о производительности, посмотрите это слайд-шоу. ,
Майк Данлавей