Навигация
Наши каталоги
PSP Обои
Реклама
Облако тегов

Блок использует технологию Flash.
Для полноценного просмотра
требуется более новая версия
Adobe Flash Player.


Топ новостей
 

Часть 3 - учимся правильно готовить Cache

автор: SeeD | 2-09-2009, 02:26 | Просмотров: 16359
PSP КодингАвтор: BonifacE

В этой статье мы начинаем давно обещанный курс молодого бойца дебагера и компилятора. Не думайте, что путь будет легким и комфортным, ибо PSP прежде всего закрытая проприетарная платформа. Сиречь без подписанного NDA, с распростертыми объятиями вас тут никто не ждет. Основная проблема, встающая перед начинающими разработчиками homebrew, связана с тем, что разработка на PSP, как и на любой другой консоли, в первую очередь сильно завязана на железо.



Забудьте про ленивое программирование на ПК, где умные IDE и 87 базилионов библиотек на все случаи жизни берут большую часть работы на себя – программирование под PSP это чистый и неприкрытый путь Zen буддиста. Данную статью я посвящу целиком памяти и правильной работе с ней.

Memento L1


Сердце PSP, на котором запускается весь ваш код, - это 32-битный MIPS архитектуры RISC, а конкретнее R4000 с расширенным набором инструкций. Нормальная скорость его работы 222-333 MHz, т.е. гораздо быстрее, чем скорость основной памяти PSP. Если бы процессору приходилось ждать каждый раз, когда в память записываются или из нее считываются значения, его эффективная скорость была бы куда скромнее. В MIPS архитектурах подобные проблемы решаются таким же инженерным способом, как и в обычных х86, а именно наличием у CPU КЭШа (cache). КЭШ - это память небольшого размера, обладающая большей скоростью чтения/записи, чем основная память, и обычно расположенная на одном с CPU чипе. Быстрому и светлому будущему мешает только один жирный минус - размер КЭШа всегда СУЩЕСТВЕННО меньше размера основной памяти. В отличии от х86, структура КЭШа PSP сильно упрощена - есть только 32КВ L1 КЭШ, следовательно об L2 волноваться не придется, но при этом КЭШ разбит на две области по 16КВ.
Первая область – это Дата КЭШ (Data Cache). Этот КЭШ служит как буфер быстрого доступа CPU к данным, когда ваша программа загружает, или сохраняет что-то в памяти. Вторая область – это КЭШ инструкций и служит он, как не сложно догадаться, как быстрый буфер для запуска инструкций вашей программы. Обе области КЭШа подчиняются одним и тем же правилам, но до тех пор, пока вы не используете динамическую генерацию кода, про КЭШ инструкций можете смело забыть.

Часть 3 - учимся правильно готовить Cache


Теперь давайте подробнее рассмотрим принцип работы. Все 32КВ КЭШа разделены на 64 байтные кусочки (чанки) называемые кэш линиями (cache lines). Дословный перевод этого термина на родной Великий и Могучий – убог и не удобен, посему сойдемся на том, что дальше по тексту я буду звать это чанками. Так вот, КЭШ управляется на уровне именно чанков, т.е. если выделить 1 байт в чанке, выделиться весь чанк целиком, т.е. все его 64 байта. Перед тем как CPU начинает читать из памяти, предварительно в поисках нужного куска памяти он обращается к своему КЭШу. Если нужный кусок памяти присутствует в КЭШе, это зовется *cache hit*, при этом чтение нужных данных пройдет всего в пару циклов CPU. Если же нужных данных нет, это *cache miss*, соответственно в силу низкой скорости памяти, считывание нужных данных займет куда больше машинного времени. Однако при *cache miss* данные не просто будут прочитаны из основной памяти, при этом будет найден подходящий чанк КЭШа и в него будут записаны нужные данные. Так что в следующий раз, когда вам понадобятся те же данные, вы вероятнее всего получите *cache hit*. Запись в память подчиняется тем же законам – когда ваша программа пишет в память, она всего лишь пишет в КЭШ, выделяя при необходимости новые чанки. Соответственно последующие записи и чтения этого куска памяти получат *cache hit*.

