ЗАГОЛОВОК

Подсистема JIT Паррота


ВЕРСИЯ

ТЕКУЩАЯ

    Maintainer: Daniel Grunblatt
    Class: Internals
    PDD Number: 8
    Version: 1.3
    Status: Developing
    Last Modified: 26 Nov 2002
    PDD Format: 1
    Language:English


КРАТКИЙ ОБЗОР

Этот PDD описывает подсистему компиляции Паррота Just in Time(Как Раз Вовремя).


ОПИСАНИЕ

Подсистема Just In Time(или JIT) переводит файл с байт-кодом в инструкции родного машинного кода и выполняет сгенерированную последовательность.


РЕАЛИЗАЦИЯ

В настоящий момент подсистема работает на процессорных системах ALPHA, Arm, Intel x86, PPC, and SPARC version 8 и на большинстве операционных системах. Пока поддерживаются только 32-разрядные значения INTVAL.

На начальном этапе генерации родного кода вызывается функция Parrot_jit_begin, которая обеспечивает архитектурно-специфический вводный код. Для каждого кода операции Паррота генерируется либо общая последовательность, либо специальная последовательность родного кода. Файлы с расширением .jit предоставляют функции, генерирующие родной код для специальных кодов операций с учетом имеющегося у архитектуры набора инструкций. Если для специального кода операции функция не предоставляется, то выводится общая последовательность родного кода, которая вызывает интерпретатор функций Си, который и реализует код операции. Такой код операции обрабатывается Parrot_jit_normal_op.

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

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


ФАЙЛЫ

jit/${jitcpuarch}/jit_emit.h
Этот файл определяет функции Parrot_jit_begin, Parrot_jit_dofixup, Parrot_jit_normal_op, Parrot_jit_cpcf_op, Parrot_jit_restart_op и возможно Parrot_jit_vtable*_op. Кроме того, он определяет макросы и статические функции, используемые в файлах .jit для получения двоичного представления родных инструкций.

Для перемещения регистров от процессора к Парроту и обратно должны быть реализованы функции Parrot_jit_emit_mov*.

jit/${jitcpuarch}/core.jit
Здесь указываются функции, генерирующие родной код для основной части кодов операций Паррота. Для упрощения поддержания функции указываются в формате, который предварительно обрабатывается jit2h.pl для получения действительного исходного файла Си jit_cpu.c. Смотрите ниже Format of .jit Files.

include/parrot/jit.h
Этот Файл содержит определения общих структур используемых подсистемой JIT.

Массив op_jit структур jit_fn_info_t обеспечивает для каждого кода операции указатель на функцию, которая генерирует родной код. Функция является либо общей функцией Parrot_jit_normal_op, либо Parrot_jit_cpcf_op, либо специальной функцией для кода операции. Функция Parrot_jit_restart_op похожа на функцию Parrot_jit_cpcf_op с добавлением проверки нуля программного счетчика. Функции Parrot_jit_vtable*_op как Parrot_jit_normal_op или Parrot_jit_cpcf_op и могут быть реализованы для выполнения родных вызовов vtable (смотрите jit/i386/jit_emit.h для примера).

Структура Parrot_jit_fixup хранит смещение в родном коде, где должна быть произведена адресная запись, тип требуемой адресной записи и специальная информация необходимая для выполнения параметров записи. Сейчас параметр адресной записи является либо значением типа opcode_t, либо указателем на функцию.

Структура Parrot_jit_info хранит данные, используемые вовремя получения и исполнения родного кода. Важной частью данных в этой структуре является массив op_map, который отображает адреса кодов операций на адреса родного кода.

jit.c
build_asm() является главной программой генератора кода, которая проходит по байт-коду Паррота, вызывая программы, генерирующие код, и заполняя массив op_map. Этот массив используется подсистемой JIT для выполнения определенного типа адресных записей в родном коде, а также самим родным кодом для преобразования значений программных счетчиков в значения аппаратных счетчиков.

Байт-код по сути является массивом элементов размера opcode_t с параллельными записями op_map. На начальном этапе op_map заполнен смещениями в родном коде, соответствующими кодам операций в байт-коде. Как только генерация кода завершена и применины адресные записи, смещения в родном коде переводятся в абсолютные адреса. Таким образом удается обменять неопределенность затрат неоднократных преобразований смещений во время выполнения на небольшие затраты на начальном этапе.

Если архитектура определяет INT_REGISTERS_TO_MAP и FLOAT_REGISTERS_TO_MAP как не нулевые, то эти значения предельного числа используемых регистров отображается на родные регистры процессора.

