Реализация регистров в виртуальной машине C

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

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

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

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

6 ответов


  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, кроме декодирования инструкций) может повысить производительность 3x или более (это фактические результаты от простого дрожания поверх Lua-подобной регистрационной виртуальной машины). Интерпретатор лучше всего хранить в качестве справочного кода (или для холодного кода для снижения стоимости JIT - памяти-стоимость генерации JIT не является проблемой для простых JITs).


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

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

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

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

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

al ^= 0x2E; of =0; cf=0; sf=al

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

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

или, может быть, просто перепроектировать эмулятор, чтобы быть более эффективным с точки зрения выполнения. Посмотрите на эмулированные процессоры в MAME, например, читаемость и ремонтопригодность кода была принесена в жертву производительности. Когда написано, что было важно, сегодня с многоядерными гигагерцовыми процессорами вам не нужно так много работать, чтобы эмулировать 1.5 ghz 6502 или 3ГГц на Z80. Что-то простое, как поиск следующего кода операции в таблице и решение не эмулировать некоторые или все вычисления флага для инструкции, может дать вам заметный толчок.

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


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

лучшие методы потоковой передачи для интерпретаторов на основе C-это прямая потоковая передача (расширение label-as-address) и реплицированная потоковая передача (ANSI conform).


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

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


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


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

вы можете поручить компилятору C оставить кучу регистров для вашего использования. Указатели на первую страницу памяти обычно не допускаются, они зарезервированы для проверки нулевого указателя, поэтому вы можете злоупотреблять начальными указателями для маркировки регистров. Это помогает, если у вас есть несколько собственных регистров, поэтому мой пример использует 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;
    }
}