Сообщения без ответов | Активные темы Текущее время: 28 апр 2024, 01:57



Ответить на тему  [ Сообщений: 30 ]  На страницу Пред.  1, 2
 Как создать эмулятор? 

Нужно оно кому-то?
Да 89%  89%  [ 25 ]
Нет 11%  11%  [ 3 ]
Всего голосов : 28

 Как создать эмулятор? 
Автор Сообщение
Сообщение 05 дек 2010, 13:03
Профиль

Зарегистрирован:
14 ноя 2007, 11:19
Сообщения: 370
Итак, давайте более подробно рассмотрим «динамическую рекомпиляцию»
Как я уже писал ранее, скорость достигается за счет того, что создается «кеш» ранее откомпилированного кода, любая программа это в конечном итоге цикл, т.е. например при загрузке уровня в каком-нибудь файтинге наступает момент когда весь код, используемый в данный момент времени становится откомпилированным т.е. происходит только его исполнение, из этого стоит сделать вывод чем лучше откомпилирован код, тем выше скорость его исполнения:
Ну для начала рассмотрим пример из PCSX:

Код:
void execute() {
   void (**recFunc)() = NULL; /* указатель на вход в откомпилированный код */
   char *p;

   p = (char *)PC_REC(psxRegs.pc);
   if (p != NULL) recFunc = (void (**)()) (u32)p; /* собственно получения указателя на точку входа в откомпилированный код */
   else { recError(); return; }

   if (*recFunc == 0) { /* в случае если код еще не скомпилирован вызов компилятора */
      recRecompile();
   }
   (*recFunc)(); /* исполнение кода */
}


К сожалению, компилятор в PCSX из себя представляет мало интересного, ибо никаких мало-мальских оптимизаций при компиляции кода не использует.

Итак какие же основные способы оптимизации компилируемого кода:
В первую очередь это конечно «регкешинг», т.е. мы загружаем регистр из памяти в регистр процессора под который пишем компилятор, и используем его как можно дольше, т.е. пример:

Код:
Add r1, r2, r3
Add r1, r1, r2


Если компилятор встретит такой код он загрузит регистр r2 в случае x86-проца, например в EAX, далее r3 в ECX, потом сложит регистры и загрузит результат сложения в EDX, далее при компилировании второй инструкции сложения уже EDX сложит с EAX, и в конце блока измененные регистры и только измененные загрузит обратно в память, заметьте R1 в EDX ни разу из памяти не был считан ибо его значение нас не интересует:

Далее пример кода откомпилированного кода:

Код:
MOV EAX, dword ptr [R2]
MOV ECX, dword ptr [R3]
LEA EDX, [EAX + ECX]
ADD EDX, EAX
MOV dword ptr [R1], EDX


Итог, 4 инструкции вместо двух в изначальном коде, много это или мало? Сложно сказать, но если встречается много арифметических вычислений и минимум обращений к оперативной памяти может быть и меньше чем в изначальном коде.

Кроме, «регкешинга» при компиляции кода применяется также сверстка констант, дело в том что за постоянную длину команд необходимо платить и зачастую высокую цену, не соизмеримую с выгодой от постоянной длины команды, т.е. чтобы загрузить 32бита в регистр в MIPS необходимо обычно две команды:

Код:
LUI R1, IMM_HIGH
ADDI R1, R1, IMM_LOW
ADD  R2, R2, R1

Что же сделает компилятор кода встретит такой кусок кода,
На первой команде он пометит регистр R1 как константу и присвоит ему значение IMM_HIGH, на второй команде он проверит R1 не константа ли он и соответственно убедившись что да, прибавит к константе IMM_LOW, и только на третьей команде он впервые что-то запишет в память для последующего исполнения:

Итог весь код будет представлен одной инструкцией:

Код:
ADD dword ptr [R2],  (IMM_HIGH + IMM_LOW)


Не нужно думать, что я привожу очень удобные для компиляции куски кода, в реальном коде бывает и не такое, по всей видимости это следствие отсутствия достойных компиляторов для архитектур отличных от x86.

Далее, третий способ оптимизации, это так называемая линковка блоков, суть ее проста, но реализовать всегда не так просто много подводных камней, поэтому применяется далеко не всегда, и по моим собственным экспериментам не дает существенного выигрыша в скорости в сравнении с ценой использования, но тем не менее не сказать двух словах о данном методе я не могу: Как мы все прекрасно знаем львиная доля переход как условных так и безусловных в коде это переход на известный адрес, т.е. вызов ф-ии, это не что иное как переход на точку входа в ф-ию, соответственно, удобно встретив в конце блока переход на константный адрес, почему бы сразу не собрать следующий блок и слинковать переход в одну инструкцию вместо проверки на то существует ли данный блок и дальше во время исполнения сразу переходить куда нам нужно одной инструкцией, более того в следствии того что длина команд опять же постоянна, часто бывает код в котором переход указывает на инструкцию перехода, потому что ну нельзя такой далекий переход сделать за один раз, только разбив на два, а то и на три, в итоге мы можем целый блок скипануть а перейти сразу на нужное место, но как я сказал выше куча подводный камней, основной из них это инвалидация кода, т.е. представьте если код обновился, в оперативной памяти нашей системы, мы его в буфере тоже пометили как инвалид, а вот пометить инвалидным его для всех блоков которые на прямую переходят на этот блок без проверок весьма проблематично, нельзя сказать что это совсем невозможно, но нельзя сказать что овчинка стоит выделки, в общем всегда решается по месту нужно или нет.

Бывают иные более сложные способы оптимизации, но они уже узкоспециализированы, например: при оптимизации фпу команд, можно попробовать отслеживать, а нет ли однотипных парных команд сложения, вычитания, умножения и т.д. т.е. пробывать собирать эти пары и выполнять нашими любимыми SSE1,2,3…, но это довольно сложно и крайне сложно отлаживаемо, но возможность такая существует

Ну думаю для начала о динамической рекомпиляции достаточно, если есть вопрос буду рад ответить:


Сообщение 05 дек 2010, 23:16
Интересно было бы узнать насчёт динамической рекомпиляции поподробнее, код генерируется напрямую в машинные коды x86? И не портятся ли регистры за счёт работы самого рекомпилятора?


Сообщение 06 дек 2010, 01:55
Профиль WWW
Основатель сайта
Аватара пользователя

Зарегистрирован:
21 июл 2007, 15:40
Сообщения: 2234
Откуда: Москва
Признаюсь, я "поплыл", начиная с твоего предыдущего сообщения. Хотелось бы более простых примеров, может на псевдо коде каком, чтобы яснее было.
Сейчас по динамической рекомпиляции есть такой вопрос. Ты пишешь, что декодированные команды собираются в буфер и потом оптимизируется. Как я понимаю, для этого пишется отдельная функция. Так вот, как эта функция узнает, как ей оптимизировать текущий буфер команд? Ведь сочетаний команд может быть великое множество.


Сообщение 06 дек 2010, 05:27
Профиль

Зарегистрирован:
14 ноя 2007, 11:19
Сообщения: 370
Гость писал(а):
Интересно было бы узнать насчёт динамической рекомпиляции поподробнее, код генерируется напрямую в машинные коды x86? И не портятся ли регистры за счёт работы самого рекомпилятора?


Да конечно, напрямую в машиные коды, по общепринятым правилам, нельзя изменять ebx, esi, edi, ebp регистры, соответственно их обычно сохраняем еще на входе в "recExecute".

-- 06 дек 2010, 07:30 --

Eevon писал(а):
Ты пишешь, что декодированные команды собираются в буфер и потом оптимизируется.


Не не, всю сборку кода обычно делают за один проход, максимум два, да и то первый это обычно анализ блока, на предмет какие регистры чаще юзаются, и где адрес последнего использования каждого из регистров, вечером постараюсь придумать побольше примерчиков, для большего понимания


Сообщение 07 дек 2010, 01:10
Профиль ICQ
Аватара пользователя

Зарегистрирован:
21 авг 2007, 14:55
Сообщения: 301
Eevon писал(а):
Признаюсь, я "поплыл", начиная с твоего предыдущего сообщения. Хотелось бы более простых примеров, может на псевдо коде каком, чтобы яснее было.
Сейчас по динамической рекомпиляции есть такой вопрос. Ты пишешь, что декодированные команды собираются в буфер и потом оптимизируется. Как я понимаю, для этого пишется отдельная функция. Так вот, как эта функция узнает, как ей оптимизировать текущий буфер команд? Ведь сочетаний команд может быть великое множество.

Признаться, я тоже "поплыл".
Wind, а может есть возможность как-то на картинках изобразить что и откуда берется?)

_________________
Копи/паст в подписи.


Сообщение 07 дек 2010, 19:45
Профиль

Зарегистрирован:
14 ноя 2007, 11:19
Сообщения: 370
Вот думал думал как бы это попонятней все описать и что-то толком ничего не придумал, ну да ладно, давайте попробую для начала регкешинг объяснить более понятно:

Обычно для регкешинга создают карту памяти регистров, примерная карта памяти:

Код:
struct
{
bool mapped;
bool write;
int R3000A_REG;
} _x86reg;

_x86reg x86reg[REG_COUNT];



Ну и ф-ии для создания, сброса карты памяти:

Код:
void MapReg(u32 R3000A_REG, u32 mode) /* где первый параметр номер регистра который необходимо "замэппить", второй параметр указывает нужно ли загрузить регистр из памяти и будет ли в него в дальнейшем произведена запись */
{

for (int i; i < REG_COUNT; i++)
{
if((x86reg[i].mapped) && (x86reg[i].R3000A_REG == R3000A_REG)) {
If(mode & modeWrite) x86reg[i].write = true;
return i; // если наш регистр уже занесен в карту памяти то просто возвращаем номер регистра в котором он находится
}
}

// сюда попадем если нету в нашей карте памяти необходимого регистра

for (int i; i < REG_COUNT; i++)
{
if(x86reg[i].mapped) continue; // поиск первого свободного регистра

x86reg[i].mapped = true;
If(mode & modeRead) LoadRegFromMemory(i, R3000A_REG);
If(mode & modeWrite) x86reg[i].write = true;

return i;
}

// сюда попадем если все регистры уже использованы, в этом случае ищем самый давно не используемый регистр

}


Код придуман из головы и вероятно скомпилировать его даже не получится, но дает возможность понять что есть такое регкешинг, про константы расскажу попозже, пока задавайте вопросы или выражайте не согласие со мной ;)


Сообщение 08 дек 2010, 01:47
Профиль WWW
Основатель сайта
Аватара пользователя

Зарегистрирован:
21 июл 2007, 15:40
Сообщения: 2234
Откуда: Москва
Я "плыву" ещё дальше. =)
Wind, вот лично мне в твоих примерах не хватает самих простых примеров. Т.е. тот же регкешинг был бы понятнее, если бы ты написал, что вот в исходном состоянии было то-то, применяем такую-ту функцию, алгоритм которой построен так-то (сначала алгоритм на псевдо языке, а потом уже пример на с++) и в итоге получаем вот это. Вот так мне было бы понятнее (думаю, не только мне). А в приведённом тобой примере нет ни исходного положения, ни цели, ни вывода. Только куски кода, которые не всем будет легко понять.
Хотелось бы объяснения как для "самых маленьких".


Сообщение 22 янв 2012, 22:11
Профиль

Зарегистрирован:
22 янв 2012, 21:48
Сообщения: 2
Сильно заинтересовался темой эмуляции. Получилось написать эмулятор Chip-8(но там уж до безобразия всё просто, всё-таки не "настоящая" система). Сейчас пробую написать эмулятор GameBoy, и возник вопрос - для чего процессору флаги(Carry, Zero, Overflow и т.д.), как они работают, как и для чего они применяются? Может быть, кто-нибудь может описать или дать ссылку на описание.
P.S. А чего забросили данную тему, я думаю она была бы полезна очень многим в завершённом виде.


Сообщение 23 янв 2012, 10:39
Профиль
Аватара пользователя

Зарегистрирован:
24 июл 2007, 06:54
Сообщения: 492
Откуда: Embedded
Флаги процессора это ключевые свойства АЛУ и нужны для управления поведением. Т.е., если процессор будет только что-то вычислять, то получается обычный калькулятор. А вот если процессор будет менять свое поведение в зависимости от результата вычислений - это уже другое. Сами флаги это просто признаки свойства полученного результата после арифметической или логической операции АЛУ над операндами. Например, если результят вдруг стал равен 0, то взводится флаг Z, а значит процессор может распознать эту ситуацию командой условного перехода и изменить свое поведение (перейдет по другой ветке программы). Ну вот как-то так, на пальцах.

_________________
Tried so hard and got so far, but in the end, it doesn't even matter...


Сообщение 23 янв 2012, 18:41
Профиль

Зарегистрирован:
22 янв 2012, 21:48
Сообщения: 2
Спасибо за разъяснение, действительно помогло.


Показать сообщения за:  Поле сортировки  
Ответить на тему   [ Сообщений: 30 ]  На страницу Пред.  1, 2

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 66


Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете добавлять вложения

Найти:
Перейти:  
Powered by phpBB © 2000, 2002, 2005, 2007 phpBB Group.
Designed by STSoftware for PTF (mod by Zeru-j).
Русская поддержка phpBB