Чанки КЭШа могут иметь только одно из трех состояний: invalid, clean и dirty. Статус invalid означает, что чанк не содержит никакой полезной информации и ни одна из операций над памятью не вызовет cache hit. Статус clean значит, что чанк содержит точную копию определенного куска памяти. И, наконец, статус dirty означает, что в данный чанк были записаны данные, которые еще не записаны в основную память. Так как КЭШ очень мал в сравнении с объемом основной памяти, каждый раз, когда вам нужно выделить новый чанк, необходимо очистить какой-то другой. Если чанк, который вы переписываете, имеет статус invalid – все супер. Если clean – придется затереть существующие данные. И, наконец, если статус dirty – необходимо переписать данные из этого чанка в основную память и только после этого данный чанк можно использовать.

Ну вот нудная теория подошла к концу и начался экшОн - приготовьтесь к эпическому хоррор- блокбастеру с вопящим благим матом названием «Где @#$ мои данные, Чувак?». Начнем с того, что обычно вы не знаете, в какой момент времени данные из dirty чанков будут реально записаны в память. Грубо говоря, данные из них попадут в память только тогда, когда такие чанки будут выделены под запись в них новых данных, т.е. данный момент может и не наступить…

Вознесемся же теперь с глубины L1 до вещей более насущных и менее железных, т.е. собственно к нашей программе. А с точки зрения программы такой проблемы как будто бы и нет, т.е действенного способа узнать что «в Багдаде не все спокойно» у нас нет. Зато теперь мы знаем, что CPU работает со своей собственной копией памяти и, хотя она и не всегда соответствует основной памяти, пока CPU единственный пользователь системной памяти, все вроде как хорошо. И тут ваши глаза застилает кровавый туман и, предчувствуя худшее, вас осеняет – «А ведь CPU далеко не единственный, кто юзает основную память! Ведь CPU так же общается через нее с ME и GE!». И вот только тут вы начинаете ощущать, с чем столкнулись. Тут же вспоминаются живущие своей жизнью вершины, текстуры из которых выпадают случайные куски и другие страшные артефакты, которые нещадно рвали вам мозг своим присутствием на экране, но каждый раз мистически исчезали стоило вам запустить дебагер. Звучит тревожная музыка, нервы на пределе, кульминация и… начинаются суровые рабочие будни.

В мире больших настольных систем с этим борются специальными методами, в миру носящих название снупинг (cache snooping). Обычно это означает, что специальная железка мониторит все операции с памятью и проверяет, чтобы КЭШ всегда соответствовал всем изменениям в памяти. Соответственно эта же железка следит за своевременным внесением данных из dirty чанков в память. Увы, но MIPS, установленный на PSP, к сожалению, ни снупингом, ни грамотной когеренцией памяти похвастаться не может. Однако решение данной проблемы все-таки есть. Инженеры Сони сделали хитрый финт ушами и дали возможность простым разработчикам использовать основную память напрямую, минуя КЭШ. И так к основной памяти можно обратиться двумя способами. Первый - это через указатель 0х40000000 на кэшируемое адресное пространство, и второй - это указатель 0х04000000 на некэшируемое адресное пространство. По сути дела оба указателя ссылаются на один и тот же железный кусок памяти, но в зависимости от того какой именно из этих двоих указателей вы используете, CPU будет пропускать операции с памятью через КЭШ либо, минуя его, будет обращаться к памяти напрямую. Тут, однако, тоже все не совсем радужно, ведь существует такая проблема как обращение к кэшируемой памяти через некэшируемое адресное пространство, и наоборот – обращение к некэшируемому адресному пространству через кэшируемый указатель. Нет, ваша PSP не сгорит синим пламенем при подобной работе с памятью, но полный спектр заказных ужасов, описанных выше, наверняка обрадует вас свои присутствием.

