Как стать автором
Обновить

Чего не хватает для идеального профилирования кода

Время на прочтение4 мин
Количество просмотров1.8K

Знаете ли вы, как работает такая серьезная программа оценки производительности, как Intel VTune Amplifier? Не в смысле интерфейса с пользователем и разных возможностей, а на какой аппаратной поддержке она основана?

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

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

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

Т.е. достаточно поставить обработчик на системный таймер и извлекать из автоматически запоминаемого контекста указатель текущей команды RIP (EIP). Затем, имея адреса подпрограмм, легко определить, какая именно подпрограмма работала в момент прерывания, и статистически учитывать это в профиле выполняемого кода. Поиск подпрограммы по адресу RIP процесс, конечно, тоже не мгновенный и не бесплатный, но его можно отложить, а в реальном времени лишь запоминать в памяти очередное значение RIP.

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

Помнится, давным-давно, в начале 90-х, у меня был настольный компьютер (к сожалению, забыл марку, по-моему, немецкий), у которого на передней панели был спидометр! Я не шучу. На его передней панели был индикатор из светодиодных сегментов для трех цифр. Согласно очень мутному описанию, он показывал число команд в штуках, то ли в среднем за тик, то ли еще за какое-то фиксированное время. В общем, он показывал какое-то число, обычно близкое к 1.00, и чем оно было выше, тем более производительным был в этот момент процессор.

По тем временам прекрасная вещь для поиска «узких мест»! И, главное, специально для замеров в программе делать ничего не надо было: поменял очередной раз код, запустил тестовый прогон и смотришь на индикатор. Ну да, в те стародавние времена и тактовые частоты были невысокие, считалось все относительно медленно, и объем ПО был небольшим, да и MS DOS с современной точки зрения, это, вероятно, случай отсутствия ОС. Вполне можно было разглядеть на индикаторе любые незначительные отклонения производительности.

С его помощью я, отчасти из спортивного интереса, отчасти из уязвленного самолюбия, пытался ускорить работу транслятора с ассемблера RASM-86. Он выполнял тест за 23.4 секунды, а микрософтовский MASM выполнял этот же тест за 8.3 секунды. С помощью этого волшебного индикатора я быстро нашел все узкие места и неудачные команды (конечно, они оказались в лексическом анализаторе) и добился выполнения теста за 5.6 секунды.

И у этого примитивного средства было преимущество, которого, на мой взгляд, и не хватает для идеального профилирования выполняемого кода сегодня – аппаратная поддержка.

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

Разумеется, никаких новых команд вводить для этого не требуется. Задание условий для гипотетического дополнительного счетчика можно делать и через запись в порт самого процессора командой WRMSR, а чтение значения счетчика – командой чтения из порта процессора RDMSR. Кроме этого, запись в порт должна вызывать и сброс счетчика, чтобы не создавать для этого отдельное действие.

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

Смысл в том, что при задании начального и конечного адресов внутренний счетчик тактов обнуляется и теперь увеличивается на каждом такте только, когда указатель RIP/EIP попадает внутрь заданного диапазона адресов. В другом режиме — счетчик начинает работу, когда RIP/EIP строго совпадает с начальным адресом и заканчивает, когда строго совпадает с конечным адресом.

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

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

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

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

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

Таких команд, правда, пока нет. Но, возможно, что-то похожее именно для поддержки профилирования кода со стороны процессора в будущем и появится. А может быть, появится сначала в виртуальных машинах.

Теги:
Хабы:
Всего голосов 2: ↑1 и ↓1+2
Комментарии6

Публикации

Истории

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань