8.1. Анализ и оптимизация программы
Каждую программу можно усовершенствовать. Можно попробовать уменьшить текст программы, уменьшить размер выполняемого файла, улучшить структурированность, модульность и так далее. В данном случае мы попытаемся повысить скорость рендеринга — уменьшить время формирования кадров изображения.
Как оптимизировать программу по быстродействию? Для этого необходимо выполнить анализ работы программы. В результате анализа нужно обнару-
жить операции, которые обуславливают быстродействие программы. Пос^ того как будут найдены критические места программы, можно сделать выв^ ды относительно конкретных путей оптимизации.
Для измерения времени выполнения операций в программе для Window можно воспользоваться функцией API GetLocalTime :
Необходимо предупредить, что миллисекунды измеряются не очень точно, поэтому для повышения точности измерения для некоторой отдельной операции можно делать цикл из многих (сотен, тысяч, ...) одинаковых операций (если вспомогательные операции создания цикла сами по себе не длительные). Кроме того, различные сеансы измерений могут давать различные значения, поэтому необходимо как-то усреднять результаты. Понятно, что все] измерения должны выполняться на одном и том же компьютере и обязатель-; но в одинаковых условиях выполнения программы. Также необходимо учи- ? тывать, что в полночь измерение времени может дать ошибку, — если переход на 0 часов случится в ходе измерений. Впрочем, я и не рекомендую вам по ночам засиживаться за компьютером — ночью надо спать.
Теперь приступим к анализу программы studex34. Вся работа по созданию объектов, их отображение в различных ракурсах и уничтожение объектов делается в теле функции DrawstudyExampie. Сделаем измерения времени ос-
новных операций. На создание объектов, открытие контекста, подготовку битмапа двойного буфера и создание Z-буфера расходуется менее десяти миллисекунд (измерения с точностью до процентов секунд дают 0.00). Таким образом, в ходе дальнейшего анализа сосредоточимся на цикле создания 361 кадра.
Как измерить время, расходуемое во всех 361 кадрах на выполнение функции ciearMyZbuf f er () ? Это сделаем способом, который можно назвать "способом контрольно-измерительного стенда". Такой "стенд" можно сделать на основе текста нашей программы, например, следующим образом:
//далее вычисляем разность в секундах и выводим результат
Разумеется, подобный способ измерений можно считать корректным лишь тогда, когда время выполнения функции CiearMyZbuffer () значительно больше, чем время выполнения операций организации цикла по j.
Время выполнения 361 операции ciearMyZbuffer составляет в среднем 2.1 секунды. Аналогично можно сделать измерения для PatBit— 0.06 сек., SetCameraviewMatrix — 0.00 сек., BitBit — 0.5 сек. Однако делать измерения времени для цикла отображения объектов таким "стендовым" способом нельзя. Для корректного создания изображения обязательно выполнение всех подготовительных операций в полном объеме. Для измерений времени здесь можно предложить другой способ. Суть его такова. Вначале измеряем время выполнения полного цикла создания изображений:
5
i
А потом исключаем анализируемую операцию:
и измеряем время выполнения без нее. Полный цикл — 807 сек., без исключенной операции — 2.6 сек. Назовем такой способ "временным исключением". Необходимо заметить, что цифру 2.6 можно было бы получить и иначе, если от времени полного цикла вычесть уже измеренное время других операций. Однако способ "временного исключения" предназначен в первую оче-j редь для тех случаев, когда измерение всех составных операций затруднено или не нужно. Продолжим измерения дальше.
Как мы видим, в цикле создания кадров на подготовительные операции расходуется мало времени в сравнении с отображением объектов. В ходе отображения объектов выполняется много операций. Какая из них самая длительная? Осуществим поиск способом "временного исключения". Однако для применения этого способа есть много ограничений. Сформулируем основные условия корректного использования данного способа.
1. Временно исключить можно не любую операцию, а только ту, отсутствие которой не нарушает логику работы программы. Другие операции должны выполняться в полном объеме и в той же последовательности. Это главное условие. Остальные условия можно сформулировать как следствие.
2. Необходимо быть внимательными, если вы работаете с оптимизирующим компилятором. Он может сделать такие изменения в программе во время компиляции, о которых мы и не подозреваем. Мы можем исключить одну операцию — а компилятор исключит еще несколько и никак нам об этом не сообщит. Это приведет к иллюзии значительной роли временно исключенной операции.
3. Нельзя изменять стратегию использования виртуальной памяти (если это специально не анализируется). Например, в функции DrawstudyExampie нами не используется ни одна из файловых операций. Однако в ходе выполнения этой функции, программа может часто обращаться к диску. Это может быть в случаях, когда открываются значительные по объему массивы, и операционная система делает перераспределение виртуальной памяти между оперативной памятью (RAM) и диском. А в данное время мы временно выключили эту операцию, и обращения к диску прекратились — это может быть свидетельством того, что отныне все массивы целиком размещаются в RAM. Последнее может привести к ускорению выполнения программы, поскольку обращение к RAM осуществляется намного быстрее, чем к диску. Кроме того, когда размера RAM недостаточно для полной программы, то даже уменьшение объема кода при исключении может привести к тому, что программа будет работать быстрее — так как отныне все размещается в RAM. Однако в этом случае уменьшения времени не пропорционально времени выполнения исключенной функции, и это не позволит рассчитать ее вклад в общее время выполнения программы. Таким образом, необходимо пользоваться правилом: объем оперативной памяти должен быть достаточным как для полной программы, так и программы с исключением.
Способ временного исключения отдельных операций можно трактовать так: если после исключения некоторой операции время выполнения программы уменьшается не существенно, то эту операцию не нужно оптимизировать в первую очередь. Однако если в результате анализа мы и обнаружим некоторую длительную операцию, то это еще не означает, что ее можно ускорить.
Продолжим дальше анализ программы. Временно выключить функцию MyPoiygon нельзя, поскольку тогда будет рисоваться только шар, а это означает другой порядок заполнения пикселами Z-буфера и растра битмапа двойного буфера. По аналогичной причине нельзя временно выключить функцию
Sphere: : Draw (HDC hdc). А ЧТО же ТОГДа МОЖНО?
Рассмотрим функцию SetPixMyz. Если ее исключить, то прекратится запись пикселов в оба растра — Z-буфера и растра битмапа. Однако те функции, ко-
торые остались, выполняются так же, как и до исключения. Временно ш ключим setPixMyz. Результат— 57.5. То есть, из 807 секунд почти все врем расходуется на запись пикселов.
Составная часть функции SetPixMyz — вызовы функции SetPixel. Времен^ исключим ее. Результат— 70 секунд. Результаты временных исключен^ отобразим следующей диаграммой (рис. 8.3).
Рис. 8.3. Диаграмма исключения операций
По диаграмме исключения можно рассчитать, сколько времени расходуется» программе на выполнение отдельных операций:
Время выполнения функции SetPixel:
Необходимо учитывать, что функция SetPixel — вложенная по отношению |
К SetPixMyz.
поэтому целесообразно будет для анализа SetPixMyZ рассматривать долю времени, не относящегося к вызову setPixei:
Таким образом, мы уже можем рассмотреть результаты измерений для некоторых отдельных операций:
Этот перечень неполон, можно анализировать еще некоторые функции, однако уже ясно, что основная причина низкой скорости — это использование функции setpixei. Ее мы никак не можем изменить, ибо это функция API Windows. Но можно попробовать обойтись без нее.
Один из способов работы с растром — непосредственный доступ к памяти, хранящей растровый массив. Такой способ достаточно известен. Он использовался при разработке почти всех быстрых графических программ для когда-то популярной операционной среды MS-DOS. Среди функций MS-DOS предусмотрена функция рисования пиксела на экране, но она работала так же медленно, как и функция API Windows SetPixei. Поэтому для создания изображений на экране часто использовались операции непосредственной записи в видеопамять. В операционной системе Windows обращение прикладных программ к видеопамяти запрещено, а вся растровая графика основывается на понятии контекста графического устройства. Через контекст мы рисуем на экране, через контекст— в растрах битмапов. Кажется, это и все. Однако разработчики Windows предусмотрели еще одну возможность работы с растрами — операции с растрами в формате DIB (Device Independent Bitmap).
Можно создавать растр, не привязанный к какому-то графическому устройству по формату пикселов, а самое главное — предусмотрен доступ к растру по указателю на массив в памяти. С этим массивом можно производить любые операции, поскольку становится известным его адрес в виртуальной памяти. В том числе и выполнять операции над отдельными байтами и битами,; которые представляют пикселы растра. Это открывает широкие возможности для создания собственных графических библиотек. Текст программы studex35. срр:
В программе использована одна из функций API Windows для работы с растрами DIB. Это функция stretchDiBits, которая осуществляет вывод растра из памяти по адресу pRastBuf в определенный контекст. Растровый буфер DIB здесь в формате 24-битного цвета— это позволяет достаточно просто моделировать различные интенсивности отражения света для цветных объектов. Для корректности сравнения работы обеих программ (studex34,35) все испытания следует производить в видеорежиме True Color 24 бит на пиксел.
В 24-битном режиме цвет каждого пиксела определяется тройкой байтов RGB. Как раз эти байты и записываются в память функцией setPixRastrMem.
Скомпилируйте и проверьте работу программы studex35. Полный оборот камеры в ней делается за 88 секунд в отличие от studex34, где полный оборот составлял 807 секунды. Таким образом, создание одного кадра выполняется в среднем за 88/361 = 0.24 секунды. Благодаря замене функции SetPixel нашей собственной функцией SetPixRastrMem достигнуто уменьшение времени рендеринга более, чем в 9 раз. И это несмотря на то, что функция stretchDiBits работает медленнее, чем BitBit.
Необходимо отметить, что функция SetPixRastrMem предназначена только для 24-битных растров, a SetPixel корректно работает во всех цветовых форматах, поэтому она и работает медленнее.