Осталось только заметить, что для эффективной борьбы с ошибками синхронизации КЭШа существует два пути. Первый – предложенный все теми же хитрецами из Сони предлагает вам не использовать операции с памятью вообще, ограничившись первоначальной загрузкой нужных данных. Взамен этого, все операции проводить на уровне регистров VFPU, а при необходимости передать информацию между CPU, GE и ME командой mov. Плюсы такого подхода очевидны – всегда правильный КЭШ и общее повышение производительности вашей программы. Минус у этого способа весьма спорный, но в нашем случае весьма весомый – все операции с данными придется писать на чистом ассемблере. Для гуру, знающих allegrex, сие не составит большого труда, также в одной из следующих статей по программированию VFPU я наглядно покажу что это на самом деле куда проще, чем кажется на первый взгляд. Однако, прекрасно понимая, что делает чистый ассемблер с незамутненным сознанием начинающих консольных программистов, пока мы остановимся на способе номер два. А именно на файле прототипов из PSPSDK. Данная библиотечка функций предоставляет нам следующие действенные методы для борьбы с КЭШем:

void sceKernelDcacheWritebackAll(void)

После вызова – все dirty чанки незамедлительно будут сдамплены в память и получат статус clean. В большинстве программ вся работа с КЭШем этой функцией и ограничивается. Достаточно просто грамотно вызывать ее в нужные моменты.

void sceKernelDcacheWritebackInvalidateAll(void)

Тоже самое, что и предыдущая функция, но при этом еще проверяет весь кэш на правильность статусов чанков.

void sceKernelDcacheWritebackRange(const void *p, unsigned int size)

Дампит только выбранный кусок КЭШа. Функция эффективна на маленьких диапазонах. При необходимости проверить большие диапазоны КЭШа – проще дампить его целиком.

void sceKernelDcacheWritebackInvalidateRange(const void *p, unsigned int size)

Тоже самое что и предыдущая функция, но при этом еще проверяет выбранный диапазон КЭШа на правильность статусов чанков.

sceKernelDcacheInvalidateRange(const void *p, unsigned int size)

А вот эта достаточно опасная штука. Смысл ее в том, что все dirty чанки сбрасываются для последующей записи в них значений из памяти. Применять для быстрого построения эффективного КЭШа, но только при условии, что вы точно знаете - dirty чанков в КЭШе нет.

На этом разбор полетов работы КЭШа окончен. Теперь, пожалуй, перейдем собственно к рассказу о типах памяти имеющейся в PSP.
Итак, на борту PSP имеются: системная память она же RAM объемом 32МВ со скоростью чтения текстур около 50MB/s, и видео память, она же VRAM объемом 2МВ, скоростью чтения текстур около 500MB/s. Кстати не следует рассматривать видеопамять как супер-мега-закрытую часть видеоподсистемы, как скажем на ПК. На PSP это вполне себе обычная линейная память с вменяемым адресным пространством. Как видно по скорости чтения текстур, видеопамять куда как быстрее чем основная память и прямо просится для хранения различной полезной инфы типа текстур, вертексных буферов и т.д. Однако есть пара нюансов, собственно коли уж читаете эту статью, то привыкайте – особенности и не всегда хорошие есть у всего что я затрагиваю smile.

Начнем мы с реального размера видео памяти. Чтобы его посчитать, необходимо понимать, как любая видеоподсистема строит кадр (видеокарты на ПК исключением не являются, посему все ниженаписанное применимо и к ним). Чтобы что-то отобразить на экране, нужен буфер понятный железу, в котором мы предварительно строим наше изображение. Такой буфер называют буфером кадра (frame buffer). Издревле повелось, что эффективно иметь не один буфер, а целых два. Т.е. так как считывание из буфера тоже не со скоростью света происходит, то пока заполненный буфер считывается и заполняется экран – мы преспокойно заранее забиваем второй буфер новым кадром. Поочередно меняя буфера местами, мы и получаем динамическую смену картинки на экране. Такой подход зовется double buffering, соответственно первый буфер зовется front buffer, а второй back buffer. Так же для расчета геометрии есть еще один буфер, служащий для промежуточного хранения порядка отображения – знаменитый Z buffer. Каждый из этих трех буферов на PSP, как и на больших платформах, является 32-битным (можно использовать и 16-битные). Итак, начнем считать. Разрешение у нашей любимой PSP целых 480*272 пикселя, т.е. каждый буфер занимает 480*272*4 байта. Посчитаем, сложим и получим, что на хранение буферов в видео памяти должно быть выделено 1566720 байта, и того свободных у нас остаются всего около 530 Кбайт. «Негусто» - скажете вы? Отнюдь! Все дело в том, как это свободное место использовать.

