Проектирование виртуальной машины с помощью JIT

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

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? Но я действительно не знаю, с чего начать, поэтому прошу совета.

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

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


person Jack    schedule 28.11.2009    source источник
comment
действительно хорошее упражнение... но зачем изобретать велосипед? Уже есть много отличных виртуальных машин: LLVM, JVM, BEAM (эмулятор Erlang) и т. д.   -  person jldupont    schedule 28.11.2009
comment
Потому что смешно разбираться во внутренностях этих тем..   -  person Jack    schedule 01.12.2009
comment
Я забыл упомянуть об этом раньше, но вы видели OpenCL? (khronos.org/opencl) Может дать вам несколько идей.   -  person Steve Jessop    schedule 01.12.2009
comment
@ Джек Как в ха-ха смешно?   -  person Michael    schedule 14.04.2015


Ответы (3)


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

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

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

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

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

person Steve Jessop    schedule 28.11.2009

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

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

person Jeff Foster    schedule 28.11.2009

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

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

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

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

Тогда вот то, что я делал, это похожие ситуации:

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

  • Запишите их в обычный исходный файл C с правильными #includes и т. д.

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

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

Некоторые примечания:

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

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

person Tomek Szpakowicz    schedule 28.11.2009
comment
(глупо, но бывает). Немного странно иметь компилятор C, но без динамической компоновки. Но отсутствие компилятора C довольно распространено, если учесть, что большая часть кода не работает на ПК... - person Steve Jessop; 01.12.2009
comment
@Steve: Думаю, я удалю этот комментарий. Речь шла об ограничении права на использование собственного кода (exe, dll и т. д.) в качестве системной политики, а не об отсутствии компилятора. Я знаю, что это происходит. В любом случае, если пользователь не может загрузить свой собственный (в отличие от установленного администратором) код, программа, включающая JIT, также должна работать с каким-то образом повышенными привилегиями. В некоторых средах вы не сможете выполнить блок данных как двоичный код (защита от переполнения буфера и т. д.), поэтому вам все равно придется загружать его как разделяемые библиотеки. - person Tomek Szpakowicz; 01.12.2009
comment
Да, или в зависимости от системы это может быть наоборот - JIT могут выделять исполняемую память с привилегиями среднего уровня, но для авторизации загрузки DLL требуется цифровая подпись (или привилегии уровня ядра). Я могу только предположить, что любой, кто достаточно умен, чтобы написать JIT и продемонстрировать его работу, достаточно умен, чтобы не записывать вредоносный код в память и выполнять его. В то время как любой дурак может загрузить библиотеку, и, следовательно, ему нельзя ;-) - person Steve Jessop; 01.12.2009
comment
@Steve: И любой, кто достаточно умен, чтобы создать ядерную бомбу, также достаточно умен, чтобы не делать этого... Эх... Подождите... Хм... Черт возьми! - person Tomek Szpakowicz; 01.12.2009
comment
Да, я не говорю, что аргументация клиническая, просто они должны чувствовать что-то в этом роде — соотношение риск/вознаграждение для одного ниже, чем для другого. - person Steve Jessop; 01.12.2009