Реализация регистров в виртуальной машине C - PullRequest
6 голосов
/ 25 января 2011

Я написал виртуальную машину на C как хобби-проект.Эта виртуальная машина выполняет код, очень похожий на синтаксис Intel x86.Проблема в том, что регистры, которые использует эта виртуальная машина, являются только регистрами по имени.В моем коде виртуальной машины регистры используются так же, как регистры x86, но машина сохраняет их в системной памяти.Нет никаких улучшений производительности при использовании регистров над системной памятью в коде виртуальной машины.(Я думал, что только локальность немного увеличит производительность, но на практике ничего не изменилось.)

При интерпретации программы эта виртуальная машина сохраняет аргументы инструкций в виде указателей.Это позволяет виртуальной инструкции принимать в качестве аргумента адрес памяти, постоянное значение, виртуальный регистр или что угодно.

Поскольку аппаратные регистры не имеют адресов, я не могу придумать, как на самом деле хранить свои регистры ВМ в аппаратных регистрах.Использование ключевого слова register в моем типе виртуального регистра не работает, потому что мне нужно получить указатель на виртуальный регистр, чтобы использовать его в качестве аргумента.Есть ли способ заставить эти виртуальные регистры работать более как их родные аналоги?

Мне совершенно удобно вникать в сборку, если это необходимо.Мне известно, что JIT-компиляция этого кода виртуальной машины может позволить мне использовать аппаратные регистры, но я бы хотел использовать их и с моим интерпретированным кодом.

Ответы [ 6 ]

8 голосов
/ 25 января 2011
  1. Машинные регистры не имеют поддержки индексирования: вы не можете получить доступ к регистру с указанным во время выполнения «индексом», что бы это ни значило, без генерации кода. Поскольку вы, скорее всего, декодируете индекс регистра из ваших инструкций, единственный способ - это сделать огромный переход (т. Е. switch (opcode) { case ADD_R0_R1: r[0] += r[1]; break; ... }). Скорее всего, это плохая идея, поскольку она слишком сильно увеличивает размер цикла интерпретатора, поэтому вводит кэш инструкций.

  2. Если мы говорим о x86, дополнительная проблема заключается в том, что количество регистров общего назначения довольно мало; некоторые из них будут использоваться для бухгалтерии (хранение ПК, сохранение состояния стека виртуальной машины, инструкции по декодированию и т. д.) - маловероятно, что у вас будет более одного свободного регистра для виртуальной машины.

  3. Даже если бы была доступна поддержка индексации регистров, вряд ли это принесло бы вам большую производительность. Обычно в интерпретаторах самым большим узким местом является декодирование команд; x86 поддерживает быструю и компактную адресацию памяти, основанную на значениях регистров (т. е. mov eax, dword ptr [ebx * 4 + ecx]), поэтому вы не выиграете много. Однако стоит проверить сгенерированную сборку, т.е. убедиться, что адрес «пула регистров» хранится в регистре.

  4. Лучший способ ускорить работу переводчиков - это JITting; даже простой JIT (т. е. без интеллектуального распределения регистров - в основном, просто испуская тот же код, который вы выполняете с помощью цикла инструкций и оператора switch, кроме декодирования инструкций), может повысить вашу производительность в 3 раза или более (это реальные результаты простого JITter поверх виртуальной виртуальной машины типа Lua). Интерпретатор лучше всего хранить как справочный код (или для холодного кода, чтобы уменьшить стоимость памяти JIT - стоимость генерации JIT не является проблемой для простых JIT).

3 голосов
/ 25 января 2011

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

Чтобы получить производительность, вам нужно заранее спроектировать производительность.

Несколько примеров.

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

Статический двоичный перевод. Разберите и переведите код перед выполнением, например, инструкция 0x34,0x2E превратится в ascii в файле .c:

al ^ = 0x2E; из = 0; ср = 0; SF = ал

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

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

Или, может быть, просто перепроектируйте ваш эмулятор, чтобы он был более эффективным с точки зрения исполнения. Посмотрите на эмулируемые процессоры в MAME, например, читаемость и ремонтопригодность кода были принесены в жертву производительности. Когда было написано, что это важно, сегодня с многоядерными гигагерцовыми процессорами вам не нужно так усердно работать, чтобы эмулировать 1,5 ГГц 6502 или 3 ГГц z80. Такое простое решение, как поиск следующего кода операции в таблице и решение не эмулировать некоторые или все вычисления флага для инструкции, может дать вам ощутимый импульс.

