Как написать профилировщик?

Я хотел бы знать, как написать профайлер? Какие книги и / или статьи рекомендуется? Кто-нибудь может мне помочь?

Кто-то уже делал что-то подобное?


person Agusti-N    schedule 15.12.2008    source источник


Ответы (5)


Сначала я бы посмотрел на эти проекты с открытым исходным кодом:

Тогда я бы посмотрел на JVMTI (не JVMPI)

person Boune    schedule 16.12.2008

Обнадеживающе, не так ли :)

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

Так что, если вам просто нужны ответы, которые даст вам профилировщик, выберите тот, который написал кто-то другой. Если вы ищете интеллектуальный вызов, почему бы не написать его?

Я написал пару для сред выполнения, которые с годами стали неактуальными.

Есть два подхода

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

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

Версия JVMPI, по-видимому, относится к первому типу - ссылка, предоставленная uzhin, показывает, что она может сообщать о целом ряде вещей (см. раздел 1.3). То, что выполняется, изменяется для этого, поэтому профилирование может повлиять на производительность (и если вы профилируете то, что в противном случае было бы очень легкой, но часто вызываемой функцией, это может ввести в заблуждение).

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

Павел.

person The Archetypal Paul    schedule 15.12.2008

Я написал один раз, главным образом как попытку сделать «глубокую выборку» более удобной для пользователя. Когда вы выполняете этот метод вручную, это объясняется здесь. Он основан на выборке, но вместо того, чтобы брать большое количество маленьких выборок, вы берете небольшое количество больших выборок.

Например, он может сказать вам, что инструкция I (обычно вызов функции) стоит вам некоторый процент X от общего времени выполнения, более или менее, поскольку она появляется в стеке на X% выборок.

Подумайте об этом, потому что это ключевой момент. Стек вызовов существует, пока работает программа. Если конкретная инструкция вызова I находится в стеке X% времени, то, если эта инструкция может исчезнуть, этот X% времени исчезнет. Это не зависит от того, сколько раз выполняется I или сколько времени занимает вызов функции. Таким образом, таймеры и счетчики упускают суть. И в некотором смысле все инструкции являются инструкциями вызова, даже если они вызывают только микрокод.

Сэмплер основан на предположении, что лучше точно знать адрес инструкции I (потому что это то, что вы ищете), чем знать точно число X%. Если вы знаете, что можете сэкономить примерно 30% времени, перекодировав что-то, вас действительно волнует, что вы можете ошибиться на 5%? Вы все равно захотите это исправить. Количество времени, которое он реально сэкономит, не станет ни меньше, ни больше от того, что вы точно знаете X.

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

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

Главное, что предоставил профилировщик, — это пользовательский интерфейс, чтобы вы могли безболезненно изучить результаты. На этапе выборки получается набор образцов стека вызовов, где каждый образец представляет собой список адресов инструкций, где каждая инструкция, кроме последней, является инструкцией вызова. Пользовательский интерфейс был в основном так называемым «видом бабочки». У него есть текущий «фокус», который является конкретной инструкцией. Слева отображаются инструкции вызова непосредственно над этой инструкцией, взятые из образцов стека. Если инструкция фокуса является инструкцией вызова, то инструкции под ней отображаются справа, как отобранные из образцов. На инструкции фокуса отображается процент, который представляет собой процент стеков, содержащих эту инструкцию. Точно так же для каждой инструкции слева или справа процент разбивается по частоте каждой такой инструкции. Конечно, инструкция была представлена ​​файлом, номером строки и именем функции, в которой она находилась. Пользователь мог легко изучить данные, щелкнув любую из инструкций, чтобы сделать ее новым фокусом.

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

Возможно, это неочевидно, поэтому стоит упомянуть некоторые свойства этой техники.

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

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

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

  • Как упоминалось ранее, скорость выборки не является проблемой, потому что мы не измеряем производительность, мы диагностируем. Выборка не искажает результаты, потому что выборка не влияет на то, что делает программа в целом. Алгоритм, для завершения которого требуется N инструкций, по-прежнему требует N инструкций, даже если он останавливается любое количество раз.

  • Меня часто спрашивают, как сэмплировать программу, которая завершается за миллисекунды. Простой ответ заключается в том, чтобы обернуть его во внешний цикл, чтобы сделать выборку достаточно долго. Вы можете узнать, что занимает X% времени, удалить это, получить ускорение X%, а затем удалить внешний цикл.

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

Когда говорят о профилировании, часто упускают из виду тот факт, что вы можете делать это неоднократно, чтобы найти множество проблем. Например, предположим, что инструкция I1 находится в стеке 5% времени, а I2 находится в стеке 50% времени. Двадцать образцов легко найдут I2, но, возможно, не I1. Итак, вы исправляете I2. Затем вы делаете все это снова, но теперь I1 занимает 10% времени, поэтому 20 сэмплов, вероятно, увидят это. Этот эффект увеличения позволяет многократно применять профилирование для достижения больших совокупных коэффициентов ускорения.