Дело в том, что у GE так же существует специфический тип КЭШа – текстурный КЭШ, необходимый для промежуточного хранения текстур во время заполнения буфера кадра. Разработчики игр тоже люди, которым не чуждо желание халявы, а посему одинаковые текстуры на различных объектах можно встретить даже в самых топовых играх на ПК с сотнями мегабайт видеопамяти. Что уж и говорить про нашу маленькую консоль? Ну а теперь еще вспомните, что текстура применяется обычно к группе полигонов, а не к единичным экземплярам. Соответственно дабы избежать чтения текстуры из памяти для каждого полигона, задача хранения текстуры во время сборки и текстурирования группы полигонов в буфере кадра и возлагается на текстурный КЭШ. Но, опять это НО…

Размер текстурного КЭШа PSP, в отличие от больших платформ, где он измеряется мегабайтами, всего 8Кбайт. История повторяется, PSX-PS2-PSP, наличие такого маленького текстурного КЭШа всегда являлось особенностью консолей Сони. Если посчитать, то получается, что в текстурном КЭШе мы можем разместить текстуры следующих размеров: 64x32х32 бит, 64x64х16 бит, 128x64х8 бит и 128x128х4 бит. Еще во времена разработки PSP было объявлено, что консоль умеет работать с текстурами сжатых форматов s3ct, т.е. со знаменитыми DXT1/DXT3/DXT5. Но опять мы натыкаемся на хитрые инженерные ходы девтехов Сони. Сжатые текстуры в текстурном КЭШе распаковываются! Т.е. преимущество сжатых текстур в нашем случае только в уменьшенном трафике между памятью и GE. Отсюда примите за аксиому, которой надо просто верить – самый главный формат текстур нашей консоли это 64х64х4 бит палитровые, то есть содержащие не цвет пикселя, а индекс на clut таблицу в видеопамяти.

Именно на них достигается оптимальный размер передачи данных между памятью и GE и при этом в текстурном КЭШе их умещается целых две. Но чем же так важен текстурный КЭШ и правильное его заполнение? Ответ из вышесказанного прост – если текстура не будет умещаться в текстурный КЭШ, то при текстурировании каждого полигона, GE снова и снова будет обращаться к памяти в поисках нужной текстуры, тратя на это бесценные циклы своего аппаратного времени. Такой ужас в простонародье зовется КЭШ трэшем (cache trash) и этот маленький нюанс способен играючи поставить на колени ваш замечательный движок. Ожидайте падения фрэймрэйта самое грубое раз в 5-10. Что же делать, если обязательно надо большую текстуру? В таком случае предлагаю вам порезать ее на кусочки (тайлы). Так же упомяну, что PSP умеет тягать текстуры вплоть до 512х512, но в силу кэш трэша забудьте об этом, если вы работаете с риал-тайм графикой.

Ну вот на сегодня и все. Приятного вам кода. Встретимся в следующей статье, которая будет полностью посвящена работе с видеосистемой. Все вопросы/пожелания о том, что вы хотите увидеть в новой статье, оставляйте в теме на форуме.
Remember your brightest future is completely up to you.


Ключевые теги: кодинг, cache
 
  • Вы не вошли или уже голосовали
  • Рейтинг: 22 (Голосов: 24)
 
Новинки
Комментарии