Насколько я могу судить, единственная разница между __asm {... };
и __asm__("...");
является то, что первый использует mov eax, var
а второй использует movl %0, %%eax
с :"=r" (var)
в конце. Какие еще различия есть? А как насчет только asm
?
В чем разница между "asm", "__asm" и "__asm__"?
Ответ 1
Какой из них вы используете, зависит от вашего компилятора. Это не стандартно, как язык C.
Ответ 2
Существует огромная разница между встроенным 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, которую компилятор не сгенерирует для вас, включает обход по памяти при входе и выходе. - более дружественный для новичка, но часто невозможный избежать ввода/вывода данных. Даже помимо синтаксических ограничений, оптимизатор в текущих версиях MSVC также не способен оптимизировать работу со встроенными 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
даже в правильном регистре), чтобы показать ситуацию, которая ближе к тому, что вы увидели бы при добавлении крошечной функции, подобной этой.
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
внутри встроенного asm и сделает это хранилищем для 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 и магазин, как вы можете видеть по изменению вещей в этой ссылке 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 для вывода.)
Ответ 3
С gcc-компилятором это не большая разница. asm
или __asm
или __asm__
одинаковы, они просто используют, чтобы избежать использования пространства имен конфликтов (там определена пользовательская функция с именем asm и т.д.).
Ответ 4
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__
который задокументирован.