JITtting с ограниченным знанием целевой архитектуры - PullRequest
2 голосов
/ 03 июля 2019

Я реализовал небольшой интерпретатор байт-кода, используя вычисленное goto ( см. Здесь , если не знакомо).

Может показаться, что возможно выполнить простое JITting, скопировав память между метками, оптимизировав таким образом скачки. Например, скажем, у меня есть следующий переводчик:

op_inc: val++; DISPATCH();

Я бы изменил это на:

op_inc: val++;
op_inc_end:

При JITting я добавляю память между метками к своему выводу:

memcpy(jit_code+offset, &&op_inc, &&op_inc_end - &&op_inc);

(jit_code помечен как исполняемый с использованием mmap)

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

goto *(void*)jit_code

Будет ли это работать? Чего-то не хватает в моей ментальной модели машинного кода, которая бы помешала этой идее?

Предположим, что код и данные находятся в одном и том же адресном пространстве. Давайте также предположим, PIC.

Обновление

Глядя на пример в связанной статье , после удаления DISPATCH получаем:

        do_inc:
            val++;
        do_dec:
            val--;
        do_mul2:
            val *= 2;
        do_div2:
            val /= 2;
        do_add7:
            val += 7;
        do_neg:
            val = -val;
        do_halt:
            return val;

Сгенерированный код для do_inc (без оптимизации) просто:

Ltmp0:                                  ## Block address taken
## %bb.1:
    movl    -20(%rbp), %eax
    addl    $1, %eax
    movl    %eax, -20(%rbp)

(сопровождается непосредственно do_dec). Похоже, этот маленький фрагмент может быть вырезан.

Ответы [ 5 ]

5 голосов
/ 03 июля 2019

Вот еще одна причина, по которой это не сработает на одной архитектуре:

В коде ARM Thumb используются прямые значения вне строки с адресацией относительно ПК.Операция, подобная

a += 12345;

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

ldr  r3, [pc, #<offset to constant>]
adds r0, r4, r3

… other unrelated code …

bx   lr      ; end of the function
.word 12345  ; oh, and here's the constant

При копировании фрагмента этой функции ссылка на константу останется неразрешенной;в конечном итоге он будет использовать неожиданное значение из другого места в памяти.

4 голосов
/ 03 июля 2019

Нет, это не будет работать в целом. Вот только одна из многих причин, почему:

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

3 голосов
/ 04 июля 2019

Обновление ... Похоже, этот маленький фрагмент может быть вырезан.

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

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

Если бы вы обращались к чему-либо в статическом хранилище (константы или глобальные / статические переменные), компиляторы x86-64 использовали бы режимы RIP-относительной адресации, такие как var(%rip), которые прервались бы, если бы вы изменили положение кода относительно данные.

Вызовы к другим функциям также прервутся, потому что они скомпилируются в call rel32, где целевой адрес закодирован относительно сайта вызова.


В целом это вряд ли полезно. Все, что вы могли бы сделать, скопировав блоки неоптимизированного кода, вероятно, можно было бы сделать более эффективно, написав интерпретатор на переносном языке C. Ваша идея очень легко сломается при включенной оптимизации.

Например, если вы хотите увеличить * в 1020 * раз, введение цикла сохранения / перезагрузки добавляет около 5 циклов задержки пересылки хранилища в критический путь, если вы просто скопируете этот блок n раз.

Кроме того, вам нужно __builtin___clear_cache в диапазоне, который вы скопировали, если вы хотите включить оптимизацию. И да, это применимо даже к x86, где он на самом деле не очищает кеш, но все же останавливает удаление мертвого хранилища от удаления memcpy.


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

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

3 голосов
/ 03 июля 2019

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

TL; DR: C - это , а не"сборка высокого уровня", и поэтому вы не можете использовать ее там, где требуется ассемблер.

1 голос
/ 03 июля 2019

Будет ли это работать?

Да ; это может сработать.

Однако он может работать только в очень ограниченных случаях, когда:

  • нет аргументов для ваших функций. Например, если вы JITting для такого языка, как brainfuck (см. https://en.wikipedia.org/wiki/Brainfuck), то у вас могут быть такие функции, как "void increment(void); и void putchar(void), которые изменяют глобальное состояние (например, struct machine_state { void * ptr; }).

  • Нет либо независимости позиции (адрес глобального состояния является фиксированным / постоянным адресом), либо вы можете указать компилятору зарезервировать регистр для использования в качестве «указателя на глобальное состояние» (что является чем-то который поддерживает GCC).

  • Целевая машина поддерживает некоторый способ обработки данных как кода. Примечание. Это относительно неактуально (если машина не может это сделать каким-либо образом, вы также не сможете выполнять файлы).

  • Вы используете барьеры для предотвращения смещения компилятором кода из ваших меток. Примечание. Это также относительно не имеет значения (например, если вы используете встроенную сборку, чтобы определить метку и пометить встроенную сборку как энергозависимую, и поместите все в список дубликатов, то ..).

  • Вы не используете «чистый портативный C». Примечание: вы уже используете специфичные для компилятора расширения (вычисленные goto), поэтому я предполагаю, что это также относительно не имеет значения.

Для всех этих ограничений; единственное, что может иметь значение на практике, это первое - все, что стоит JITting, будет слишком сложным (например, вам понадобятся такие функции, как void add(int register_number_1, int register_number_2);), и как только вы попытаетесь передать аргументы своим функциям, вы закончите в зависимости от конкретных соглашений о вызовах.

...