В чем разница между "asm", "__asm" и "__asm__"?

Насколько я могу судить, единственная разница между __asm {... }; и __asm__("..."); является то, что первый использует mov eax, var а второй использует movl %0, %%eax с :"=r" (var) в конце. Какие еще различия есть? А как насчет только 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 и вопросы и ответы. В теге есть много хороших вещей для 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 на rextestermain() который находит каталог 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__ который задокументирован.