Проектирование виртуальной машины с JIT - PullRequest
4 голосов
/ 28 ноября 2009

Я разрабатываю язык сценариев, который компилируется для собственной виртуальной машины, простой, который содержит инструкции для работы с такими данными, как points , vector , float и т. д. Ячейка памяти представлена ​​следующим образом:

struct memory_cell
{
    u32 id;
    u8 type;

    union
    {
        u8 b; /* boolean */
        double f; /* float */
        struct { double x, y, z; } v; /* vector */
        struct { double r, g, b; } c; /* color */
        struct { double r, g, b; } cw; /* color weight */
        struct { double x, y, z; } p; /* point variable */
        struct { u16 length; memory_cell **cells; } l; /* list variable */
    };  
};

Инструкции являются общими и могут работать со многими различными операндами. Например

ADD dest, src1, src2

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

Основной цикл выполнения просто проверяет код операции инструкции (которая представляет собой структуру, содержащую объединения для определения любого вида инструкции) и выполняет ее. Я использовал упрощенный подход, при котором у меня нет регистров, а просто большой массив ячеек памяти.

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

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

 void VirtualMachine::executeInstruction(instr i)
 {
     u8 opcode = (i.opcode[0] & (u8)0xFC) >> 2;

     if (opcode >= 1 && opcode <= 17) /* RTL instruction */
     {
        memory_cell *dest;
        memory_cell *src1;
        memory_cell *src2;

        /* fetching destination */
        switch (i.opcode[0] & 0x03)
        {
            /* skip fetching for optimization */
            case 0: { break; }
            case MEM_CELL: { dest = memory[stack_pointer+i.rtl.dest.cell]; break; }
            case ARRAY_VAL: { dest = memory[stack_pointer+i.rtl.dest.cell]->l.cells[i.rtl.dest.index]; break; }
            case ARRAY_CELL: { dest = memory[stack_pointer+i.rtl.dest.cell]->l.cells[(int)i.rtl.dest.value]; break; }
        }

     /* omitted code */

     switch (opcode)
     {
         case ADD:
         {
             if (src1->type == M_VECTOR && src2->type == M_VECTOR)
             {
                 dest->type = M_VECTOR;
                 dest->v.x = src1->v.x + src2->v.x;
                 dest->v.y = src1->v.y + src2->v.y;
                 dest->v.z = src1->v.z + src2->v.z;
              }

      /* omitted code */

Легко / удобно попробовать jit-компиляцию? Но я действительно не знаю, с чего начать, поэтому спрашиваю несколько советов.

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

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

Ответы [ 3 ]

7 голосов
/ 28 ноября 2009

Перед написанием JIT ("Just-in-time") компилятора, вы должны хотя бы подумать, как бы вы написали "Way-For-Time-Time" компилятор.

То есть, учитывая программу, состоящую из инструкций для вашей виртуальной машины, как бы вы создали программу, состоящую из инструкций x86 (или чего-то еще), которая выполняет те же функции, что и исходная программа? Как бы вы оптимизировали вывод для разных наборов команд и разных версий одной и той же архитектуры? Пример кода операции, который вы дали, имеет довольно сложную реализацию, поэтому какие коды операции вы бы реализовали «встроенным», просто испуская код, который выполняет свою работу, и что бы вы реализовали, отправив вызов некоторого общего кода?

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

Если вы еще не ассемблер, то я не рекомендую писать JIT. Это не значит «никогда не делай этого», но ты должен стать сборщиком, прежде чем начать всерьез.

Альтернативой может быть написание не-JIT-компилятора для преобразования инструкций вашей VM (или исходного языка сценариев) в байт-код Java или LLVM, как говорит Джефф Фостер. Затем позвольте инструментальной цепочке для этого байт-кода выполнить сложную, зависящую от процессора работу.

6 голосов
/ 28 ноября 2009

ВМ - большая задача для рассмотрения. Рассматривали ли вы основание своей виртуальной машины на чем-то вроде LLVM ?

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

3 голосов
/ 28 ноября 2009

Стив Джессоп имеет точку зрения: JIT-компилятор намного сложнее, чем обычный компилятор. А обычный компилятор сам по себе сложен.

Но, читая последнюю часть вопроса, мне интересно, действительно ли вам нужен JIT-компилятор.

Если ваша проблема такая:

Я хочу создать программу трассировки лучей, которая позволит пользователю предоставлять свои процедуры шейдеров и т. Д. используя свой собственный домен конкретного языка. Все идет хорошо. Мой язык определен, интерпретатор реализован, и он работает хорошо и правильно. Но это медленно: как я могу выполнить его как нативный код?

Тогда вот что я делал в подобных ситуациях:

  • Переведите предоставленные пользователем процедуры в функции C, которые можно вызывать из вашей программы.

  • Запишите их в обычный исходный файл C с соответствующими #include и т. Д.

  • Скомпилируйте их как .dll (или .so в * nix), используя обычный компилятор C.

  • Динамически загружайте .dll в вашу программу, узнайте ваши указатели на функции и используйте их в вашем трассировщике лучей вместо интерпретированных версий.

Некоторые заметки:

  • В некоторых средах это может быть невозможно: нет доступа к компилятору C или системной политике что запрещает вам загружать свои собственные DLL. Поэтому проверьте, прежде чем попробовать.

  • Не выбрасывайте переводчика. Сохраните его как справочную реализацию вашего языка.

...