jit2h.pl
Предварительно обрабатывает файлы .jit для получения jit_cpu.c.


Определения в jit_emit.h

Архитектурно-специфический файл jit_emit.h сообщает некоторые определения и таблицы с помощью jit.c и languages/imcc/imc.c. Поэтому структура файла и определения должны следовать специальному синтаксису.

Общая структура

        #if JIT_EMIT
        ... emit код
        #else
        ... определения
        #ifndef JIT_IMCC
        ... инициализация отображений
        ... и возможно частные статические функции
        #endif
        #endif

Определения

INT_REGISTERS_TO_MAP
Это число целых регистров для отображения на регистры процессора. Соответствующий intval_map[] должен быть в точности равен этому числу. Нулевой регистр не может быть в списке.

FLOAT_REGISTERS_TO_MAP
Когда определено, то имеет такой же смысл только для регистров с плавающей точкой.

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

PRESERVED_FLOAT_REGS
То же самое только для регистров с плавающей точкой.

jit_emit_noop(pc)
JUMP_ALIGN
Если jit_emit_noop(pc) и JUMP_ALIGN определены, JUMP_ALIGN должен иметь маленькое значение, указывающее желаемое выравнивание целей перехода, которое равно 1 << JUMP_ALIGN. jit_emit_noop повторно вызывается с невыровненым pc до тех пор, пока the pc не буде иметь желаемое выравнивание. Таким образом функция может выпускать одну однобайтовую инструкцию noop или похожую на noop инструкцию(последовательность) требуемого размера для достижения необходимого выравнивания.

ALLOCATE_REGISTERS_PER_SECTION
Обычно jit.c выполняет назначение регистров по секционно. Но есть в некоторой степени экспериментальное назначение - по блочно.

MAP
Jit-код, сгенерированный JIT оптимизатором imcc использует отрицательные числа для отображаемых регистров и положительные для неотображаемых регистров Паррота. Для использования этой особенности определение отображаемых регистров может быть переопределено следующим образом:
        #define MAP(i) OMAP(i)
        #undef MAP
        #define MAP(i) (i) >= 0 : 0 ? OMAP(i)

EXTCALL
Этот макрос, если определен, может заменять, то что считается внешней функцией(external function). По умолчанию определение является:
        #  define EXTCALL(op) (op_jit[*(op)].extcall)

JIT/i386 имеет jit'ированные функции vtable, где extcall является номером записи vtable и (JIT/i386) переопределяет EXTCALL на:

        #  define EXTCALL(op) (op_jit[*(op)].extcall == 1)

Смотрите действительное применение этих определений в jit/i386/jit_emit.h


Формат файлов .jit

Jit-файлы интерпретируются следующим образом:

op-name { \n body \n }
Где op-name является названием кода операции Паррота и body состоит из кода Си, который может содержать любой из идентификаторов, перечисленных в следующем разделе.

Закрывающая фигурная скоька должна находится в первой колонке.

Строки комментариев
Комментарии помечаются ;, находящимся в первой колонке и, как и пустые строки, игнорируются.

Идентификаторы
Вообще префиксирование идентификатора символом & дает адрес, а префикс * указывает значение. Так как значение регистров Паррота меняется в течение выполнения кода, то эти значения не могут быть получены через единственную замену идентификатора.

INT_REG[n]

Замещается регистром INTVAL, указанным в аргументе n.

NUM_REG[n]

Замещается регистром FLOATVAL, указанным в аргументе n.

STRING_REG[n]

Замещается регистром STRING, указанным в аргументе n.

INT_CONST[n]

Замещается константой INTVAL, указанной в аргументе n.

NUM_CONST[n]

Замещается константой FLOATVAL, указанной в аргументе n.

MAP[n]

n-й целый или регистр с плавайщей точкой отображается в данной секции.

ЗАМЕЧАНИЕ: Регистр с физическим номером ноль не может быть отображен.

NATIVECODE

Замещается на значение текущего программного счетчика.

*CUR_OPCODE[n]

Замещается на адрес текущего кода операции байт-кода Паррота.

ISRn FSRn

Целый или с плавающей точкой регистр временной памяти.

TEMPLATE template-name { \n body \n }
Определяет шаблон для схожих функций. Например, все бинарный коды операций три переменных параметра.

template-name perl-subst ...
Принимает шаблон и производит все замены для получения реализации jit-функции.

