Как я могу профилировать код C ++, работающий в Linux? - PullRequest
1645 голосов
/ 17 декабря 2008

У меня есть приложение C ++, работающее под Linux, которое я сейчас оптимизирую. Как я могу определить, какие области моего кода работают медленно?

Ответы [ 14 ]

1299 голосов
/ 18 декабря 2008

Если ваша цель - использовать профилировщик, воспользуйтесь одним из предложенных.

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

Просто остановите его несколько раз и каждый раз смотрите на стек вызовов. Если есть какой-то код, который тратит впустую некоторый процент времени, 20% или 50% или что-то еще, то есть вероятность, что вы поймаете его в действии на каждом образце. Так что это примерно процент образцов, на которых вы это увидите. Там не требуется образованное предположение. Если у вас есть предположение, в чем заключается проблема, это докажет или опровергнет ее.

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

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

  1. они не суммируются на уровне инструкций, а
  2. они дают запутанные выводы при наличии рекурсии.

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

P.S. Это также может быть сделано в многопоточных программах, если есть способ собрать выборки стека вызовов из пула потоков в определенный момент времени, как в Java.

P.P.S Как грубое обобщение, чем больше уровней абстракции в вашем программном обеспечении, тем больше вероятность того, что вы обнаружите, что это является причиной проблем с производительностью (и возможностью получить ускорение).

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

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

ДОБАВЛЕНО: Позвольте мне сделать байесовское объяснение того, как это работает. Предположим, что есть какая-то инструкция I (вызов или другое), которая находится в стеке вызовов в некоторой доле f времени (и, следовательно, стоит так дорого). Для простоты предположим, что мы не знаем, что такое f, но предположим, что это либо 0,1, 0,2, 0,3, ... 0,9, 1,0, и априорная вероятность каждой из этих возможностей равна 0,1, поэтому все эти затраты одинаково вероятны априори.

Тогда предположим, что мы берем только 2 выборки из стека и видим инструкцию I для обеих выборок, обозначенную как наблюдение o=2/2. Это дает нам новые оценки частоты f I, согласно этому:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&&f=x)  P(o=2/2&&f >= x)  P(f >= x | o=2/2)

0.1    1     1             0.1          0.1            0.25974026
0.1    0.9   0.81          0.081        0.181          0.47012987
0.1    0.8   0.64          0.064        0.245          0.636363636
0.1    0.7   0.49          0.049        0.294          0.763636364
0.1    0.6   0.36          0.036        0.33           0.857142857
0.1    0.5   0.25          0.025        0.355          0.922077922
0.1    0.4   0.16          0.016        0.371          0.963636364
0.1    0.3   0.09          0.009        0.38           0.987012987
0.1    0.2   0.04          0.004        0.384          0.997402597
0.1    0.1   0.01          0.001        0.385          1

                  P(o=2/2) 0.385                

В последнем столбце говорится, что, например, вероятность того, что f> = 0,5, составляет 92%, по сравнению с предыдущим предположением 60%.

Предположим, что предыдущие предположения отличаются. Предположим, мы предполагаем, что P (f = 0,1) составляет 0,991 (почти наверняка), а все остальные возможности практически невозможны (0,001). Другими словами, наша предварительная уверенность в том, что I дешево. Тогда мы получим:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&& f=x)  P(o=2/2&&f >= x)  P(f >= x | o=2/2)

0.001  1    1              0.001        0.001          0.072727273
0.001  0.9  0.81           0.00081      0.00181        0.131636364
0.001  0.8  0.64           0.00064      0.00245        0.178181818
0.001  0.7  0.49           0.00049      0.00294        0.213818182
0.001  0.6  0.36           0.00036      0.0033         0.24
0.001  0.5  0.25           0.00025      0.00355        0.258181818
0.001  0.4  0.16           0.00016      0.00371        0.269818182
0.001  0.3  0.09           0.00009      0.0038         0.276363636
0.001  0.2  0.04           0.00004      0.00384        0.279272727
0.991  0.1  0.01           0.00991      0.01375        1

                  P(o=2/2) 0.01375                

Теперь говорится, что P (f> = 0,5) составляет 26%, по сравнению с предыдущим предположением 0,6%. Таким образом, Байес позволяет нам обновить нашу оценку вероятной стоимости I. Если объем данных невелик, он точно не говорит нам о стоимости, а лишь о том, что он достаточно велик, чтобы его можно было исправить.

Еще один способ взглянуть на него называется Правило наследования . Если вы подбрасываете монету 2 раза, и она выпадает в голову оба раза, что это говорит вам о вероятном весе монеты? Уважаемый способ ответить на этот вопрос - сказать, что это бета-распределение со средним значением (количество попаданий + 1) / (количество попыток + 2) = (2 + 1) / (2 + 2) = 75%.

