Ответ 1
Какой из них вы используете, зависит от вашего компилятора. Это не стандартно, как язык C.
Насколько я могу судить, единственная разница между __asm {... };
и __asm__("...");
является то, что первый использует mov eax, var
а второй использует movl %0, %%eax
с :"=r" (var)
в конце. Какие еще различия есть? А как насчет только asm
?
Какой из них вы используете, зависит от вашего компилятора. Это не стандартно, как язык C.
Существует огромная разница между встроенным asm MSVC и встроенным asm GNU C. Синтаксис GCC разработан для оптимального вывода без потраченных инструкций, для переноса одной инструкции или чего-то еще. Синтаксис MSVC разработан, чтобы быть довольно простым, но AFAICT его невозможно использовать без задержки и дополнительных инструкций о прохождении туда-обратно памяти для ваших входов и выходов.
Если вы используете встроенный asm по соображениям производительности, это делает MSVC встроенный asm жизнеспособным только в том случае, если вы пишете весь цикл полностью в asm, а не для упаковки коротких последовательностей во встроенную функцию. Пример ниже (обертывание idiv
с функцией) - это то, что MSVC плох в: ~ 8 дополнительных инструкций сохранения/загрузки.
Встроенный asm MSVC (используется MSVC и, вероятно, icc, возможно, также доступен в некоторых коммерческих компиляторах):
mov ecx, shift_count
, например mov ecx, shift_count
. Таким образом, использование одной инструкции asm, которую компилятор не сгенерирует для вас, включает обход по памяти при входе и выходе.Встроенный asm GNU C не является хорошим способом изучения asm. Вы должны очень хорошо понимать asm, чтобы вы могли рассказать компилятору о своем коде. И вы должны понимать, что должны знать компиляторы. Этот ответ также содержит ссылки на другие руководства inline-asm и вопросы и ответы. В теге x86 есть много хороших вещей для asm в целом, но только ссылки на них для встроенного asm GNU. (Материал в этом ответе применим и к встроенному ассемблеру GNU на платформах, отличных от x86).
Встроенный синтаксис asm GNU C используется gcc, clang, icc и, возможно, некоторыми коммерческими компиляторами, которые реализуют GNU C:
"c" (shift_count)
заставит компилятор поместить переменную shift_count
в ecx
до того, как будет запущен shift_count
.очень неудобно для больших блоков кода, потому что asm должен быть внутри строковой константы. Так что вам обычно нужно
"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-битный регистр. Таким образом, в отличие от этих вопросов и ответов о получении коэффициента и остатка от одного div
, это пример использования встроенного asm. (Если нет способа сообщить компилятору, что результат будет соответствовать, так что idiv не будет ошибаться.)
Мы будем использовать соглашения о вызовах, которые помещают некоторые аргументы в регистры (с hi
даже в правильном регистре), чтобы показать ситуацию, которая ближе к тому, что вы увидели бы при добавлении крошечной функции, подобной этой.
Будьте осторожны с соглашениями о вызовах 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
внутри встроенного asm и сделает это хранилищем для premainder
. Но это потребовало бы загрузки premainder
из стека в регистр перед встроенным блоком asm, я думаю.
Эта функция на самом деле хуже с _vectorcall
чем с обычным ABI "все на стеке". Имея два входа в регистрах, он сохраняет их в памяти, чтобы встроенный ассемблер мог загрузить их из именованных переменных. Если бы это было встроено, даже в параметрах могло бы быть еще больше параметров, и он должен был бы хранить их все, поэтому у асма были бы операнды памяти! Так что в отличие от gcc, мы не сильно выигрываем от этого.
Выполнение *premainder = tmp
внутри блока asm означает, что больше кода пишется в asm, но избегает полностью пути хранения/загрузки/хранения braindead для оставшейся части. Это уменьшает общее количество команд на 2, до 11 (не считая ret
).
Я пытаюсь извлечь из MSVC наилучший код, а не "использовать его неправильно" и создать аргумент соломенного чучела. Но AFAICT это ужасно для упаковки очень коротких последовательностей. Предположительно, есть встроенная функция для деления 64/32 → 32, которая позволяет компилятору генерировать хороший код для этого конкретного случая, так что вся предпосылка использования встроенного asm для этого в MSVC может быть аргументом соломенного человека. Но это показывает, что встроенные функции намного лучше, чем встроенные ассемблеры для MSVC.
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 и магазин, как вы можете видеть по изменению вещей в этой ссылке Godbolt.
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 для вывода.)
С gcc-компилятором это не большая разница. asm
или __asm
или __asm__
одинаковы, они просто используют, чтобы избежать использования пространства имен конфликтов (там определена пользовательская функция с именем asm и т.д.).
asm
vs __asm__
в GCC
asm
не работает с -std=c99
, у вас есть две альтернативы:
__asm__
-std=gnu99
Более подробная информация: ошибка: 'asm undeclared (первое использование в этой функции)
__asm
против __asm__
в GCC
Я не мог найти, где __asm
задокументирован (особенно не упоминается на https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Alternate-Keywords.html#Alternate-Keywords), но из источника GCC 8.1 они точно такие же:
{ "__asm", RID_ASM, 0 },
{ "__asm__", RID_ASM, 0 },
поэтому я бы просто использовал __asm__
который задокументирован.