NES - измерение времени выполнения кода
При разработке для ретро-платформ нередко возникает необходимость сделать оценку быстродействия разных частей программы — то, что в современной практике называется 'профилированием'. Например, после добавления новых возможностей в игру её логика перестала укладываться в один кадр, начались замедления, и требуется выяснить, на какой части кода нужно сосредоточить усилия по оптимизации в первую очередь. Конечно, для небольших фрагментов кода на ассемблере всегда можно (и нужно) посчитать время выполнения в тактах вручную, но на больших участках со сложной логикой, либо при написании программы на языке высокого уровня, этот подход не годится. Рассмотрим, какие способы решения этой задачи есть в арсенале современного разработчика для NES.
На глаз
Наиболее простой способ — визуальная оценка с помощью любого подходящего растрового эффекта. Не требует никаких дополнительных средств, доступен на реальной приставке. Широко применяется на ZX Spectrum и Commodore 64, где делается через изменение цвета бордюра. На C64 он распространён настолько, что существует повсеместно используемое понятие rastertime, в котором измеряется быстродействие процедур — в частях от целого растра или в числе строк.
Идея заключается в выполнении оцениваемого участка кода во время прохода луча телевизионной развёртки по видимому экрану. Перед началом выполнения кода изменяется какой-нибудь параметр, незамедлительно и явно влияющий на отображение — такой, как цвет бордюра или фона. После выполнения кода параметр меняется обратно. По высоте получившейся полосы с изменившимся (окрашенным) изображением на экране можно сделать оценку времени — как просто сравнительную, типа 'есть запас ещё в четверть видимого растра', 'было столько, после оптимизации стало меньше', так и более точную, исходя из знания, сколько тактов процессора выполняется за одну строку растра.
Конечно, этот способ применим только до тех пор, пока время выполнения тестируемого кода не больше, чем время отображения видимой части одного телевизионного кадра. Когда код перестаёт укладываться в кадр, для проведения оценки может понадобиться отключать разные части программы, выносить код для замера в отдельно созданное окружение, оценивать по частям.
Для того, чтобы воспользоваться способом на NES, нужно знать, какой же параметр изображения подходит для этих целей — даст незамедлительный эффект, и при этом не сильно помешает наблюдению результата работы программы. У NES нет бордюра, а цвета палитры во время прохода луча по экрану изменять очень затруднительно. Регистры скролла и выбора банка графики использовать нежелательно, так как это не позволит наблюдать нормальный результат работы программы, а также обычно они изменяются в обработчике NMI, что создаёт возможность неудачного наложения обращений к регистрам в двух потоках кода.
После исключения всех неподходящих вариантов остаётся один регистр, изменение которого имеет минимальный потенциал для побочных эффектов — это $2001 (PPUMASK). Он позволяет разрешить или запретить отображение спрайтов и фона, переключить отображение в монохромный режим, а также усилить и подавить цветовые компоненты изображения — так называемый color emphasis или color tint. Именно две последние возможности обычно используется для замера времени выполнения.
Бит 0 регистра включает монохромный режим, в котором биты оттенка в кодах цвета игнорируются, а чёрный цвет становится светло-серым. Иначе говоря, все цвета заменяются с $LH на $L0 (L яркость, H оттенок). Биты 5-7 регистра включают усиление для красной, зелёной и синей компонент цвета, с одновременным подавлением остальных компонент (поэтому установка всех трёх битов даст тёмное изображение). В монохромном режиме эти биты придадут изображению цветовой оттенок.
Как и в случае с бордюром на ZX и C64, возможность задания цвета позволяет получить примерную карту распределения процессорного времени, используя разные цветовые значения для разных участков кода. При сильных изменениях времени выполнения лучше разобраться в происходящем поможет функция покадрового продвижения (Frame Advance), присутствующая в разных эмуляторах. Например, во FCEUX она доступна по клавише | (с двумя слэшами).
Надо заметить, что color emphasis оказывает разное действие на изображение во всех версиях оригинального PPU (NTSC, PAL, RGB) и на его разных клонах. Эмуляторы также отображают результат немного по-разному. Выбор компонент для усиления зависит от изображения в игре, ведь если оно по большей части синее, и усилена синяя компонента, результат будет плохо заметен. Поэтому при использовании способа на реальной приставке нужно подбирать значения, дающие хорошо заметный результат в конкретной ситуации.
Для уточнения оценки времени выполнения нужно знать несколько цифр. Растр NTSC-версии приставки содержит 261 строку, из них 224 в видимой области экрана. За одну строку процессор выполняет около 114 тактов, на один такт приходится три пикселя в строке. Для PAL-версии это 312 строк и 268 видимых, 106 тактов в строке и 3.2 пикселя на такт процессора.
Через отладчик FCEUX
В отладчике стандартной версии эмулятора FCEUX есть счётчик выполненных тактов и инструкций, с возможностью обнуления. Совместно с точками останова, его довольно просто применить для замера времени выполнения нужного участка кода. Для этого требуется поставить точки останова (Breapoints, Add) в начале и сразу после конца оцениваемого кода. При срабатывания первой точки нужно сбросить счётчик тактов (Reset counters) и дать коду выполниться до второй точки (Run).
Основная сложность применения этого способа в нахождении начала и конца нужного кода в памяти. Ведь трансляторы, в особенности компилятор C, скрывают от программиста реальные адреса, не все из них умеют показывать текущий адрес трансляции, чтобы можно было узнать, где искать полученный код, и в любом случае для получения реальных адресов и указания их в отладчике нужны дополнительные действия. К тому же, при оптимизации размер кода изменяется, что потребует постоянной коррекции адресов. В программе также может использоваться несколько банков памяти, и в нужных адресах может выполняться разный код — например, в одном банке код игрового цикла, в другом код проигрывателя музыки, что вызовет ложные срабатывания точек останова.
Для устранения этих неудобств можно применить маленькую хитрость — в начале и конце нужного кода добавить запись в 'пустой' адрес, изменение которого ни на что не повлияет, а в отладчике поставить условие останова по записи в этот адрес. В карте памяти NES как раз есть подходящий для этих целей диапазон $4020..$5fff, неиспользуемый почти во всех известных конфигурациях, за исключением Famicom Disk System и крайне экзотической NROM-368. Для того, чтобы исключить саму операцию записи в память из подсчёта, нужно после срабатывания первой точки останова пропустить её (Step over). Дальнейшие действия остаются такими же.
Отладчик FCEUX. Счётчик тактов справа посередине, обведён зелёной рамкой.
Специальные эмуляторы
Наиболее удобным способом замера времени выполнения кода являются эмуляторы, в которых предусмотрена такая возможность. К сожалению, её нет в стандартных ветках всех популярных эмуляторов, но она есть в специальных версиях VirtuaNESprof и NintendulatorDX. Незначительным недостатком этого способа является то, что по разным причинам эти эмуляторы могут быть неудобны в качестве основного инструмента для отладки, что вынудит часто переключаться между разным эмуляторами, а наиболее продвинутые возможности одного из них доступны только при использовании определённой среды разработки.
Оба эмулятора используют одинаковый принцип управления счётчиками тактов — через запись любого значения по определённым адресам из обычно неиспользуемого диапазона. Считается только время выполнения кода между этими записями, но не сами записи, так что на результат измерения они не влияют. Разница между эмуляторами заключается в адресах, количестве счётчиков и способе отображения результата.
В VirtuaNESprof всего один счётчик. Результат его работы постоянно отображается прямо на экране и содержит текущий замер, а также усреднённое и максимальное значение замеров. Счётчик запускается записью по адресу $401e, останавливается записью в $401f.
NintendulatorDX предоставляет сразу 16 независимых счётчиков. Они отображаются в окне отладчика (Debug, Disassembly, Timers) и содержат ещё больше информации — текущее, усреднённое, минимальное и максимальное значения. Запуск нужного счётчика осуществляется записью по адресу $402x, где X номер счётчика 0..15, останов записью по адресу $403x.
Помимо 'ручных' счётчиков, в NintendulatorDX реализована система автоматического профилирования кода. Она доступна только при работе в связке с входящей в его комплект модифицированной версией пакета CC65. Для использования этой возможности при компиляции изучаемой программы требуется создать файл с отладочной информацией, используя ключи -g для ассемблера и линкера, а также --dbgfile имя.nes.dbg (имя должно совпадать с именем .nes-файла) для линкера. Этот файл будет автоматически загружен эмулятором. Части программы, время выполнения которых нужно учитывать отдельно, требуется заключить в блоки .scope и .proc. Измерение запускается любой записью по адресу $4041, останавливается записью в $4042, либо по закрытию эмулятора. Результат будет выведен в текстовом формате в файл имя.nes.profiling.txt.
Отображение результата замера в VirtuaNESprof. В середине кадра также виден замер по растру, через выбор монохромного режима.
Отладчик NintendulatorDX. Счётчики тактов справа, на вкладке Timers. Работающий счётчик обведён зелёной рамкой.
На глаз
Наиболее простой способ — визуальная оценка с помощью любого подходящего растрового эффекта. Не требует никаких дополнительных средств, доступен на реальной приставке. Широко применяется на ZX Spectrum и Commodore 64, где делается через изменение цвета бордюра. На C64 он распространён настолько, что существует повсеместно используемое понятие rastertime, в котором измеряется быстродействие процедур — в частях от целого растра или в числе строк.
Идея заключается в выполнении оцениваемого участка кода во время прохода луча телевизионной развёртки по видимому экрану. Перед началом выполнения кода изменяется какой-нибудь параметр, незамедлительно и явно влияющий на отображение — такой, как цвет бордюра или фона. После выполнения кода параметр меняется обратно. По высоте получившейся полосы с изменившимся (окрашенным) изображением на экране можно сделать оценку времени — как просто сравнительную, типа 'есть запас ещё в четверть видимого растра', 'было столько, после оптимизации стало меньше', так и более точную, исходя из знания, сколько тактов процессора выполняется за одну строку растра.
Конечно, этот способ применим только до тех пор, пока время выполнения тестируемого кода не больше, чем время отображения видимой части одного телевизионного кадра. Когда код перестаёт укладываться в кадр, для проведения оценки может понадобиться отключать разные части программы, выносить код для замера в отдельно созданное окружение, оценивать по частям.
Для того, чтобы воспользоваться способом на NES, нужно знать, какой же параметр изображения подходит для этих целей — даст незамедлительный эффект, и при этом не сильно помешает наблюдению результата работы программы. У NES нет бордюра, а цвета палитры во время прохода луча по экрану изменять очень затруднительно. Регистры скролла и выбора банка графики использовать нежелательно, так как это не позволит наблюдать нормальный результат работы программы, а также обычно они изменяются в обработчике NMI, что создаёт возможность неудачного наложения обращений к регистрам в двух потоках кода.
После исключения всех неподходящих вариантов остаётся один регистр, изменение которого имеет минимальный потенциал для побочных эффектов — это $2001 (PPUMASK). Он позволяет разрешить или запретить отображение спрайтов и фона, переключить отображение в монохромный режим, а также усилить и подавить цветовые компоненты изображения — так называемый color emphasis или color tint. Именно две последние возможности обычно используется для замера времени выполнения.
Бит 0 регистра включает монохромный режим, в котором биты оттенка в кодах цвета игнорируются, а чёрный цвет становится светло-серым. Иначе говоря, все цвета заменяются с $LH на $L0 (L яркость, H оттенок). Биты 5-7 регистра включают усиление для красной, зелёной и синей компонент цвета, с одновременным подавлением остальных компонент (поэтому установка всех трёх битов даст тёмное изображение). В монохромном режиме эти биты придадут изображению цветовой оттенок.
Как и в случае с бордюром на ZX и C64, возможность задания цвета позволяет получить примерную карту распределения процессорного времени, используя разные цветовые значения для разных участков кода. При сильных изменениях времени выполнения лучше разобраться в происходящем поможет функция покадрового продвижения (Frame Advance), присутствующая в разных эмуляторах. Например, во FCEUX она доступна по клавише | (с двумя слэшами).
Надо заметить, что color emphasis оказывает разное действие на изображение во всех версиях оригинального PPU (NTSC, PAL, RGB) и на его разных клонах. Эмуляторы также отображают результат немного по-разному. Выбор компонент для усиления зависит от изображения в игре, ведь если оно по большей части синее, и усилена синяя компонента, результат будет плохо заметен. Поэтому при использовании способа на реальной приставке нужно подбирать значения, дающие хорошо заметный результат в конкретной ситуации.
Для уточнения оценки времени выполнения нужно знать несколько цифр. Растр NTSC-версии приставки содержит 261 строку, из них 224 в видимой области экрана. За одну строку процессор выполняет около 114 тактов, на один такт приходится три пикселя в строке. Для PAL-версии это 312 строк и 268 видимых, 106 тактов в строке и 3.2 пикселя на такт процессора.
Через отладчик FCEUX
В отладчике стандартной версии эмулятора FCEUX есть счётчик выполненных тактов и инструкций, с возможностью обнуления. Совместно с точками останова, его довольно просто применить для замера времени выполнения нужного участка кода. Для этого требуется поставить точки останова (Breapoints, Add) в начале и сразу после конца оцениваемого кода. При срабатывания первой точки нужно сбросить счётчик тактов (Reset counters) и дать коду выполниться до второй точки (Run).
Основная сложность применения этого способа в нахождении начала и конца нужного кода в памяти. Ведь трансляторы, в особенности компилятор C, скрывают от программиста реальные адреса, не все из них умеют показывать текущий адрес трансляции, чтобы можно было узнать, где искать полученный код, и в любом случае для получения реальных адресов и указания их в отладчике нужны дополнительные действия. К тому же, при оптимизации размер кода изменяется, что потребует постоянной коррекции адресов. В программе также может использоваться несколько банков памяти, и в нужных адресах может выполняться разный код — например, в одном банке код игрового цикла, в другом код проигрывателя музыки, что вызовет ложные срабатывания точек останова.
Для устранения этих неудобств можно применить маленькую хитрость — в начале и конце нужного кода добавить запись в 'пустой' адрес, изменение которого ни на что не повлияет, а в отладчике поставить условие останова по записи в этот адрес. В карте памяти NES как раз есть подходящий для этих целей диапазон $4020..$5fff, неиспользуемый почти во всех известных конфигурациях, за исключением Famicom Disk System и крайне экзотической NROM-368. Для того, чтобы исключить саму операцию записи в память из подсчёта, нужно после срабатывания первой точки останова пропустить её (Step over). Дальнейшие действия остаются такими же.
Отладчик FCEUX. Счётчик тактов справа посередине, обведён зелёной рамкой.
Специальные эмуляторы
Наиболее удобным способом замера времени выполнения кода являются эмуляторы, в которых предусмотрена такая возможность. К сожалению, её нет в стандартных ветках всех популярных эмуляторов, но она есть в специальных версиях VirtuaNESprof и NintendulatorDX. Незначительным недостатком этого способа является то, что по разным причинам эти эмуляторы могут быть неудобны в качестве основного инструмента для отладки, что вынудит часто переключаться между разным эмуляторами, а наиболее продвинутые возможности одного из них доступны только при использовании определённой среды разработки.
Оба эмулятора используют одинаковый принцип управления счётчиками тактов — через запись любого значения по определённым адресам из обычно неиспользуемого диапазона. Считается только время выполнения кода между этими записями, но не сами записи, так что на результат измерения они не влияют. Разница между эмуляторами заключается в адресах, количестве счётчиков и способе отображения результата.
В VirtuaNESprof всего один счётчик. Результат его работы постоянно отображается прямо на экране и содержит текущий замер, а также усреднённое и максимальное значение замеров. Счётчик запускается записью по адресу $401e, останавливается записью в $401f.
NintendulatorDX предоставляет сразу 16 независимых счётчиков. Они отображаются в окне отладчика (Debug, Disassembly, Timers) и содержат ещё больше информации — текущее, усреднённое, минимальное и максимальное значения. Запуск нужного счётчика осуществляется записью по адресу $402x, где X номер счётчика 0..15, останов записью по адресу $403x.
Помимо 'ручных' счётчиков, в NintendulatorDX реализована система автоматического профилирования кода. Она доступна только при работе в связке с входящей в его комплект модифицированной версией пакета CC65. Для использования этой возможности при компиляции изучаемой программы требуется создать файл с отладочной информацией, используя ключи -g для ассемблера и линкера, а также --dbgfile имя.nes.dbg (имя должно совпадать с именем .nes-файла) для линкера. Этот файл будет автоматически загружен эмулятором. Части программы, время выполнения которых нужно учитывать отдельно, требуется заключить в блоки .scope и .proc. Измерение запускается любой записью по адресу $4041, останавливается записью в $4042, либо по закрытию эмулятора. Результат будет выведен в текстовом формате в файл имя.nes.profiling.txt.
Отображение результата замера в VirtuaNESprof. В середине кадра также виден замер по растру, через выбор монохромного режима.
Отладчик NintendulatorDX. Счётчики тактов справа, на вкладке Timers. Работающий счётчик обведён зелёной рамкой.
4 комментария
При большом желании текущую палитру можно изменить на лету и нормальным образом, но нужно несколько пустых строк на экране. За одну строку можно поменять около двух цветов, на смену всей палитры фона нужно 5-6 строк. И много проблем с таймингами. Для эффектов в демо подойдёт, а для обычных картинок или игр не очень.