(Ключ в том, что мы видим I более одного раза. Если мы видим его только один раз, это нам мало что говорит, кроме того, что f> 0.)

Таким образом, даже очень небольшое количество образцов может многое рассказать нам о стоимости инструкций, которые он видит. (И он будет видеть их с частотой, в среднем, пропорциональной их стоимости. Если взято n образцов, а f - это стоимость, то I появится на nf+/-sqrt(nf(1-f)) образцах. Пример, n=10, f=0.3, то есть 3+/-1.4 образцов.)


ДОБАВЛЕНО, чтобы дать интуитивное представление о разнице между измерением и выборкой из случайного стека:
Сейчас есть профилировщики, которые производят выборку стека, даже по времени настенных часов, но , что выходит , это измерения (или «горячая линия», или «горячая точка», от которой «узкое место» может легко скрыться). То, что они вам не показывают (и они легко могут), - это сами образцы. И если ваша цель состоит в том, чтобы найти узкое место, то количество их, которое вам нужно увидеть, составляет в среднем , 2, деленное на долю времени, которое требуется. Таким образом, если это займет 30% времени, 2 / .3 = 6,7 выборки в среднем покажет это, и вероятность того, что 20 выборок покажет это, составляет 99,2%.

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

enter image description here

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

521 голосов
/ 21 апреля 2009

Вы можете использовать Valgrind со следующими параметрами

valgrind --tool=callgrind ./(Your binary)

Будет сгенерирован файл с именем callgrind.out.x. Затем вы можете использовать инструмент kcachegrind для чтения этого файла. Это даст вам графический анализ вещей с результатами, например, какие строки стоят сколько.

322 голосов
/ 17 декабря 2008

Я предполагаю, что вы используете GCC. Стандартное решение было бы для профиля с gprof .

Обязательно добавьте -pg к компиляции перед профилированием:

cc -o myprog myprog.c utils.c -g -pg

Я еще не пробовал, но слышал хорошие новости о google-perftools . Это определенно стоит попробовать.

Смежный вопрос здесь .

Несколько других модных слов, если gprof не выполняет работу за вас: Valgrind , Intel VTune , Sun DTrace .

240 голосов
/ 17 августа 2010

Более новые ядра (например, новейшие ядра Ubuntu) поставляются с новыми инструментами 'perf' (apt-get install linux-tools) AKA perf_events .

Они поставляются с классическими профилировщиками сэмплирования ( man-page ), а также с удивительной временной диаграммой !

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

Alt text

71 голосов
/ 23 мая 2009

Я бы использовал Valgrind и Callgrind в качестве основы для своего набора инструментов профилирования. Важно знать, что Valgrind - это виртуальная машина:

(wikipedia) Valgrind по сути является виртуальным машина, использующая оперативность (JIT) методы составления, в том числе динамическая перекомпиляция. Ничего из оригинальная программа когда-либо запускается прямо на хост-процессоре. Вместо этого Valgrind сначала переводит программа во временную, более простую форму называется промежуточное представительство (IR), который является нейтральным к процессору, Форма на основе SSA. После преобразования инструмент (см. ниже) бесплатен какие бы преобразования он ни хотел на ИК, до того, как Valgrind переводит ИК обратно в машинный код и позволяет хост-процессор запускает его.

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

Еще один инструмент, построенный на Вальгринде, - Массив. Я использую его для профилирования использования памяти кучи. Работает отлично. Что он делает, так это то, что он дает вам снимки использования памяти - детальная информация, ЧТО содержит какой процент памяти, и ВОЗ поместила его туда. Такая информация доступна в разные моменты времени запуска приложения.

58 голосов
/ 08 июня 2012

Ответ для запуска valgrind --tool=callgrind не является полным без некоторых опций. Обычно мы не хотим профилировать 10 минут медленного запуска в Valgrind и хотим профилировать нашу программу, когда она выполняет какую-то задачу.

Так что это то, что я рекомендую. Сначала запустите программу:

valgrind --tool=callgrind --dump-instr=yes -v --instr-atstart=no ./binary > tmp

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

callgrind_control -i on

Включает профилирование. Чтобы выключить и остановить всю задачу, мы можем использовать:

callgrind_control -k

Теперь у нас есть несколько файлов с именем callgrind.out. * В текущем каталоге. Чтобы увидеть результаты профилирования, используйте:

kcachegrind callgrind.out.*

Я рекомендую в следующем окне нажать на заголовок столбца «Self», в противном случае он показывает, что «main ()» является наиболее трудоемкой задачей. «Self» показывает, сколько каждой функции потребовалось время, а не вместе с иждивенцами.

