Сгенерированный компилятором cmp/jcc
на acpi_version == 0
примерно такой же дешевый, как вы получите в общем случае. Он должен очень хорошо прогнозировать, потому что ветка всегда идет одинаково каждый раз, а стоимость самой ветки довольно низкая, даже если она берется каждый раз. (Полученные ветви имеют немного большую стоимость, чем неиспользованные ветви, поскольку они влияют на этапы извлечения / декодирования внешнего интерфейса и разбивают используемые части кода на несколько строк I-кэша.)
Процессоры не могут сохранять результаты сравнения каким-либо особым образом, что быстрее, чем проверка целого числа на ненулевое. (т. е. уже ноль / ненулевое целое число означает , как вы будете хранить результат сравнения! ) 1
В конкретном случае, когда две стороны вашего if
очень похожи, может быть место для экономии, но легко прогнозируемая ветвь сравнения + стоит очень дешево. Программы полны сравнения + ветки, поэтому современные процессоры должны очень хорошо их запускать. Примерно от 10 до 25% количества команд в обычной программе - это сравнение и переход, включая безусловные переходы (не указывайте точное число, я слышал это, но не смог найти надежный источник с быстрым поиском). Больше ветвей требует немного больше ресурсов прогнозирования ветвлений или в среднем ухудшает предсказание других ветвей, но это тоже небольшой эффект.
Компилятор уже выведет проверку циклов после вставки. ( Безусловно, самое важное, что вы можете здесь сделать, это убедиться, что небольшие функции доступа, такие как sdt_wrapper::get_table
, могут быть встроенными, либо помещая их в .h
, либо используя оптимизацию во время соединения ). Встроенный ассм может только усугубить ситуацию (http://gcc.gnu.org/wiki/DontUseInlineAsm), если только вы не совершите какой-нибудь супер-хак, например, не вставите метку в код asm, чтобы вы могли изменить ее или что-то в этом роде.
Если вы сравните так много, что думаете, что стоило бы хранить acpi_version
в фиксированном глобальном регистре, выделенном только для этого (глобальная переменная регистра, которую поддерживает GNU C ++, но которая, вероятно, не на самом деле будет хорошо 2 , даже если вы думаете, что это так), тогда вместо этого вы можете сделать условие параметром шаблона для всего вашего кода (или макроса static constexpr
или CPP), и построить 2 версии вашего кода: одну для true и одну для false . Когда вы узнаете значение условия при загрузке, распакуйте и восстановите страницы, содержащие версию, которая никогда не будет работать, и перейдите к версии, которая будет работать. (Или для неядерного, то есть обычной программы, выполняемой в пространстве пользователя под ОС, обычно не проблема просто оставить чистые страницы отображенными, особенно если их не трогать (в том числе путем перемещения во время выполнения)). if(acpi_version == 0) { rest_of_kernel<0>(...); } else { rest_of_kernel<1>(...); }
(исключая unmap / free part).
Если rsdt_ptr
и xsdt_ptr
являются инвариантами, вы могли бы по крайней мере устранить этот дополнительный уровень косвенности, если оба PointerToOtherSDT
массива (для краткости PTOS
) находятся в статическом хранилище.
Исправление кода один раз при запуске
Вы не помечали архитектуру, но ваша (слишком много упоминаний) asm
выглядит как x86, так что я поговорю об этом. (Все современные процессоры x86 имеют неупорядоченное выполнение и очень хороший прогноз ветвления, поэтому, вероятно, там не так уж много выгоды.)
Ядро Linux делает это, но оно сложно: например, что-то вроде .pushsection list_of_addresses_to_patch; .quad .Lthis_instance%= ; .popsection
для построения массива указателей (в виде специального раздела компоновщика) для мест, которые необходимо пропатчить, везде, где встроен оператор asm. Одно из мест, где используется этот трюк, - это исправление lock
префиксов к nop
на однопроцессорных машинах, работающих под ядром, скомпилированным с поддержкой SMP. Это исправление происходит один раз, при загрузке. (И даже может быть в состоянии исправить префиксы lock
обратно перед горячим добавлением ЦП, потому что счетчики мьютексов все еще поддерживаются.)
Фактически, Linux даже использует asm goto
и исправления между jmp
или nop
для таких неизменных условий, как ваши, которые определяются один раз при загрузке , в bool _static_cpu_has(u16 bit)
в arch /x86/include/asm/cpufeature.h. Для начала, есть блок jmp
, который выполняет обычную проверку во время выполнения, немного протестировав. Но он использует .section .altinstructions,"a"
/ .previous
, чтобы записать, где находится каждый jmp
, а также длину / местоположение патча. Он выглядит продуманно для работы с 2-байтовыми короткими по сравнению с 5-байтовыми длинными jmp rel8
/ jmp rel32
прыжками. Таким образом, ядро может исправить все места, где заканчивается этот код, заменив jmp
либо jmp
в нужном месте или nop
, чтобы перейти к метке t_yes: return true
. GCC компилирует это довольно хорошо, когда вы пишете if(_static_cpu_has(constant)) { ... }
. После исправления в какой-то момент после обнаружения функций процессора вы получаете просто NOP и попадаете в тело цикла. (Или, возможно, несколько кратких инструкций NOP, я не проверял, но, надеюсь, нет!)
Это чертовски круто, так что я просто собираюсь скопировать код, потому что интересно видеть такое творческое использование inline asm. Я не искал код, который делает исправления, но очевидно, что + скрипт компоновщика - другие ключевые части этого. Я не пытаюсь предоставить работоспособную версию этого для этого случая, просто покажи, что метод возможен , и где найти реализацию GPLv2, которую ты мог бы скопировать.
// from Linux 4.16 arch/x86/include/asm/cpufeature.h
/*
* Static testing of CPU features. Used the same as boot_cpu_has().
* These will statically patch the target code for additional
* performance.
*/
static __always_inline __pure bool _static_cpu_has(u16 bit)
{
asm_volatile_goto("1: jmp 6f\n"
"2:\n"
".skip -(((5f-4f) - (2b-1b)) > 0) * "
"((5f-4f) - (2b-1b)),0x90\n"
"3:\n"
".section .altinstructions,\"a\"\n"
" .long 1b - .\n" /* src offset */
" .long 4f - .\n" /* repl offset */
" .word %P[always]\n" /* always replace */
" .byte 3b - 1b\n" /* src len */
" .byte 5f - 4f\n" /* repl len */
" .byte 3b - 2b\n" /* pad len */
".previous\n"
".section .altinstr_replacement,\"ax\"\n"
"4: jmp %l[t_no]\n"
"5:\n"
".previous\n"
".section .altinstructions,\"a\"\n"
" .long 1b - .\n" /* src offset */
" .long 0\n" /* no replacement */
" .word %P[feature]\n" /* feature bit */
" .byte 3b - 1b\n" /* src len */
" .byte 0\n" /* repl len */
" .byte 0\n" /* pad len */
".previous\n"
".section .altinstr_aux,\"ax\"\n"
"6:\n"
" testb %[bitnum],%[cap_byte]\n"
" jnz %l[t_yes]\n"
" jmp %l[t_no]\n"
".previous\n"
: : [feature] "i" (bit),
[always] "i" (X86_FEATURE_ALWAYS),
[bitnum] "i" (1 << (bit & 7)),
[cap_byte] "m" (((const char *)boot_cpu_data.x86_capability)[bit >> 3])
: : t_yes, t_no);
t_yes:
return true;
t_no:
return false;
}
Бинарное исправление во время выполнения для вашего конкретного случая
В вашем конкретном случае разница между вашими двумя версиями заключается в том, какой глобальный (?) Указатель вы разыменовываете, и типом PTOS
. Хранить указатель на базу правого массива (например, void*
или char*
) легко с чистым C ++, но индексация по-другому сложна. В вашем случае это массив uint32_t
или uint64_t
, как гибкий элемент массива в конце структуры. (На самом деле uint32_t PTOS[1]
, потому что ISO C ++ не поддерживает гибкие элементы массива, но если вы собираетесь использовать встроенный синтаксис asm GNU, вам, вероятно, подойдет подходящий гибкий элемент массива, такой как uint32_t PTOS[]
).
На x86-64 изменение масштабного коэффициента в режиме индексированной адресации с 4 до 8 принесло бы пользу, потому что 64-битная загрузка по сравнению с расширяющейся до нуля 32-битной нагрузкой использует тот же код операции, только REX. W = 0 (или без префикса REX) против REX.W = 1 для размера операнда. .byte 0x40; mov eax, [rdx + rdi*4]
имеет ту же длину, что и mov rax, [rdx + rdi*8]
. (Байт 0x40
в первом - это префикс REX со всеми очищенными битами. Во 2-й версии требуется REX.W = 1 для размера 64-битного операнда; первый ноль расширяется до RAX путем записи EAX. Если первая версия уже нужен префикс REX для такого регистра, как r10
, у него уже будет префикс REX.) В любом случае, патчить один из них будет просто , если вы знаете, где находятся все соответствующие инструкции.
Если бы у вас была инфраструктура для записи мест для исправления, вы бы использовали ее для исправления инструкции mov
, которая получает указатель таблицы и index
в регистрах и возвращает 64-битное значение (из 32- или 64-битная загрузка). (И не забудьте фиктивный ввод, чтобы сообщить компилятору, что вы на самом деле читаете память, на которую указывает указатель таблицы, в противном случае компилятору разрешено выполнять оптимизации, которые могут нарушить ваш код как перемещение магазинов через оператор asm
). Но вы должны быть осторожны; встроенный asm может повредить оптимизации, отключив постоянное распространение (например, для index
). По крайней мере, если вы опустите volatile
, компилятору будет разрешено считать его чистой функцией входных данных и использовать CSE.
Взломать это на чистом C ++
На x86 масштабный коэффициент в адресации должен быть закодирован в инструкции. Даже с инвариантом времени выполнения вам (или компилятору) все равно потребуется сдвиг числа переменных или умножение, чтобы осуществить это без самоизменяющегося кода (который компиляторы не будут генерировать).
Сдвиг с переменным счетом стоит 3 моп на ЦП семейства Intel Sandybridge (http://agner.org/optimize/) (из-за устаревшей семантики CISC; count = 0 оставляет EFLAGS неизменным, поэтому EFLAGS является входом для счетчика переменных) сдвиги .) Если вы не позволите компилятору использовать BMI2 для shlx
(сдвиги без флага). index += foo ? 0 : index
будет условно удваивать index
(разница в количестве одного сдвига), но делать это без разветвления на x86 не стоит для условия, которое предсказывает это хорошо.
Использование сдвига с переменным счетчиком вместо режима адресации с масштабированным индексом может быть более дорогостоящим, чем хорошо предсказанная условная ветвь.
uint64_t
против uint32_t
без исправлений во время выполнения - другая проблема; одна версия должна выполнять расширяющуюся до нуля 32-разрядную загрузку, а другая должна выполнять 64-разрядную загрузку (если старшие байты не оказываются для вас всегда нулевыми?) Мы могли бы всегда делать 64-битная загрузка, а затем маска для сохранения или сброса старших 32 бит, но для этого нужна другая константа. И это может привести к снижению производительности, если нагрузка пересекает границу строки кэша (или, что еще хуже, страницы). например если бы 32-разрядное значение было последним на странице, обычная 32-разрядная загрузка просто загрузила бы его, но для загрузки данных со следующей страницы потребовалась бы 64-разрядная загрузка + маска.
Но если взять обе эти вещи вместе, это действительно того не стоит. Просто для удовольствия, вот что вы могли бы сделать: исходный код + вывод asm в проводнике компилятора Godbolt
// I'm assuming rsdt_ptr and xsdt_ptr are invariants, for simplicity
static const char *selected_PTOS;
static uint64_t opsize_mask; // 0x00000000FFFFFFFF or all-ones
static unsigned idx_scale; // 2 or 3
// set the above when the value for acpi_version is found
void init_acpi_ver(int acpi_version) {
... set the static vars;
}
// branchless but slower than branching on a very-predictable condition!
SDT::generic_sdt* sdt_wrapper::get_table (size_t index)
{
const char *addr = selected_PTOS + (index << idx_scale);
uint64_t entry = *reinterpret_cast<const uint64_t*>(addr);
entry &= opsize_mask; // zero-extend if needed
return reinterpret_cast<SDT::generic_sdt*>(entry);
}
Вывод Asm из Godbolt (с более простыми типами, так что он фактически компилируется)
get_table(unsigned long):
mov ecx, DWORD PTR idx_scale[rip]
mov rax, QWORD PTR selected_PTOS[rip] # the table
sal rdi, cl
mov rax, QWORD PTR [rax+rdi] # load the actual data we want
and rax, QWORD PTR opsize_mask[rip]
ret
При использовании встраивания и CSE компилятор может сохранять некоторые из этих значений маски и числа сдвигов в регистрах, но это все еще дополнительная работа (и связывает регистры).
И, кстати, не делает static
локальные переменные внутри функции; это заставит компилятор каждый раз проверять, выполнялась ли функция в первый раз. Быстрый путь для static local
(все, что запускается после того, как пыль осела из кода инициализации) довольно дешевый, но примерно такой же стоимости, как и то, что вы пытаетесь избежать : a ветвь с целым числом, отличным от нуля!
int static_local_example() {
static int x = ext();
return x;
}
# gcc7.3
movzx eax, BYTE PTR guard variable for static_local_example()::x[rip]
test al, al
je .L11
# x86 loads are always acquire-loads, other ISAs would need a barrier after loading the guard
mov eax, DWORD PTR static_local_example()::x[rip]
ret
Стоит рассмотреть статический указатель на функцию (в области класса или файла, а не функции), но замена условной ветви на безусловный косвенный вызов вряд ли будет выигрышной. И тогда у вас есть служебные вызовы функций (засоренные регистры, передача аргументов). Компиляторы обычно пытаются девиртуализировать обратно в условную ветвь в качестве оптимизации !
Сноска 1 : Если ваше условие было acpi_version == 4
, то MIPS мог бы сохранить одну инструкцию от сохранения результата 0/1. Вместо сравнения с флагами, имеет сравнение в регистр и инструкции ветвления, которые сравниваются с нулем или регистром, и регистр, который уже читается как ноль. Даже на x86 сравнение для нуля / не нуля экономит байт размера кода, если значение уже находится в регистре (test eax,eax
против cmp eax,4
). Это сэкономило бы больше, если бы это было результатом инструкции ALU (таким образом, ZF уже был бы установлен), но это не так.
Но большинство других архитектур сравниваются с флагами, и вы не можете загрузить их из памяти напрямую во флаги. Таким образом, вы захотите хранить статический bool
результат, только если acpi_version
было бы сравнительно дорого, например, целое число шире, чем регистр, такой как __int128
или int64_t
на 32-битной машине.
Сноска 2 : Не использовать глобальную переменную регистра для acpi_version
; это было бы глупо. Если он используется повсеместно, то, надеюсь, оптимизация во время компоновки может хорошо справиться с задачей по сравнению сравнения.
Прогноз ветвления + спекулятивное выполнение означает, что ЦП на самом деле не нужно ждать результата загрузки, когда вы ветвитесь на нем, и если вы все время читаете его, он все равно будет горячим в кеше L1d. (Спекулятивное выполнение означает, что управляющие зависимости не являются частью критического пути, при условии правильного предсказания ветвления)
PS: если вы сделали это далеко и все поняли, то вам следует рассмотреть возможность использования бинарного исправления, как это делает Linux, для нескольких часто проверяемых условий. Если нет, то, вероятно, не стоит!