Существует огромная разница между встроенным asm MSVC и встроенным asm GNU C.Синтаксис GCC разработан для оптимального вывода без потраченных инструкций, для переноса одной инструкции или чего-то еще.Синтаксис MSVC спроектирован так, чтобы быть довольно простым, но AFAICT невозможно использовать без задержки и дополнительных инструкций обхода памяти для ваших входов и выходов.
Если вы используете встроенный ассемблер для повышения производительности,это делает MSVC inline asm жизнеспособным, только если вы пишете весь цикл целиком в asm, а не для упаковки коротких последовательностей во встроенную функцию.В приведенном ниже примере (упаковка idiv
функцией) MSVC плохо работает: ~ 8 дополнительных инструкций сохранения / загрузки.
Встроенный asm MSVC (используется MSVC и, вероятно, icc, возможно, также доступен).в некоторых коммерческих компиляторах):
- смотрит на ваш ассемблер, чтобы выяснить, на каких этапах регистрируется ваш код.
- может только передавать данные через память.Данные, которые были вживыми в регистрах, хранятся компилятором, например, для подготовки к
mov ecx, shift_count
.Таким образом, использование одной инструкции asm, которую компилятор не сгенерирует для вас, включает обход по памяти при входе и выходе. - более дружественный для новичка, но часто невозможноизбежать накладных расходов при вводе / выводе данных .Даже помимо синтаксических ограничений, оптимизатор в текущих версиях MSVC также не способен оптимизировать работу со встроенными блоками asm.
GNU C встроенный asm не является хорошим способомвыучить асм .Вы должны очень хорошо понимать asm, чтобы вы могли рассказать компилятору о своем коде.И вы должны понимать, что должны знать компиляторы.Этот ответ также содержит ссылки на другие руководства inline-asm и вопросы и ответы.Вики-тег x86 содержит много полезных вещей для asm в целом, но только ссылки на него для встроенного asm GNU.(Содержимое этого ответа также применимо к встроенному asm GNU на платформах, отличных от x86.)
Синтаксис встроенного asm GNU C используется gcc, clang, icc и, возможно, некоторыми коммерческими компиляторами, которые реализуют GNU C:
- Вы должны сообщить компилятору, что вам нужно.Невыполнение этого требования приведет к поломке окружающего кода неочевидными трудными для отладки способами.
- Мощный, но сложный для чтения, изучения и использования синтаксис для указания компилятору, как вводить входные данные и гденайти выходы.например,
"c" (shift_count)
заставит компилятор поместить переменную shift_count
в ecx
до запуска встроенного ассемблера. очень неуклюже для больших блоков кода, потому что ассемблер должен находиться внутристроковая константа.Таким образом, вам обычно нужно
"insn %[inputvar], %%reg\n\t" // comment
"insn2 %%reg, %[outputvar]\n\t"
очень неумолимо / тяжелее, но позволяет снизить накладные расходы.для упаковки отдельных инструкций .(Завершение отдельных инструкций было первоначальной целью проекта, поэтому вы должны специально сообщить компилятору о ранних клобберах, чтобы он не использовал тот же регистр для ввода и вывода, если это проблема.)
Пример: целочисленное деление на полную ширину (div
)
На 32-битном ЦП, деление 64-битного целого числа на 32-битное целое или полное умножение (32x32-> 64)), может извлечь выгоду из встроенного ассемблера.gcc и clang не используют idiv
для (int64_t)a / (int32_t)b
, вероятно, из-за ошибки команды, если результат не помещается в 32-битный регистр.Таким образом, в отличие от этого Q & A о получении коэффициента и остатка от одного div
, это вариант использования для встроенного ассемблера.(Если нет способа сообщить компилятору, что результат будет соответствовать, поэтому idiv не будет ошибаться.)
Мы будем использовать соглашения о вызовах, которые помещают некоторые аргументы в регистры (с hi
даже в вправо регистр), чтобы показать ситуацию, которая ближе к тому, что вы увидели бы при добавлении крошечной функции, подобной этой.
MSVC
Будьте внимательны с соглашениями о вызовах register-arg при использовании inline-asm. Очевидно, поддержка inline-asm настолько плохо спроектирована / реализована, что компилятор может не сохранять / восстанавливать регистры arg вокруг встроенного asm, если эти аргументы не используются во встроенном asm . Спасибо @RossRidge за указание на это.
// MSVC. Be careful with _vectorcall & inline-asm: see above
// we could return a struct, but that would complicate things
int _vectorcall div64(int hi, int lo, int divisor, int *premainder) {
int quotient, tmp;
__asm {
mov edx, hi;
mov eax, lo;
idiv divisor
mov quotient, eax
mov tmp, edx;
// mov ecx, premainder // Or this I guess?
// mov [ecx], edx
}
*premainder = tmp;
return quotient; // or omit the return with a value in eax
}
Обновление: по-видимому, оставляя значение в eax
или edx:eax
и затем опуская конец недействительной функции (без return
), поддерживается , даже при встраивании . Я предполагаю, что это работает, только если после оператора asm
нет кода. Это позволяет избежать сохранения / перезагрузки для вывода (по крайней мере, для quotient
), но мы ничего не можем сделать с входами. В не встроенной функции с аргументами стека они уже будут в памяти, но в этом сценарии использования мы пишем крошечную функцию, которая может с пользой встроиться.
Скомпилировано с MSVC 19.00.23026 /O2
на rextester (с main()
, который находит каталог exe, и выводит вывод asm компилятора в stdout ).
## My added comments use. ##
; ... define some symbolic constants for stack offsets of parameters
; 48 : int ABI div64(int hi, int lo, int divisor, int *premainder) {
sub esp, 16 ; 00000010H
mov DWORD PTR _lo$[esp+16], edx ## these symbolic constants match up with the names of the stack args and locals
mov DWORD PTR _hi$[esp+16], ecx
## start of __asm {
mov edx, DWORD PTR _hi$[esp+16]
mov eax, DWORD PTR _lo$[esp+16]
idiv DWORD PTR _divisor$[esp+12]
mov DWORD PTR _quotient$[esp+16], eax ## store to a local temporary, not *premainder
mov DWORD PTR _tmp$[esp+16], edx
## end of __asm block
mov ecx, DWORD PTR _premainder$[esp+12]
mov eax, DWORD PTR _tmp$[esp+16]
mov DWORD PTR [ecx], eax ## I guess we should have done this inside the inline asm so this would suck slightly less
mov eax, DWORD PTR _quotient$[esp+16] ## but this one is unavoidable
add esp, 16 ; 00000010H
ret 8
Существует множество дополнительных команд mov, и компилятор даже близко не приблизился к их оптимизации. Я подумал, что, может быть, он увидит и поймет mov tmp, edx
внутри встроенного асса и сделает это хранилищем до premainder
. Но это потребовало бы загрузки premainder
из стека в регистр перед встроенным блоком asm, я думаю.
Эта функция на самом деле хуже с _vectorcall
, чем с обычным ABI "все в стеке". Имея два входа в регистрах, он сохраняет их в памяти, чтобы встроенный ассемблер мог загрузить их из именованных переменных. Если бы это было встроено, в регистрах могло бы быть еще больше параметров, и он должен был бы хранить их все, так что у асма были бы операнды памяти! Так что, в отличие от gcc, мы не сильно выиграем от этого.
Выполнение *premainder = tmp
внутри блока asm означает больше кода, написанного в asm, но исключает полностью путь хранения / загрузки / сохранения braindead для оставшейся части. Это уменьшает количество команд на 2 всего, до 11 (не включая ret
).
Я пытаюсь получить наилучший код из MSVC, а не «использовать его неправильно» и создать аргумент «соломенный человек». Но AFAICT это ужасно для того, чтобы оборачивать очень короткие последовательности. Предположительно, есть встроенная функция для деления 64/32 -> 32, которая позволяет компилятору генерировать хороший код для этого конкретного случая, так что вся предпосылка использования встроенного asm для этого в MSVC может быть аргументом соломенного человека . Но это показывает, что встроенные функции намного лучше, чем встроенные ассемблеры для MSVC.
GNU C (gcc / clang / icc)
Gcc работает даже лучше, чем вывод, показанный здесь при вставке в div64, потому что он обычно может организовать предыдущий код для генерации 64-битного целого числа в edx: eax в первую очередь.
Я не могу заставить gcc скомпилировать для 32-битного векторного вызова ABI. Clang может, но он сосет при встроенном asm с ограничениями "rm"
(попробуйте это по ссылке Godbolt: он перенаправляет функцию arg через память вместо использования параметра register в ограничении). Соглашение о вызовах 64-битной MS близко к 32-битному векторному вызову, первые два параметра в edx, ecx. Разница в том, что еще 2 параметра передаются в регистры перед использованием стека (и что вызываемый объект не выталкивает аргументы из стека, как это было в ret 8
в выводе MSVC.)
// GNU C
// change everything to int64_t to do 128b/64b -> 64b division
// MSVC doesn't do x86-64 inline asm, so we'll use 32bit to be comparable
int div64(int lo, int hi, int *premainder, int divisor) {
int quotient, rem;
asm ("idivl %[divsrc]"
: "=a" (quotient), "=d" (rem) // a means eax, d means edx
: "d" (hi), "a" (lo),
[divsrc] "rm" (divisor) // Could have just used %0 instead of naming divsrc
// note the "rm" to allow the src to be in a register or not, whatever gcc chooses.
// "rmi" would also allow an immediate, but unlike adc, idiv doesn't have an immediate form
: // no clobbers
);
*premainder = rem;
return quotient;
}
скомпилировано с gcc -m64 -O3 -mabi=ms -fverbose-asm
. С -m32 вы просто получаете 3 груза, idiv и магазин, как вы можете видеть по изменению вещей в этой связи с Годболтом.
mov eax, ecx # lo, lo
idivl r9d # divisor
mov DWORD PTR [r8], edx # *premainder_7(D), rem
ret
Для 32-битного векторного вызова gcc будет делать что-то вроде
## Not real compiler output, but probably similar to what you'd get
mov eax, ecx # lo, lo
mov ecx, [esp+12] # premainder
idivl [esp+16] # divisor
mov DWORD PTR [ecx], edx # *premainder_7(D), rem
ret 8
MSVC использует 13 инструкций (не считая ret), по сравнению с gcc 4. При вставке, как я уже сказал, он потенциально компилируется в одну, тогда как MSVC все равно будет использовать, вероятно, 9. (Не нужно резервировать стек пробел или загрузка premainder
; я предполагаю, что ему все еще нужно хранить около 2 из 3 входов. Затем он перезагружает их внутри asm, запускает idiv
, сохраняет два выхода и перезагружает их вне asm. Так что это 4 загружает / сохраняет для ввода и еще 4 для вывода.)