Хорошо, вы определяете проблему там, где, казалось бы, не так много возможностей для улучшения. По моему опыту, это довольно редко. Я попытался объяснить это в статье доктора Доббса в ноябре 1993 года, начав с традиционно хорошо спроектированной нетривиальной программы без очевидных потерь и проводя серию оптимизаций, пока время настенных часов не уменьшилось с 48 секунд. до 1,1 секунды, и размер исходного кода был уменьшен в 4 раза. Мой диагностический инструмент был этим . Последовательность изменений была такой:
Первой найденной проблемой было использование кластеров списков (теперь называемых «итераторами» и «классами контейнеров»), занимающих более половины времени. Они были заменены довольно простым кодом, сократив время до 20 секунд.
В настоящее время крупнейший хронометрист больше строит списки. В процентном отношении он был не таким большим, но теперь это потому, что большая проблема была устранена. Я нахожу способ ускорить его, и время падает до 17 секунд.
Сейчас труднее найти явных виновников, но есть несколько мелких, с которыми я могу что-то сделать, и время сокращается до 13 секунд.
Теперь я, кажется, ударил стену. Образцы рассказывают мне точно, что он делает, но я не могу найти ничего, что можно улучшить. Затем я размышляю над базовым дизайном программы, ее структурой, управляемой транзакциями, и спрашиваю, действительно ли весь поиск по списку, который она выполняет, требует выполнения требований задачи.
Затем я натолкнулся на редизайн, где программный код фактически генерируется (с помощью макросов препроцессора) из меньшего набора источников, и в котором программа не постоянно выясняет вещи, которые, как знает программист, довольно предсказуемы. Другими словами, не «интерпретировать» последовательность действий, «скомпилировать» ее.
- Этот редизайн сделан, сжатие исходного кода в 4 раза, а время сокращено до 10 секунд.
Теперь, потому что он становится таким быстрым, его сложно отобрать, поэтому я даю ему в 10 раз больше работы, но следующие времена основаны на исходной рабочей нагрузке.
Больше диагностики показывает, что он проводит время в управлении очередями. В подкладке это сокращает время до 7 секунд.
Теперь большое время занимает диагностическая печать, которую я делал. Флеш - 4 секунды.
В настоящее время крупнейшими потребителями времени являются звонки на malloc и бесплатные звонки . Перезарядка объектов - 2,6 секунды.
Продолжая пробовать, я все еще нахожу операции, которые не являются строго необходимыми - 1,1 секунды.
Общий коэффициент ускорения: 43,6
Сейчас нет двух одинаковых программ, но в не игрушечных программах я всегда видел такой прогресс. Сначала вы получаете легкие вещи, а затем более сложные, пока не дойдете до точки убывающей отдачи. Тогда понимание, которое вы получите, может привести к редизайну, начав новый раунд ускорений, пока вы снова не достигнете убывающей отдачи. Теперь это точка , в которой она может иметь смысл усомниться в том , ++i
или i++
или for(;;)
или while(1)
быстрее: виды вопросов , которые я вижу так часто на переполнение стека.
PS Может возникнуть вопрос, почему я не использовал профилировщик? Ответ заключается в том, что почти каждая из этих «проблем» была сайтом вызова функций, которые точно определяют образцы стека. Профилировщики даже сегодня едва ли приходят к мысли, что операторы и инструкции вызова более важны для поиска и легче исправить, чем целые функции.
Я на самом деле создал профилировщик, чтобы сделать это, но для реального унизительного и близкого отношения к тому, что делает код, ничто не заменит попадание в него пальцев. Дело не в том, что количество выборок невелико, поскольку ни одна из обнаруженных проблем не настолько мала, чтобы их можно было легко пропустить.
ДОБАВЛЕНО: jerryjvl запросил несколько примеров. Здесь первая проблема. Он состоит из небольшого количества отдельных строк кода, занимающих более половины времени:
/* IF ALL TASKS DONE, SEND ITC_ACKOP, AND DELETE OP */
if (ptop->current_task >= ILST_LENGTH(ptop->tasklist){
. . .
/* FOR EACH OPERATION REQUEST */
for ( ptop = ILST_FIRST(oplist); ptop != NULL; ptop = ILST_NEXT(oplist, ptop)){
. . .
/* GET CURRENT TASK */
ptask = ILST_NTH(ptop->tasklist, ptop->current_task)
Они использовали кластер списков ILST (аналогичный списковому классу). Они реализованы обычным способом, причем «скрытие информации» означает, что пользователям класса не нужно было заботиться о том, как они были реализованы. Когда эти строки были написаны (примерно из 800 строк кода), не было мысли о том, что это может быть «узким местом» (я ненавижу это слово). Это просто рекомендуемый способ сделать что-то. Оглядываясь назад , легко сказать, что этого следовало избегать, но по моему опыту все проблемы с производительностью таковы. В общем, хорошо стараться избегать проблем с производительностью. Еще лучше найти и исправить те, которые были созданы, даже если их «следовало бы избежать» (задним числом).
Вот вторая проблема, в двух отдельных строках:
/* ADD TASK TO TASK LIST */
ILST_APPEND(ptop->tasklist, ptask)
. . .
/* ADD TRANSACTION TO TRANSACTION QUEUE */
ILST_APPEND(trnque, ptrn)
Это строит списки, добавляя элементы к своим целям. (Исправление состояло в том, чтобы собрать элементы в массивах и создать списки сразу.) Интересно то, что эти операторы только стоят (то есть находились в стеке вызовов) 3/48 первоначального времени, поэтому их не было На самом деле большая проблема в начале . Тем не менее, после устранения первой проблемы они стоили 3/20 времени, и теперь стали «более крупной рыбой». В общем, так оно и есть.
Я мог бы добавить, что этот проект был извлечен из реального проекта, в котором я помог. В этом проекте проблемы с производительностью были гораздо более серьезными (как и ускорения), такими как вызов подпрограммы доступа к базе данных во внутреннем цикле, чтобы увидеть, была ли задача завершена.
ДОБАВЛЕННАЯ ССЫЛКА: Исходный код, как оригинальный, так и переработанный, можно найти на сайте www.ddj.com за 1993 год, в файле 9311.zip, файлах slug.asc и slug.zip.
РЕДАКТИРОВАТЬ 2011/11/26: В настоящее время существует проект SourceForge, содержащий исходный код в Visual C ++ и подробное описание его настройки. Он проходит только первую половину сценария, описанного выше, и не следует точно такой же последовательности, но все равно получает ускорение на 2-3 порядка.