55 голосов
/ 30 июня 2011

Это ответ на Ответ Назгоба от Gprof .

Я использовал Gprof последние пару дней и уже обнаружил три существенных ограничения, одно из которых я не видел нигде (пока), документированных:

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

  2. Граф вызовов запутывается указателями на функции. Пример: у меня есть функция с именем multithread(), которая позволяет мне выполнять многопоточность указанной функции по указанному массиву (оба передаются в качестве аргументов). Однако Gprof рассматривает все вызовы multithread() как эквивалентные для вычисления времени, проведенного у детей. Поскольку некоторые функции, которые я передаю multithread(), занимают намного больше времени, чем другие, мои графы вызовов в основном бесполезны. (Для тех, кто интересуется, является ли здесь проблема с многопоточностью: нет, multithread() может опционально, и в этом случае выполнил все последовательно только в вызывающем потоке).

  3. Здесь говорится здесь , что "... цифры количества вызовов получены путем подсчета, а не выборки. Они абсолютно точные ...". Тем не менее, я нахожу свой график вызовов, показывающий мне 5345859132 + 784984078 как статистику вызовов для моей наиболее вызываемой функции, где первый номер должен быть прямым вызовом, а второй рекурсивный вызов (который все из себя). Поскольку это означало, что у меня была ошибка, я вставил в код длинные (64-битные) счетчики и повторил тот же прогон. По моим подсчетам: 5345859132 прямых и 78094395406 саморекурсивных вызовов. Там много цифр, поэтому я укажу, что измеряемые мной рекурсивные вызовы составляют 78 млрд. Против 784 млн. Из Gprof: коэффициент в 100 разнится. Оба запуска были однопоточными и неоптимизированными, один скомпилированный -g, а другой -pg.

Это был GNU Gprof (GNU Binutils для Debian) 2.18.0.20080103, работающий под 64-битным Debian Lenny, если это кому-нибудь поможет.

18 голосов
/ 24 февраля 2017

Используйте Valgrind, callgrind и kcachegrind:

valgrind --tool=callgrind ./(Your binary)

генерирует callgrind.out.x. Прочитайте это, используя kcachegrind.

Используйте gprof (добавьте -pg):

cc -o myprog myprog.c utils.c -g -pg 

(не очень хорошо для многопоточности, указателей функций)

Используйте google-perftools:

Используется выборка по времени, обнаруживаются узкие места ввода-вывода и ЦП.

Intel VTune является лучшим (бесплатно для образовательных целей).

Другие: AMD Codeanalyst (с заменой на AMD CodeXL), OProfile, инструменты 'perf' (apt-get install linux-tools)

5 голосов
/ 17 марта 2018

Для однопоточных программ вы можете использовать igprof , Ignominous Profiler: https://igprof.org/.

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

3 голосов
/ 28 ноября 2013

Это два метода, которые я использую для ускорения моего кода:

Для приложений, связанных с процессором:

  1. Используйте профилировщик в режиме отладки для определения сомнительных частей вашего кода
  2. Затем переключитесь в режим RELEASE и закомментируйте сомнительные разделы вашего кода (заглушите его ничем), пока не увидите изменения в производительности.

Для приложений, связанных с вводом / выводом:

  1. Используйте профилировщик в режиме RELEASE для определения сомнительных частей вашего кода.

нотабене

Если у вас нет профилировщика, используйте профилировщик для бедняка. Хит пауза при отладке вашего приложения. Большинство комплектов разработчика будут разбиты на сборки с закомментированными номерами строк. По статистике вы можете приземлиться в регионе, который потребляет большую часть ваших циклов ЦП.

Для CPU, причина для профилирования в режиме DEBUG состоит в том, что если вы попробовали профилирование в режиме RELEASE , компилятор собирается уменьшить математические, векторизованные циклы и встроенные функции, которые имеет тенденцию превращать ваш код в не отображаемый беспорядок при сборке. Не отображаемый беспорядок означает, что ваш профилировщик не сможет четко определить, что занимает так много времени, поскольку сборка может не соответствовать оптимизируемому исходному коду . Если вам нужна производительность (например, чувствительная к времени) в режиме RELEASE , отключите функции отладчика, чтобы сохранить работоспособность.

Для привязки к вводу / выводу профилировщик все еще может идентифицировать операции ввода / вывода в режиме RELEASE , поскольку операции ввода / вывода либо связаны с внешней библиотекой (большую часть времени), либо в в худшем случае это приведет к вектору прерывания системного вызова (который также легко идентифицируется профилировщиком).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...