Этот вопрос был отложен как слишком широкий, по-видимому, из-за исследования, которое я включил, чтобы "показать свою работу" вместо того, чтобы задавать вопрос с минимальными усилиями. Чтобы исправить это, позвольте мне суммировать весь вопрос в одном предложении (спасибо @PeterCordes за эту фразу):
Как эффективно вызвать (x86-64) заранее скомпилированные функции (которые я контролирую, может быть дальше, чем 2 ГБ) из кода JITed (который я генерирую)?
Я подозреваю, что одно это будет отложено как "слишком широкое". В частности, ему не хватает того, "что вы пробовали". Поэтому я почувствовал необходимость добавить дополнительную информацию, показывающую мои исследования/мышление и то, что я пробовал. Ниже несколько поток сознания этого.
Обратите внимание, что ни один из вопросов, поставленных ниже, не является ответом на мои вопросы; они более риторически. Их цель - продемонстрировать, почему я не могу ответить на вышеуказанный вопрос (несмотря на мои исследования, мне не хватает опыта в этой области, чтобы делать определенные утверждения, такие как @PeterCordes), прогнозирование ветвей скрывает задержку выборки и проверки указателя функции из памяти, при условии, что это хорошо предсказывает. ") Также обратите внимание, что компонент Rust здесь в значительной степени не имеет значения, так как это проблема сборки. Мои соображения по поводу включения этого были в том, что заранее скомпилированные функции написаны на Rust, поэтому я не был уверен, что есть что-то, что Rust сделал (или проинструктировал LLVM), что может быть выгодно в этой ситуации. Для ответа вполне приемлемо вообще не учитывать Rust; на самом деле, я ожидаю, что это будет так.
Думайте о следующем, как о работе с царапинами на обратной стороне экзамена по математике:
Примечание: я запутал термин "внутренние" здесь. Как отмечается в комментариях, "заранее скомпилированные функции" - лучшее описание. Ниже я сокращу, что функции AOTC.
Я пишу JIT в Rust (хотя Rust имеет отношение только к части моего вопроса, основная часть этого относится к соглашениям JIT). У меня есть функции AOTC, которые я реализовал в Rust и которые мне нужно call
из кода, генерируемого моим JIT. Мой JIT mmap(_, _, PROT_EXEC, MAP_ANONYMOUS | MAP_SHARED)
несколько страниц для объединенного кода. У меня есть адреса моих функций AOTC, но, к сожалению, они намного дальше, чем 32-битное смещение. Я пытаюсь решить, как отправлять вызовы этих функций AOTC. Я рассмотрел следующие варианты (это не вопросы, на которые нужно отвечать, просто демонстрирую, почему я не могу ответить на основной вопрос этой SO-темы):
-
(Специфично для Rust) Каким-то образом заставьте Rust разместить функции AOTC близко (возможно, к?) Кучи, чтобы
call
находился в пределах 32-битного смещения. Непонятно, что это возможно с Rust (есть способ указать собственные аргументы компоновщика, но я не могу сказать, к чему они применяются, и могу ли я указать одну функцию для перемещения. И даже если бы я мог, куда я могу поместить Это?). Также кажется, что это может потерпеть неудачу, если куча достаточно велика. -
(Специфично для ржавчины) Распределите мои страницы JIT ближе к функциям AOTC. Это может быть достигнуто с помощью
mmap(_, _, PROT_EXEC, MAP_FIXED)
, но я не уверен, как выбрать адрес, который не загромождает существующий код Rust (и соблюдает ограничения арки - есть ли разумный способ получить те ограничения?). -
Создайте заглушки на страницах JIT, которые обрабатывают абсолютный переход (код ниже), а затем
call
заглушки. Это имеет то преимущество, что (исходный) сайт вызова в коде JITted является хорошим небольшим относительным вызовом. Но неправильно прыгать через что-то. Похоже, что это отрицательно сказывается на производительности (возможно, мешает прогнозированию RAS/адреса перехода). Кроме того, кажется, что этот переход будет медленнее, так как его адрес является косвенным, и это зависит отmov
для этого адреса.
mov rax, {ABSOLUTE_AOTC_FUNCTION_ADDRESS}
jmp rax
-
Реверс (3), просто вставка вышеупомянутого на каждом внутреннем сайте вызова в коде JITed. Это решает проблему косвенного обращения, но увеличивает код JITted (возможно, это приводит к кешу команд и последствиям декодирования). Он по- прежнему имеет вопрос о том, что переход является непрямым и зависит от
mov
. -
Разместите адреса функций AOTC на странице PROT_READ (только) рядом со страницами JIT. Сделайте все сайты вызовов рядом, абсолютно косвенные вызовы (код ниже). Это удаляет второй уровень косвенности из (2). Но код этой инструкции, к сожалению, большой (6 байт), поэтому он имеет те же проблемы, что и (4). Кроме того, теперь вместо зависимости от регистра переходы без необходимости (поскольку адрес известен во время JIT) зависят от памяти, что, безусловно, влияет на производительность (несмотря на то, что эта страница кэшируется?).
aotc_function_address:
.quad 0xDEADBEEF
# Then at the call site
call qword ptr [rip+aotc_function_address]
-
Futz с регистром сегмента, чтобы разместить его ближе к функциям AOTC, чтобы можно было выполнять вызовы относительно этого регистра сегмента.Кодирование такого вызова является длинным (поэтому, возможно, у него есть проблемы с конвейерным декодированием), но кроме этого это в значительной степени позволяет избежать множества хитрых битов всего, что было до него.Но, возможно, вызов относительно сегмента non-cs
работает плохо.Или, может быть, такое размышление не является разумным (например, портит среду выполнения Rust).(как отмечает @prl, это не работает без дальнего вызова, что ужасно для производительности) -
Не совсем решение, но я мог бы сделать компилятор 32-битным и вообще не иметь этой проблемы. Это не очень хорошее решение, и оно также помешало бы мне использовать расширенные регистры общего назначения (из которых я использую все).
Все представленные варианты имеют недостатки. Вкратце, 1 и 2 - единственные, которые, похоже, не влияют на производительность, но неясно, есть ли non- хакерский способ их достижения (или вообще какой-либо способ в этом отношении). 3-5 не зависят от Rust, но имеют очевидные недостатки производительности.
Учитывая этот поток сознания, я пришел к следующему риторическому вопросу (который не нуждается в явных ответах), чтобы продемонстрировать, что мне не хватает знаний, чтобы самостоятельно ответить на основной вопрос этой SO-темы. Я ударил их, чтобы было совершенно ясно, что я не излагаю все это - часть моего вопроса.
-
Для подхода (1), возможно ли заставить Rust связать определенные
extern "C"
по определенному адресу (около кучи)? Как выбрать такой адрес (во время компиляции)? Безопасно ли предполагать, что любой адрес, возвращаемыйmmap
(или выделяемый Rust), будет в пределах 32-битного смещения от этого местоположения? -
Для подхода (2), как я могу найти подходящее место для размещения JIT-страниц (так, чтобы оно не затирало существующий код Rust)?
И некоторые JIT (non- Rust) конкретные вопросы:
-
Для подхода (3), заглушки будут препятствовать производительности достаточно, чтобы я заботился? Как насчет косвенного
jmp
? Я знаю, что это несколько напоминает заглушки компоновщика, за исключением того, что, как я понимаю, заглушки компоновщика разрешаются, по крайней мере, только один раз (поэтому они не должны быть косвенными?). Используют ли какие-нибудь JIT эту технику? -
Для подхода (4), если косвенный вызов в 3 в порядке, стоит ли встраивать вызовы? Если в JIT обычно используется подход (3/4), лучше ли этот вариант?
-
Для подхода (5) плохая зависимость скачка от памяти (учитывая, что адрес известен во время компиляции)? Это сделает это менее производительным, чем (3) или (4)? Используют ли какие-нибудь JIT эту технику?
-
Для подхода (6) такое фьюзинг неразумно? (Специфично для Rust) Имеется ли сегментный регистр (не используемый средой выполнения или ABI) для этой цели? Будут ли вызовы относительно сегмента non-
cs
такими же производительными, как и вызовы относительноcs
? -
И, наконец (и самое главное), есть ли лучший подход (возможно, более часто используемый JIT), который я здесь упускаю?
Я не могу реализовать (1) или (2) без ответов на мои вопросы Rust. Я мог бы, конечно, реализовать и протестировать 3-5 (возможно, 6, хотя было бы неплохо знать о смещении регистра сегментов заранее), но, учитывая, что это очень разные подходы, я надеялся, что существует литература по этому вопросу, которая Я не мог найти, потому что я не знал правильных терминов для Google (я также в настоящее время работаю над этими тестами). Или, может быть, кто-то, кто вникает во внутреннее устройство JIT, может поделиться своим опытом или тем, что он обычно видел?
Мне известен этот вопрос: переходы на JIT (x86_64). Он отличается от моего, потому что речь идет о соединении базовых блоков (и принятым решением является слишком много инструкций для часто называемого встроенного). Мне также известно о вызове абсолютного указателя в машинном коде x86, который, хотя и обсуждает схожие темы с моим, отличается, потому что я не предполагаю, что абсолютные переходы необходимы (например, подходы 1-2 позволят избежать их).