Пример:

    TEMPLATE Parrot_set_x_ic {
        if (MAP[1]) {
            jit_emit_mov_ri<_N>(NATIVECODE, MAP[1], <typ>_CONST[2]);
        }
        else {
            jit_emit_mov_mi<_N>(NATIVECODE, &INT_REG[1], <typ>_CONST[2]);
        }
    }
    Parrot_set_i_ic {
        Parrot_set_x_ic s/<_N>/_i/ s/<typ>/*INT/
    }
    Parrot_set_n_ic {
        Parrot_set_x_ic s/<_N>/_ni/ s/<typ>/&INT/ s/INT_R/NUM_R/
    }

Jit-функция Parrot_set_i_ic основана на шаблоне Parrot_set_x_ic, s/x/y/ - замены, производимые в шаблоне для получения настоящего тела функции. Эти замены перед другими заменами.

Смотрите jit/i386/core.jit для получения дополнительной информации.

Соглашения по именам для функций jit_emit

С целью упрощения распределения файлов core.jit между машинами со сходной архитектурой функции jit_emit должны следовать следующему синтаксису:

jit_emit_<op>_<args>_<type>

<op> Это операция наподобие mov, add или bxor. В обычных ситуациях название кода операции соответствует его PASM-имени.
<args>
args указывают аргументы функции в PASM-последовательности: dest, source ... args состоят из букв - по одной на каждый аргумент.
r
Отображенный регистр процессора.

m
Опреанд, хранимый в памяти - адрес регистра Парота.

i
Непосредственный операнд, т.е. целая константа.

<type>
Указывает, работает ли операция с целыми аргументами или аргументами с плавающей точкой. Если все аргументы одного типа, то нужен только один спецификатор типа.
i
Целый аргумент.

n
Аргумент с плавающей точкой.

Примеры:

jit_emit_sub_rm_i
Отнимает целое из памяти от целого регистра процессора.

jit_emit_mov_ri_ni
Перемещает целую константу(непосредственный операнд) в регистр с плавающей точкой.


Замечания для ALPHA

Доступ к регистрам Парота выполняется относительно $6, к памяти - относительно $27, к константам с плавающей точкой - относительно to access float constants relative to $7. То есть вы можете осуществлять контроль над инструкцией с помощью ldah $7,0($27).


Замечания для i386

Поддерживаются только 32-разрядные значения INTVALs. Поддерживаются значения типа long double FLOATVAL.

Есть четыре отображаеммых регистра - %edi, %ebx, %esi и %edx. Первые три из них сохраняются для вызываемой стороны. Они предохраняют свои значения во время вызовов внешних функций.

Регистры ST1 ... ST4 четырех операций с плавающей точкой отображаются и рассматриваются как предохраняемые во время вызовов функций.


ПРИМЕР

Давайте посмотрим как это работает:

Ассемблерный код Парота:

 set I0,8
 set I2,I0
 print I2
 end

Байт-код Парота: (показан только сегмент байт-кода)

 +--------------------------------------+
 | 73 | 0 | 8 | 72 | 2 | 0 | 21 | 2 | 0 |
 +-|------------|------------|--------|-+
   |            |            |        |
   |            |            |        +----------- end (нет аргументов)
   |            |            +-------------------- print_i (1 аргумент)
   |            +--------------------------------- set_i_i (2 аргумента)
   +---------------------------------------------- set_i_ic (2 аргумента)

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

Версия ассемблера кодов операций для Intel x86:

Parrot_jit_begin

    0x817ddd0 <jit_func>:       push   %ebp
    0x817ddd1 <jit_func+1>:     mov    %esp,%ebp
    0x817ddd3 <jit_func+3>:     push   %ebx
    0x817ddd4 <jit_func+4>:     push   %esi
    0x817ddd5 <jit_func+5>:     push   %edi
    до сюда идет обычный заголовок функции,
    теперь "заталкиваем" интерпретатор
    0x817ddd6 <jit_func+6>:     push   $0x8164420
    помещаем таблицу jit-функций в %ebp и
    переходим к первой инструкции
    0x817dddb <jit_func+11>:    mov    0xc(%ebp),%eax
    0x817ddde <jit_func+14>:    mov    $0x81773f0,%ebp
    0x817dde3 <jit_func+19>:    sub    $0x81774a8,%eax
    0x817dde9 <jit_func+25>:    jmp    *%ds:0x0(%ebp,%eax,1)

set_i_ic

    0x817ddee <jit_func+30>:    mov    $0x8,%edi

set_i_i

    0x817ddf3 <jit_func+35>:    mov    %edi,%ebx

Parrot_jit_save_registers

    0x817ddf5 <jit_func+37>:    mov    %edi,0x8164420
    0x817ddfb <jit_func+43>:    mov    %ebx,0x8164428

Parrot_jit_normal_op

    0x817de01 <jit_func+49>:    push   $0x81774c0
    0x817de06 <jit_func+54>:    call   0x804be00 <Parrot_print_i>
    0x817de0b <jit_func+59>:    add    $0x4,%esp

Parrot_jit_end

    0x817de0e <jit_func+62>:    add    $0x4,%esp
    0x817de14 <jit_func+68>:    pop    %edi
    0x817de16 <jit_func+70>:    pop    %ebx
    0x817de18 <jit_func+72>:    pop    %esi
    0x817de1a <jit_func+74>:    pop    %ebp
    0x817de1c <jit_func+76>:    ret

Пожалуйста, обратите внимание на противоположную последовательность аргументов. Нотации PASM and JIT используют dest,src,src, в то время как gdb и внутренние макросы в jit_emit.h - src,dest.


Отладка

Листинг вверху был сгенерирован gdb(отдладчик GNU) с небольшой помощью Parrot_jit_debug, который генерирует символьный файл в формате stabs. Смотрите info stabs за дополнительной информацией.

Следующий скрипт вызывает ddd (внешний графический отладчик) и прикрепляет символьной файл, после чего этот файл создается в build_asm.

        # dddp
        # запускаем ddd Парота с имеющимся файлом
        # должны начинаться выводится подтверждения gdb
        # $ ln -s languages/imcc предполагается imcc
        cd languages/imcc
        make -s
        cd -
        imcc -o $1.pbc -d -O1 $1.pasm
        echo "b runops_jit
        r -d -j $1.pbc
        n
        n
        n
        n
        n
        n
        add-symbol-file $1.o 0
        s
        " > .ddd
        ddd --command .ddd parrot &

Запускаем, например, с dddp t/op/jit_2, затем включаем статус регистров then turn on the register status, проходим по исходному файлу(step или nexti) или устанавливаем точки останова как и в любом другом языке. Хотя пока нет информации о номерах строк, вы можете удалять пустые строки и объединять метки с кодами операций в pasm-файле.

Вы можете проверять регистры Паррота через отладчик или даже устанавливать их и всегда можете заходить во внешний код операции и смотреть на *interpreter.

Тесты t/op/jit*.t имеют несколько тестовых ситуаций для тестирования назначения регистров. Эти тесты написаны для отображения на 4-х регистровый процессор. Если ваш процессор имеет больше четырех регистров, уменьшите значение до четырех и запустите тесты.

Пример отладочной сессии

  $ cat j.pasm
        set I0, 10
        set N1, 1.1
        set S2, "abc"
        print "\n"
        end
  $ dddp j

(ddd показывает исходный и ассемблерный код (начальный код пропущен):

    0x815de46 <jit_func+30>:    mov    $0xa,%ebx
    0x815de4b <jit_func+35>:    fldl   0x81584c0
    0x815de51 <jit_func+41>:    fstp   %st(2)
    0x815de53 <jit_func+43>:    mov    %ebx,0x8158098
    0x815de59 <jit_func+49>:    fld    %st(1)
    0x815de5b <jit_func+51>:    fstpl  0x8158120
    0x815de61 <jit_func+57>:    push   $0x815cd90
    0x815de66 <jit_func+62>:    call   0x804db90 <Parrot_set_s_sc>
    0x815de6b <jit_func+67>:    add    $0x4,%esp
    0x815de6e <jit_func+70>:    push   $0x815cd9c
    0x815de73 <jit_func+75>:    call   0x804bcd0 <Parrot_print_sc>
    0x815de78 <jit_func+80>:    add    $0x4,%esp
    0x815de7b <jit_func+83>:    add    $0x4,%esp
    0x815de81 <jit_func+89>:    pop    %edi
    0x815de83 <jit_func+91>:    pop    %ebx
    0x815de85 <jit_func+93>:    pop    %esi
    0x815de87 <jit_func+95>:    pop    %ebp
    0x815de89 <jit_func+97>:    ret
  (gdb) n
  (gdb) n
  (gdb) n
  (gdb) p I0
  $1 = 10
  (gdb) p N1
  $2 = 1.1000000000000001
  (gdb) p *S2
  $3 = {bufstart = 0x815ad30, buflen = 15, flags = 336128, bufused =
  3, strstart = 0x815ad30 "abc"}
  (gdb) p &I0
  $4 = (INTVAL *) 0x8158098