Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Знаете ли вы, как работает такая серьезная программа оценки производительности, как 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 строго совпадает с начальным адресом и заканчивает, когда строго совпадает с конечным адресом.
Такая незначительная доработка процессора позволит использовать его для профилирования выполняемого кода без какого либо изменения самого кода анализируемой программы и совершенно прозрачно для нее.
В первом режиме я задаю начало и конец своей, может быть небольшой подпрограммы внутри огромного проекта, причем начало — это необязательно именно точка входа в подпрограмму. Затем просто запускаю главную программу, возможно, заканчиваю или останавливаю её прогон, а затем читаю получившийся счетчик — число тактов, которое управление находилось именно в этой подпрограмме, без учета, например, времени работы системных вызовов (так как они наверняка окажутся вне заданного диапазона адресов).
Во втором режиме я задаю точку входа в подпрограмму и точку выхода из неё. И получаю счетчик тактов нахождения в подпрограмме уже с учетом всех вложенных вызовов. Только в том случае, если точек выхода несколько, возможно, придется слегка менять код программы, чтобы такое профилирование давало верные результаты.
В большинстве же случаев можно не ставить никаких специальных отладочных вызовов, не использовать никаких прерываний и вообще никак не влиять на работу кода. И в результате получать некоторую количественную характеристику производительности выполнения анализируемого участка кода.
При попытках улучшения теперь можно сравнивать не все время работы огромной программы, а лишь с такой же предыдущей количественной характеристикой, т.е. становится возможным увидеть изменения производительности в масштабах микро- и наносекунд, не запуская миллионы циклов теста.
Таких команд, правда, пока нет. Но, возможно, что-то похожее именно для поддержки профилирования кода со стороны процессора в будущем и появится. А может быть, появится сначала в виртуальных машинах.