Если этот результат получен для программы, которую не предполагается запускать на слабых машинах, то ее оптимизацию следует считать законченной и можно сформулировать такой постулат: «При отображении картинки на экран не следует стремиться к частоте вывода кадров, большей частоты кадровой развертки». Но с одной стороны, игра — это не только отображение спрайтов на экран. При ее создании необходимо предусмотреть использование части вычислительной мощи процессора также на физическую модель и логику взаимодействия объектов. С другой стороны, разрешение 320x200 точек было характерно для ПК моделей 286 и 386 с тактовой частотой 8—40 МГц, но сейчас более адекватным следует считать разрешение 800x600 или 1024x768 точек. Однако с таким разрешением мы работать не будем, так как использовать в этом случае Борланд Паскаль довольно неудобно из-за сегментированной модели памяти реального режима. При высоком экранном разрешении целесообразно выбрать защищенный режим, к чему мы еще вернемся в последних статьях цикла, а пока продолжим работу с разрешением 320x200 точек.
Прежде всего, следует сказать, что язык Ассемблера не очень подходит для написания крупных программ. При использовании языка высокого уровня ПО получается компактнее, понятнее и содержит меньше ошибок, поэтому целесообразно переписать на языке Ассемблера лишь некоторые критичные по времени исполнения фрагменты. Какова их доля? Предполагается, что она колеблется от 1—5 до 10—15% от общей длины текста. При выборе этих фрагментов следует придерживаться второго правила, гласящего: «В оптимизации нуждается только тот фрагмент программного текста, который занимает значительную долю времени выполнения». Как найти такие фрагменты? Это можно сделать несколькими способами: с помощью профайлера, путем проведения анализа вложенности циклов, последовательно отключая модули и наблюдая, как это скажется на времени выполнения программы. Работу с профайлером мы рассматривать не будем, а воспользуемся двумя другими методами, тем более что они прекрасно дополняют друг друга. С одной стороны, оценка на основе анализа текста может дать лишь рекомендации, которые нужно проверить, а с другой, далеко не все модули можно отключить без потери работоспособности программы.
Итак, начнем с анализа. Оценим, какие части программы выполняются чаще всего, — возможно, ими окажутся циклы с наибольшей вложенностью. Например, в нашей программе есть основной игровой цикл, выполняемый при отрисовке каждого кадра. Значит, все, что лежит за его пределами, можно не рассматривать. А внутри самого цикла вложений не так уж и много: вывод в буфер фона спрайтов и текста, а также переброска данных из буфера на экран и их синхронизация. В первую очередь наиболее глубокий цикл существует при выводе спрайта: внутри основного игрового выполняется цикл по спрайтам, а внутри него — вложенный цикл вывода точек по горизонтали и вертикали. Если иметь 200 спрайтов размером 20x20 точек, то можно получить 80 тыс. повторений на каждый кадр основного игрового цикла. Во вторую очередь идет вывод текста: отображение строки происходит посимвольно, а для показа символа опять же используется вложенный цикл. Таким образом, вложенность здесь такая же, как и при выводе спрайтов, только сами циклы короче, поэтому при 10—30 символах размером 8x8 точек цикл повторяется 700—2000 раз. Казалось бы, не так уж много, но современные суперскалярные процессоры, способные выполнять несколько команд за один такт, очень плохо переносят вызовы процедур, поэтому следует запомнить еще одно правило: «Не нужно размещать вызов процедуры или функции во вложенном цикле, а лучше перенести внутрь цикла фрагмент программного текста из процедуры». Иногда это может дать выигрыш в несколько раз. Мы не будем сейчас следовать этому принципу, а лучше минимизируем количество выводимого в кадре текста — тогда о его оптимизации можно не заботиться.
Теперь все рассмотрено, и список циклов глубокой вложенности исчерпан. Однако при переброске фона в буфер или изображения на экран приходится последовательно копировать 64 000 точек. Это также цикл, хотя он и скрыт внутри процедуры move. Значит, анализ привел нас к необходимости минимизировать количество выводимого текста, для чего мы уберем с экрана слова «Демонстрационная программа» и оптимизируем процедуры BackBufferToScreen, PutSprite, ScreenBufferToScreen.
Переходим к следующему этапу: отключим «ненужные» процедуры. В основном игровом цикле оставим только вызов процедур PutText и ScreenBufferToScreen (иначе мы не увидим на экране величины FPS), а все остальное временно «закомментируем». Запустим урезанную программу и запомним для дальнейшего сравнения величину FPS.
Сначала перепишем Screen BufferToScreen на языке Ассемблера (листинг 1). Здесь воспользуемся командой rep movsX (где X может быть b, w или d — при этом соответственно команда за один раз копирует по одному, два или четыре байта). Она служит для копирования одной области памяти в другую. Процедура move использует команду rep movsb. Копировать по 4 байта за один раз могут только процессоры, начиная с 386-го, о которых Борланд Паскаль ничего не знает. Команда rep movsd на встроенном Ассемблере Борланд Паскаля будет выглядеть так: db $66 rep movsw. Еще одна тонкость, связанная с использованием языка Ассемблера в Борланд Паскале: все статические переменные хранятся в сегменте данных, адресуемом через регистр ds. Поэтому, если необходимо переопределить последний, то его содержимое следует запомнить, а затем восстановить. Кроме того, изменение ds следует провести как можно позже, ведь после нее обращение к статическим переменным программы будет уже невозможным.
Затем восстановим переброску фона, вновь запустим программу и запомним FPS. Аналогично перепишем BackBufferToScreen и снова измерим FPS. Если в вашем ПК установлен современный процессор (Pentium II, Celeron и выше), то вас ждет разочарование, — практически ничего не изменится. Это связано с влиянием кэш-памяти, в которую целиком помещаются оба буфера. Как только мы подключим вывод спрайтов, кэш-память переполнится, и наше усовершенствование не будет незамеченным.
Далее приступим к оптимизации PutSprite. Измерим FPS до и после изменений, показанных в листинге 1. Приведенный вариант, конечно, не идеален. Например, при наличии процессора Celeron, Pentium II или Pentium III замена loop @l2 на последовательность команд
dec cx
jnz @l2
приведет к незначительному ускорению работы программы. Тем не менее мне кажется, что не следует оптимизировать программу под конкретный процессор — зачастую читаемость программного текста важнее некоторого увеличения скорости.
Если в ПК установлен быстрый процессор (обеспечивающий FPS > 140), спрайты через некоторое время могут остановиться. Это связано с тем, что вычисленная величина приращения координат стала меньше 1/2 и была округлена до 0. Восстановите кадровую синхронизацию — оптимизация закончена!
Есть способ добиться правильного движения спрайтов и при высоких FPS: перейти от целых координат к дробным. Этот же подход позволит обеспечить и различную скорость движения разных спрайтов. Кстати, «дробные» не обязательно означают «с плавающей точкой». Можно воспользоваться и представлением чисел с фиксированной точкой. Таких чисел стандартного типа нет, но их легко получить из целых. Например, мы знаем, что диапазон изменения координат не превышает 0—320 и для его показа достаточно 9-разрядных чисел (или 10-разрядных, если число со знаком). Давайте считать, что единица соответствует 1/64 размера пиксела, тогда старшие 10 разрядов будут представлять целую часть координаты на экране, а младшие 6 — дробную. Чтобы перейти от целых чисел к числам с фиксированной точкой, достаточно при определении координат и приращений умножить числа на 64, а при использовании — разделить их на 64. Изменения в модуле Sprites и в основной программе даны в листинге 2. Вместо деления или умножения на число, являющееся степенью два, можно применить сдвиг, что будет более быстрой операцией.
Выводить спрайты мы уже научились, теперь следует подумать о том, как наша программа будет «взаимодействовать» с пользователем. В следующих статьях мы расскажем о том, как можно использовать мышь и клавиатуру.