person Community    schedule 10.04.2009
comment
Это звучит как отличный подход к поиску узкого места в программе. Но я не думаю, что это устраняет необходимость в традиционном профилировщике, когда вы выполняете низкоуровневую настройку. Вам нужны точные измерения, чтобы подтвердить ваши изменения. Если Foo заняло 50% до моего изменения и 40% после, я сделал значительное улучшение или это ошибка выборки? И иногда вам нужно прибить узкое место к уровню if-ветки, которая срабатывает гораздо чаще, чем вы ожидаете. Если это встроенный код, стек вызовов не даст вам этого. - person Adrian McCarthy; 23.04.2010
comment
@Adrian McCarthy: (Я не буду говорить о встраивании, потому что вы всегда можете перейти к машинному коду и вернуться к исходному коду.) Что касается вашей точки 50-40%, в этой парадигме вы не смотрите на функции и ожидаете небольших изменений как это. Скорее вы смотрите на линии (или инструкции, звонки или нет) и видите, какой % времени они составляют, и (что особенно важно) определяете, почему это время тратится. ... - person Mike Dunlavey; 23.04.2010
comment
@Adrian McCarthy: ... Если это не очень хорошая причина, то есть вы можете найти способ не тратить его, тогда % в этой строке не просто немного падает, он часто исчезает, и 1) все время уменьшается на этот %, и 2) распределение того, какие операторы занимают временные сдвиги. (Нет ничего плохого в измерении с помощью традиционного профилировщика, но мне это никогда не понадобилось, потому что общее время падает на процент, за который отвечает строка или инструкция.) - person Mike Dunlavey; 23.04.2010
comment
@Adrian McCarthy: Не буду заморачиваться, но, может быть, вы увидите, что точность измерения никогда не должна входить в процесс, потому что если строка кода (или целая функция) находится в стеке на 50% и оптимизирована до 40%, то изменение легко увидеть, потому что эти 10% приходятся непосредственно на общее время, которое можно измерить простым секундомером, не сомневаясь в точности. - person Mike Dunlavey; 23.04.2010

Спецификация JVMPI: http://java.sun.com/j2se/1.5.0/docs/guide/jvmpi/jvmpi.html

Я приветствую ваше мужество и храбрость

РЕДАКТИРОВАТЬ: И, как заметил пользователь Boune, JVMTI: http://java.sun.com/developer/technicalArticles/Programming/jvmti/

person Yoni Roit    schedule 15.12.2008

В качестве другого ответа я только что посмотрел LukeStackwalker на sourceforge. Это хороший небольшой пример стекового семплера, и с него хорошо начать, если вы хотите написать профилировщик.

Вот, на мой взгляд, правильно:

  • Он сэмплирует весь стек вызовов.

Эх... так близко, но так далеко. Вот, ИМО, что он (и другие сэмплеры стека, такие как xPerf) должен делать:

  • Он должен сохранять необработанные образцы стека. Как бы то ни было, он резюмирует на функциональном уровне по мере выборки. При этом теряется ключевая информация о номере строки, определяющая местонахождение проблемных сайтов вызовов.

  • Нет необходимости брать так много образцов, если их хранение является проблемой. Так как типовые проблемы с производительностью стоят от 10% до 90%, то 20-40 семплов покажут их вполне достоверно. Сотни образцов обеспечивают большую точность измерений, но не увеличивают вероятность обнаружения проблем.

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

    5/20 MyFile.cpp:326 для (i = 0; i ‹ strlen(s); ++i)

Это говорит о том, что строка 326 в MyFile.cpp появлялась в 5 из 20 образцов в процессе вызова strlen. Это очень важно, потому что вы сразу видите проблему и знаете, какое ускорение можно ожидать от ее устранения. Если вы замените strlen(s) на s[i], он больше не будет тратить время на этот вызов, поэтому эти выборки не будут происходить, и ускорение будет примерно 1/(1-5/20) = 20/(20-5) = 4 /3 = ускорение на 33%. (Спасибо Дэвиду Торнли за этот пример кода.)

  • Пользовательский интерфейс должен иметь представление «бабочка», показывающее операторы. (Если он также показывает функции, это нормально, но действительно важны операторы.) Например:

    3/20 MyFile.cpp:502 MyFunction(myArgs)
    2/20 HisFile.cpp:113 MyFunction(hisArgs)

    5/20 MyFile.cpp:326 для (i = 0; i ‹ strlen(s); ++i)

    5/20 strlen.asm:23 ... какой-то ассемблерный код ...

В этом примере строка, содержащая оператор for, находится в центре внимания. Это произошло на 5 образцах. Две строчки над ним говорят, что в 3 из этих семплов он был вызван из MyFile.cpp:502, а в 2 из этих сэмплов он был вызван из HisFile.cpp:113. Строка ниже говорит, что во всех 5 этих образцах это было в strlen (это неудивительно). В общем случае линия фокуса будет иметь дерево «родителей» и дерево «детей». Если по какой-то причине линию фокусировки нельзя исправить, ее можно поднять или опустить. Цель состоит в том, чтобы найти линии, которые вы можете исправить, на как можно большем количестве образцов.

ВАЖНО! Профилирование не следует рассматривать как нечто, что вы делаете один раз. Например, в приведенном выше примере мы получили ускорение на 4/3, исправив одну строку кода. Когда процесс повторяется, другие проблемные строки кода должны появляться в 4/3 раза чаще, чем раньше, и поэтому их будет легче найти. Я никогда не слышал, чтобы люди говорили об итерации процесса профилирования, но это крайне важно для получения общего значительного совокупного ускорения.

P.S. Если оператор встречается более одного раза в одном образце, это означает, что имеет место рекурсия. Это не проблема. Он по-прежнему считается только одним образцом, содержащим оператор. По-прежнему верно, что стоимость заявления приблизительно равна доле образцов, содержащих его.

person Mike Dunlavey    schedule 17.05.2009