У меня была эта проблема @ на одном из моих сайтов wordpress после обновления и / или перемещения:)
Проверьте таблицу базы данных «wp_options» на «upload_path» и отредактируйте ее правильно ...
Резюме : попытайтесь выделить память рядом со статическим кодом. Но для вызовов, которые не могут дозвониться с помощью rel32
, вернитесь к call qword [rel pointer]
или встроенным mov r64,imm64
/ call r64
.
Ваш механизм 5., вероятно, лучше всего работает, если вы не можете 2. работать, но 4. это легко и должно быть хорошо. Direct call rel32
также нуждается в некотором предсказании ветвлений, но он определенно все же лучше.
Терминология: «встроенные функции», вероятно, должны быть «вспомогательными» функциями. «Внутренний» обычно означает встроенную в язык (например, значение на Фортране) или «не настоящую функцию , просто что-то, что указывает на машинную инструкцию» (C / C ++ / Rust означает как для SIMD, или как _mm_popcnt_u32()
, _pdep_u32()
или _mm_mfence()
). Ваши функции Rust собираются для компиляции с реальными функциями, которые существуют в машинном коде, который вы собираетесь вызывать с помощью call
инструкций.
Да, распределение буферов JIT в пределах + -2GiB от ваших целевых функций, очевидно, идеально, позволяя выполнять прямые вызовы rel32.
Наиболее простым было бы использование большого статического массива в BSS (который компоновщик поместит в пределах 2 ГБ вашего кода) и выделение ваших выделений из этого . (Используйте mprotect
(POSIX) или VirtualProtect
(Windows), чтобы сделать его исполняемым).
Большинство ОС (включая Linux) выполняют ленивое выделение для BSS (отображение COW на нулевой странице, выделяя только физические фреймы страницы, чтобы поддержать это выделение, когда оно записано, как mmap без MAP_POPULATE
), поэтому оно тратит впустую только виртуальное адресное пространство для массива 512 МБ в BSS, для которого используются только нижние 10 КБ.
Не делайте его больше или близким к 2 ГБ, тем не менее, потому что это оттолкнет другие вещи в BSS слишком далеко. Модель «маленького» кода по умолчанию (как описано в x86-64 System V ABI) помещает все статические адреса в пределах 2 ГБ друг от друга для адресации данных, относящихся к RIP, и вызова rel32 / jmp.
Недостаток: вам придется написать хотя бы простой распределитель памяти самостоятельно, вместо того, чтобы работать с целыми страницами с помощью mmap / munmap. Но это легко, если вам не нужно ничего освобождать. Возможно, просто сгенерируйте код, начинающийся с адреса, и обновите указатель, как только вы дойдете до конца, и выясните, как долго ваш блок кода. (Но это не многопоточность ...) В целях безопасности не забудьте проверить, когда вы доберетесь до конца этого буфера и прервитесь или вернетесь к mmap
.
Если ваши абсолютные целевые адреса находятся на низком 2 ГБ виртуального адресного пространства, используйте mmap(MAP_32BIT)
в Linux . (Например, если ваш код Rust скомпилирован в исполняемый файл не-PIE для Linux x86-64. Но это не относится к исполняемым файлам PIE ( обычно в наши дни ) или к целям в общих библиотеках. Вы можете обнаружить это во время выполнения, проверив адрес одной из ваших вспомогательных функций.)
В целом (если MAP_32BIT
не полезно / не доступно), ваша лучшая ставка, вероятно, [ 1115] без MAP_FIXED
, но с ненулевым адресом подсказки, который вы считаете свободным.
В Linux 4.17 появилась MAP_FIXED_NOREPLACE
, которая позволила бы вам легко искать ближайший неиспользуемый регион (например, шаг на 64 МБ и повторить попытку, если вы получите EEXIST
, затем запомните этот адрес, чтобы избежать поиска в следующий раз) , В противном случае вы можете проанализировать /proc/self/maps
один раз при запуске, чтобы найти не отображенное пространство рядом с отображением, содержащее адрес одной из ваших вспомогательных функций. Будет близко друг к другу.
Обратите внимание, что более старые ядра, которые не распознают флаг
blockquote>MAP_FIXED_NOREPLACE
, обычно (при обнаружении коллизии с существующим отображением) возвращаются к типу поведения «не MAP_FIXED»: они возвращают адрес, который отличается от запрошенного адреса.На следующих более высоких или более низких свободных страницах было бы идеально иметь непарную карту памяти, чтобы таблице страниц не требовалось слишком много различных каталогов страниц верхнего уровня. (Таблицы страниц HW представляют собой основополагающее дерево.) И как только вы найдете место, которое работает, сделайте будущие распределения непрерывными с этим. Если вы в конечном итоге используете там много места, ядро может оппортунистически использовать огромную страницу размером в 2 МБ, и если ваши страницы снова будут смежными, это означает, что они совместно используют один и тот же каталог родительских страниц в таблицах страниц HW, поэтому iTLB может пропустить запуск страниц . немного дешевле (если эти более высокие уровни остаются горячими в кэшах данных или даже кешируются внутри самого оборудования Pagewalk). И для эффективного для ядра, чтобы отслеживать как одно большее отображение. Конечно, использование большего количества уже выделенной страницы еще лучше, если есть место. Лучшая плотность кода на уровне страницы помогает инструкции TLB, и, возможно, также на странице DRAM (но это не обязательно тот же размер, что и на странице виртуальной памяти).
Затем, когда вы выполняете генерацию кода для каждого вызова, просто проверяет , находится ли цель в диапазоне для
call rel32
сoff == (off as i32) as i64
[ 1187] остальное отступает до 10-байтовыхmov r64,imm64
/call r64
. (rustcc скомпилирует это вmovsxd
/cmp
, поэтому проверка каждый раз имеет только тривиальную стоимость для времени компиляции JIT.)(или 5-байтовый
mov r32,imm32
, если это возможно. Операционные системы, которые этого не делают) supportMAP_32BIT
может по-прежнему иметь целевые адреса там внизу. Проверьте это с помощьюtarget == (target as u32) as u64
. Третьеmov
-обеспечивающее кодирование, 7-байтовоеmov r/m64, sign_extended_imm32
, вероятно, неинтересно, если вы не используете JIT-код ядра для ядра. отображается в высоком 2GiB виртуального адресного пространства.)Прелесть проверки и использования прямого вызова всегда, когда это возможно, заключается в том, что он не связывает код-ген с любыми сведениями о размещении соседних страниц или откуда поступают адреса, и просто оппортунистически делает хороший код. (Вы можете записать счетчик или войти в систему один раз, чтобы вы / ваши пользователи по крайней мере заметили, что ваш ближайший механизм распределения не работает, потому что производительность сравнения обычно не может быть легко измерима.)
Альтернативы mov -imm / call reg
mov r64,imm64
- это 10-байтовая инструкция, которая немного велика для выборки / декодирования и для хранения uop-кэша. И может потребоваться дополнительный цикл для чтения из кэша UOP в семействе SnB, согласно микроарху pdf Агнера Фога ( https://agner.org/optimize ). Но современные процессоры имеют довольно хорошую пропускную способность для выборки кода и надежные внешние интерфейсы.Если профилирование обнаружит, что узкие места внешнего интерфейса являются большой проблемой в вашем коде, или большой размер кода вызывает вытеснение другого ценного кода из I-кэша L1, я бы выбрал вариант 5.
Кстати, если какая-либо из ваших функций является переменной, x86-64 System V требует, чтобы вы передали AL = количество аргументов XMM, вы можете использовать
r11
для указателя функции. Он закрыт и не используется для передачи аргументов. Но RAX (или другой «устаревший» регистр) сохранит префикс REX вcall
.
blockquote>
- Распределить функции Rust рядом с тем местом, где
mmap
будет выделятьНет, я не думаю, что есть какой-либо механизм для статической компиляции функции рядом с тем местом, где может оказаться
mmap
для размещения новых страниц.
mmap
имеет более 4 ГБ свободного виртуального адресного пространства для выбора. Вы не знаете заранее, где он будет выделяться. (Хотя я думаю, что Linux, по крайней мере, сохраняет некоторую локальность для оптимизации таблиц страниц HW.)Теоретически вы могли бы скопировать машинный код ваших функций Rust, но они, вероятно, другой статический код / данные с режимами RIP-относительной адресации.
call rel32
для заглушек, использующихmov
/jmp reg
Похоже, это отрицательно скажется на производительности (возможно, мешает с прогнозом RAS / адреса перехода).
blockquote>Недостатком перфекта является только наличие 2 общих инструкций по вызову / прыжку, которые передний интерфейс должен пройти, прежде чем он сможет накормить внутренний конец полезными инструкциями. Это не здорово; 5. намного лучше.
Это в основном то, как PLT работает для вызовов функций совместно используемой библиотеки в Unix / Linux, и будет выполнять то же самое . Вызов через PLT (таблица связывания процедур) Функция заглушки почти так же, как это. Таким образом, влияние на производительность было хорошо изучено и сопоставлено с другими способами работы. Мы знаем, что динамические вызовы библиотек не являются причиной падения производительности.
Звездочка перед адресом и инструкциями push, куда она направляется? показывает разбор AT & T одного или одношаговую C-программу, такую как
main(){puts("hello"); puts("world");}
, если вам интересно. (При первом вызове он выдвигает аргумент arg и переходит к функции отложенного динамического компоновщика; при последующих вызовах целью косвенного перехода является адрес функции в общей библиотеке.)Почему PLT существует в дополнение к GOT, вместо того, чтобы просто использовать GOT? объясняет больше.
jmp
, адрес которого обновляется посредством ленивых ссылок, являетсяjmp qword [xxx@GOTPLT]
. (И да, PLT действительно использует косвенную памятьjmp
здесь, даже на i386, где перезаписанныйjmp rel32
будет работать. IDK, если GNU / Linux когда-либо исторически использовался для перезаписи смещения вjmp rel32
. )
jmp
- это просто стандартный хвостовой вызов, и не нарушает дисбаланс стека предикторов обратного адреса . Возможное значениеret
в целевой функции будет возвращаться к инструкции после исходногоcall
, то есть по адресу, которыйcall
помещен в стек вызовов и на микроархитектурный RAS. Только если вы использовали push / ret (например, «retpoline» для смягчения Призрака), вы бы разбалансировали RAS.Но код в Jump for JIT (x86_64) , который вы связали, к сожалению, ужасен (см. Мой комментарий под ним). Это будет сломать RAS для будущих возвратов. Вы могли бы подумать, что он сломает его только для этого вызова (чтобы получить адрес возврата, который нужно скорректировать), если балансировать push / ret, но на самом деле
call +0
это особый случай, который не идет на RAS в большинстве процессоров: http://blog.stuffedcow.net/2018/04/ras-microbenchmarks . (Обращение кnop
может измениться, я думаю, но все это совершенно безумие противcall rax
, если только он не пытается защитить от эксплойтов Spectre.) Обычно на x86-64 вы используете REA-относительный LEA, чтобы получить соседний адрес в регистре, а неcall/pop
.
blockquote>
- inline
mov r64, imm64
/call reg
Это, вероятно, лучше, чем 3; Стоимость внешнего интерфейса с большим размером кода, вероятно, ниже, чем стоимость вызова через заглушку, которая использует
jmp
.Но это также, вероятно, достаточно хорошо , особенно если ваши методы alloc-inside-2GiB работают достаточно хорошо большую часть времени на большинстве целей, которые вас интересуют.
Однако могут быть случаи, когда он медленнее, чем 5. Предсказание ветвей скрывает задержку выборки и проверки указателя функции из памяти, предполагая, что он хорошо предсказывает. (И обычно это происходит, или же это происходит так редко, что это не имеет отношения к производительности.)
blockquote>
call qword [rel nearby_func_ptr]
Вот как
[ 11141]gcc -fno-plt
компилирует вызовы функций совместно используемых библиотек в Linux (call [rip + symbol@GOTPCREL]
), и как обычно выполняются вызовы функций Windows DLL. (Это похоже на одно из предложений в http://www.macieira.org/blog/2012/01/sorry-state-of-dynamic-libraries-on-linux/ )call [RIP-relative]
имеет размер 6 байтов, всего на 1 байт больше, чемcall rel32
, поэтому он оказывает незначительное влияние на размер кода по сравнению с вызовом заглушки. Интересный факт: иногда вы видитеaddr32 call rel32
в машинном коде (префикс размера адреса не имеет никакого эффекта, кроме заполнения). Это происходит из-за того, что компоновщик ослабляетcall [RIP + symbol@GOTPCREL]
доcall rel32
, если во время компоновки был обнаружен символ с невидимой видимостью ELF в другом.o
, а не в другом общем объекте.
Для вызовов из общей библиотеки это обычно лучше, чем заглушки PLT, с единственным недостатком - более медленный запуск программы, поскольку она требует раннего связывания (не ленивое динамическое связывание). Это не проблема для вас; целевой адрес известен раньше времени генерации кода.
Автор патча проверил его производительность против традиционный PLT на неизвестном оборудовании x86-64. Clang, возможно, является наихудшим сценарием для вызовов совместно используемой библиотеки, поскольку он выполняет много вызовов небольших функций LLVM, которые не занимают много времени, и он долго выполняется, поэтому издержки при раннем связывании при запуске незначительны. После использования
gcc
иgcc -fno-plt
для компиляции clang время дляclang -O2 -g
для компиляции tramp3d увеличивается с 41,6 с (PLT) до 36,8 с (-fno-plt).clang --help
становится немного медленнее.(в заглушках x86-64 PLT используются
jmp qword [symbol@GOTPLT]
, а неmov r64,imm64
/jmp
. Косвенная памятьjmp
- это всего лишь один шаг на современных процессорах Intel, поэтому при правильном прогнозировании это дешевле, но может быть медленнее при неправильном предсказании, особенно если запись GOTPLT отсутствует в кеше. Однако, если она используется часто, она обычно предсказывает правильно, но в любом случае 10-байтовыйmovabs
и 2-байтовыйjmp
могут быть извлечены как block (если он умещается в 16-байтовом выровненном блоке выборки) и декодировать за один цикл, поэтому 3. не является абсолютно необоснованным. Но это лучше.)При выделении места для вашего указатели, помните, что они извлекаются как данные в кэш L1d , и с записью dTLB, а не iTLB. Не чередуйте их с кодом, который будет тратить пространство в I-кэше на эти данные, и тратить пространство в D-кэше на строки, содержащие один указатель и в основном код. Сгруппируйте ваши указатели вместе в отдельном 64-байтовом фрагменте из кода, чтобы строка не обязательно находилась как в L1I, так и в L1D. Хорошо, если они находятся на той же странице , что и некоторый код; они доступны только для чтения, поэтому не будут вызывать нюки конвейера с самоизменяющимся кодом.