В чем разница между "asm", "__asm" и "__asm__"?
Насколько я могу судить, единственная разница между __asm { ... };
а также __asm__("...");
является то, что первый использует mov eax, var
а второй использует movl %0, %%eax
с :"=r" (var)
в конце. Какие еще различия есть? А как насчет просто asm
?
4 ответа
Какой вы используете, зависит от вашего компилятора. Это не стандартно, как язык Си.
Существует огромная разница между встроенным asm MSVC и встроенным asm GNU C. Синтаксис GCC разработан для оптимального вывода без потраченных инструкций, для переноса одной инструкции или чего-то еще. Синтаксис MSVC разработан, чтобы быть довольно простым, но AFAICT невозможно использовать без задержки и дополнительных инструкций прохождения туда-обратно памяти для ваших входов и выходов.
Если вы используете встроенный asm по соображениям производительности, это делает MSVC встроенный asm жизнеспособным только в том случае, если вы пишете весь цикл полностью в asm, а не для упаковки коротких последовательностей во встроенную функцию. Пример ниже (упаковка idiv
с функцией) - это то, что плохо в MSVC: ~8 дополнительных инструкций сохранения / загрузки.
Встроенный asm MSVC (используется MSVC и, вероятно, icc, возможно, также доступен в некоторых коммерческих компиляторах):
- смотрит на ваш ассемблер, чтобы выяснить, какие шаги регистрирует ваш код.
- может передавать данные только через память. Данные, которые были записаны в регистрах, хранятся компилятором для подготовки к
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
до того, как ваш встроенный асм запускается. очень неудобно для больших блоков кода, потому что asm должен быть внутри строковой константы. Так что вам обычно нужно
"insn %[inputvar], %%reg\n\t" // comment "insn2 %%reg, %[outputvar]\n\t"
очень неумолимый / тяжелый, но позволяет снизить накладные расходы. для упаковки отдельных инструкций. (Завершение отдельных инструкций было первоначальной целью разработки, поэтому вы должны специально сообщить компилятору о ранних клобберах, чтобы он не использовал тот же регистр для ввода и вывода, если это проблема.)
Пример: целочисленное деление на полную ширину (div
)
На 32-битном процессоре деление 64-битного целого на 32-битное или полное умножение (32x32->64) может принести пользу от встроенного asm. 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
на тестере (с 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 может, но это полный отстой "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 для вывода.)
asm
против __asm__
в GCC
asm
не работает с -std=c99
У вас есть две альтернативы:
- использование
__asm__
- использование
-std=gnu99
Подробнее: error: 'asm' undeclared (первое использование в этой функции)
__asm
против __asm__
в GCC
Я не мог найти где __asm
задокументировано (в частности, не упомянуто по адресу https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Alternate-Keywords.html), но из источника GCC 8.1 они точно такие же:
{ "__asm", RID_ASM, 0 },
{ "__asm__", RID_ASM, 0 },
так что я бы просто использовал __asm__
который задокументирован.
С компилятором gcc это не большая разница. asm
или же __asm
или же __asm__
одинаковы, они просто используются, чтобы избежать конфликта в пространстве имен (есть пользовательская функция с именем asm и т. д.)