В чем разница между «asm», «__asm» и «__asm__»?
Насколько я могу судить, единственная разница между __asm { ... };
и __asm__("...");
что первый использует mov eax, var
а второй использует movl %0, %%eax
с :"=r" (var)
в конце. Какие еще существуют различия? А как насчет просто asm
?
- MSVC не расширяет __VA_ARGS__ правильно
- ошибка неразрешенного внешнего символа при соединении с OpenCV 3.0
- Как установить OpenCV 2.0 на win32
- fuken устаревшее предупреждение
- Является !! безопасный способ конвертировать в bool в C ++?
- Как установить путь к DLL-файлу в Visual Studio?
- Разница в производительности между MSVC и GCC для высоко оптимизированного матричного кода
- что означает __declspec (dllimport)?
Какой из них вы используете, зависит от вашего компилятора. Это не стандартно, как язык C.
Между MSVC inline asm и GNU C inline asm существует огромная разница. Синтаксис GCC предназначен для оптимального вывода без использования инструкций, для упаковки одной инструкции или чего-то еще. Синтаксис MSVC разработан довольно просто, но AFAICT невозможно использовать без задержки и дополнительных инструкций по обходному пути через память для ваших входов и выходов.
Если вы используете встроенный asm для повышения производительности, это делает MSVC inline asm жизнеспособным только в том случае, если вы полностью пишете весь цикл в asm, а не для обертывания коротких последовательностей встроенной функцией. Пример ниже (обертывание idiv
с помощью функции) – это то, что MSVC плохо работает: ~ 8 дополнительных инструкций по хранению / загрузке.
MSVC inline asm (используется MSVC и, возможно, icc, возможно, также доступен в некоторых коммерческих компиляторах):
- смотрит на ваш asm, чтобы выяснить, что регистрирует ваши шаги кода.
- может передавать данные только через память. Данные, которые были
mov ecx, shift_count
в регистры, хранятся компилятором, например, для подготовки вашегоmov ecx, shift_count
. Таким образом, используя единую инструкцию asm, которую компилятор не будет генерировать для вас, вы должны совершать кругооборот по памяти в пути и на выходе. - более дружелюбный к новичкам, но часто невозможно избежать накладных расходов при вводе / выводе данных . Даже помимо ограничений синтаксиса оптимизатор в текущих версиях MSVC также не подходит для оптимизации вокруг встроенных блоков asm.
GNU C inline asm не является хорошим способом изучения asm . Вы должны понять asm очень хорошо, чтобы вы могли рассказать компилятору о своем коде. И вы должны понять, что компиляторы должны знать. Этот ответ также содержит ссылки на другие руководства inline-asm и вопросы и ответы. В wiki для x86 есть много хороших вещей для asm в целом, а просто ссылки на них для GNU inline asm. (Материал в этом ответе применим также к GNU inline asm на платформах, отличных от x86).
GNU C inline asm синтаксис используется gcc, clang, icc и, возможно, некоторые коммерческие компиляторы, которые реализуют GNU C:
- Вы должны сказать компилятору, что вы clobber. Несоблюдение этого требования приведет к поломке окружающего кода неочевидными труднодоступными способами.
- Мощный, но трудный для чтения, изучения и использования синтаксиса для того, чтобы сообщать компилятору, как подавать входные данные, и где искать выходы. например,
"c" (shift_count)
, заставит компилятор поставить переменнуюshift_count
вecx
до того, как ваш inline asm будет запущен. -
лишний clunky для больших блоков кода, потому что asm должен находиться внутри строковой константы. Так что вам, как правило, нужно
"insn %[inputvar], %%reg\n\t" // comment "insn2 %%reg, %[outputvar]\n\t"
-
очень неумолимо / тяжелее, но позволяет снизить накладные расходы. для упаковки отдельных инструкций . (обертывание отдельных инструкций было оригинальным умыслом дизайна, поэтому вам нужно специально рассказать компилятору о ранних clobbers, чтобы он не мог использовать один и тот же регистр для ввода и вывода, если это проблема).
Пример: целочисленное деление ( div
) полной ширины
На 32-битном процессоре разделение 64-битного целого на 32-битное целое число или выполнение полного умножения (32×32-> 64) может выиграть от встроенного asm. gcc и clang не используют idiv
for (int64_t)a / (int32_t)b
, возможно потому, что команда не работает, если результат не подходит в 32-битном регистре. Таким образом, в отличие от этого Q & A о получении частного и остального от одного div
, это пример использования inline 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
а затем падает с конца не-void (без 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
внутри встроенного asm и сделайте это хранилищем для premainder
. premainder
, это потребует загрузки premainder
из стека в регистр перед встроенным блоком asm.
Эта функция на самом деле хуже с _vectorcall
чем с обычной ABI. С двумя входами в регистры он сохраняет их в памяти, поэтому встроенный asm может загружать их из именованных переменных. Если бы это было включено, даже больше параметров могли бы быть в regs, и им пришлось бы хранить их все, поэтому asm имел бы операнды памяти! Так что, в отличие от gcc, мы не получаем многого от этого.
Выполнение *premainder = tmp
внутри блока asm означает больше кода, написанного в asm, но не позволяет полностью сохранить магазин / путь загрузки / хранения для остатка. Это уменьшает количество команд на 2 всего, до 11 (не включая ret
).
Я пытаюсь получить лучший код из MSVC, а не «использовать его неправильно» и создать аргумент соломы. Но AFAICT это ужасно для упаковки очень коротких последовательностей. Предположительно, есть встроенная функция для разделения 64/32 -> 32, которая позволяет компилятору генерировать хороший код для этого конкретного случая, поэтому вся предпосылка использования inline asm для этого на MSVC может быть аргументом соломы . Но это показывает вам, что intrinsics намного лучше, чем встроенный asm для 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
Для 32bit vectorcall 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. (Нет необходимости резервировать пространство стека или нагрузку я предполагаю, что он все равно должен хранить около 2 из 3 входов, а затем перезагружает их внутри asm, запускает idiv
, сохраняет два выхода и перезагружает их вне asm. Таким образом, это 4 загрузки / хранения для ввода и еще 4 для вывода.)
С gcc-компилятором это не имеет большого значения. asm
или __asm
или __asm__
одинаковы, они просто используют, чтобы избежать цели пространства имен конфликта (есть функция, определенная пользователем, имя asm и т. д.),
asm
vs __asm__
в GCC
asm
не работает с -std=c99
, у вас есть две альтернативы:
- использовать
__asm__
- use
-std=gnu99
Подробнее: ошибка: «asm» uneclared (сначала использовать в этой функции)
__asm
vs __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__
который документирован.