Итог, если вы заинтересованы в использовании аппаратных регистров x86, Ax, BX и т. Д. Для эмуляции регистров AX, BX и т. Д. При запуске программы, единственный эффективный способ сделать это - фактически выполнить инструкцию, и не выполнять и перехватывать, как при пошаговом отладчике, а выполнять длинные строки инструкций, не давая им покинуть пространство виртуальной машины. Есть разные способы сделать это, и результаты производительности могут отличаться, и это не значит, что это будет быстрее, чем эффективный эмулятор. Это ограничивает вас для согласования процессора с программой. Эмуляция регистров с эффективным кодом и действительно хорошим компилятором (хорошим оптимизатором) даст вам разумную производительность и переносимость, так как вам не нужно будет подбирать аппаратное обеспечение для запускаемой программы.

1 голос
/ 29 декабря 2011

преобразует ваш сложный код на основе регистров перед выполнением (раньше времени).Простым решением для выполнения будет четвертый аналог vm с двумя стеками, который дает возможность кэшировать элемент top-of-stack (TOS) в регистре.Если вы предпочитаете решение на основе регистров, выберите формат «код операции», который объединяет как можно больше инструкций (правило большого пальца, до четырех инструкций можно объединить в байт, если выбран дизайн в стиле MISC).Таким образом, доступ к виртуальному регистру локально разрешается к физическим ссылкам на регистры для каждой статической суперинструкции (clang и gcc способны выполнить такую ​​оптимизацию).В качестве побочного эффекта пониженная частота ошибочного прогнозирования BTB приведет к гораздо лучшей производительности независимо от распределения отдельных регистров.

Лучшими методами потоковой обработки для интерпретаторов на основе C являются прямая потоковая обработка (расширение метки как адреса) и реплицируемая коммутациянарезание резьбы (соответствует ANSI).

0 голосов
/ 25 января 2011

Чтобы ответить на конкретный вопрос, который вы задали:

Вы могли бы поручить своему компилятору C оставить кучу регистров бесплатными для вашего использования. Указатели на первую страницу памяти обычно не допускаются, они зарезервированы для проверок указателей NULL, поэтому вы можете использовать начальные указатели для маркировки регистров. Это помогает, если у вас есть несколько собственных регистров для резервирования, поэтому мой пример использует 64-битный режим для симуляции 4 регистров. Вполне возможно, что дополнительные издержки коммутатора замедляют выполнение, а не ускоряют его. Также см. Другие ответы для общих советов.

/* compile with gcc */

register long r0 asm("r12");
register long r1 asm("r13");
register long r2 asm("r14");
register long r3 asm("r15");

inline long get_argument(long* arg)
{
    unsigned long val = (unsigned long)arg;
    switch(val)
    {
        /* leave 0 for NULL pointer */
        case 1: return r0;
        case 2: return r1;
        case 3: return r2;
        case 4: return r3;
        default: return *arg;
    }
}
0 голосов
/ 25 января 2011

Ваша виртуальная машина кажется слишком сложной для эффективной интерпретации.Очевидная оптимизация - иметь виртуальную машину с «микрокодом» с инструкциями загрузки / сохранения в регистре, возможно, даже в стеке.Вы можете перевести свою высокоуровневую виртуальную машину в более простую перед выполнением.Другая полезная оптимизация зависит от расширения вычисляемых меток gcc, см. Пример интерпретатора VM Objective Caml для такой реализации многопоточной виртуальной машины.

0 голосов
/ 25 января 2011

Итак, вы пишете интерпретатор x86, который должен быть на 1 - 3 степени 10 медленнее, чем фактическое оборудование. В реальном оборудовании, сказать, что mov mem, foo займет намного больше времени, чем mov reg, foo, в то время как в вашей программе mem[adr] = foo займет примерно столько же, сколько myRegVars[regnum] = foo (кеширование по модулю). Значит, вы ожидаете такой же перепад скорости?

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

...