В чем разница между «asm», «__asm» и «__asm__»? - PullRequest
31 голосов
/ 24 июля 2010

Насколько я могу судить, единственная разница между __asm { ... }; и __asm__("..."); заключается в том, что первый использует mov eax, var, а второй использует movl %0, %%eax с :"=r" (var) в конце.Какие еще различия есть?А как насчет asm?

Ответы [ 4 ]

32 голосов
/ 12 марта 2016

Существует огромная разница между встроенным asm MSVC и встроенным asm GNU C.Синтаксис GCC разработан для оптимального вывода без потраченных инструкций, для переноса одной инструкции или чего-то еще.Синтаксис MSVC спроектирован так, чтобы быть довольно простым, но AFAICT невозможно использовать без задержки и дополнительных инструкций обхода памяти для ваших входов и выходов.

Если вы используете встроенный ассемблер для повышения производительности,это делает MSVC inline asm жизнеспособным, только если вы пишете весь цикл целиком в asm, а не для упаковки коротких последовательностей во встроенную функцию.В приведенном ниже примере (упаковка idiv функцией) MSVC плохо работает: ~ 8 дополнительных инструкций сохранения / загрузки.

Встроенный asm MSVC (используется MSVC и, вероятно, icc, возможно, также доступен).в некоторых коммерческих компиляторах):

  • смотрит на ваш ассемблер, чтобы выяснить, на каких этапах регистрируется ваш код.
  • может только передавать данные через память.Данные, которые были вживыми в регистрах, хранятся компилятором, например, для подготовки к mov ecx, shift_count.Таким образом, использование одной инструкции asm, которую компилятор не сгенерирует для вас, включает обход по памяти при входе и выходе.
  • более дружественный для новичка, но часто невозможноизбежать накладных расходов при вводе / выводе данных .Даже помимо синтаксических ограничений, оптимизатор в текущих версиях MSVC также не способен оптимизировать работу со встроенными блоками asm.

GNU C встроенный asm не является хорошим способомвыучить асм .Вы должны очень хорошо понимать asm, чтобы вы могли рассказать компилятору о своем коде.И вы должны понимать, что должны знать компиляторы.Этот ответ также содержит ссылки на другие руководства inline-asm и вопросы и ответы.Вики-тег содержит много полезных вещей для asm в целом, но только ссылки на него для встроенного asm GNU.(Содержимое этого ответа также применимо к встроенному asm GNU на платформах, отличных от x86.)

Синтаксис встроенного asm GNU C используется gcc, clang, icc и, возможно, некоторыми коммерческими компиляторами, которые реализуют GNU C:

  • Вы должны сообщить компилятору, что вам нужно.Невыполнение этого требования приведет к поломке окружающего кода неочевидными трудными для отладки способами.
  • Мощный, но сложный для чтения, изучения и использования синтаксис для указания компилятору, как вводить входные данные и гденайти выходы.например, "c" (shift_count) заставит компилятор поместить переменную shift_count в ecx до запуска встроенного ассемблера.
  • очень неуклюже для больших блоков кода, потому что ассемблер должен находиться внутристроковая константа.Таким образом, вам обычно нужно

    "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-битный регистр.Таким образом, в отличие от этого 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 на 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 внутри встроенного асса и сделает это хранилищем до 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 и магазин, как вы можете видеть по изменению вещей в этой связи с Годболтом.

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 для вывода.)

16 голосов
/ 24 июля 2010

Какой вы используете, зависит от вашего компилятора.Это не стандартно, как язык C.

5 голосов
/ 08 июня 2012

С компилятором gcc это не большая разница. asm или __asm или __asm__ одинаковы, они просто используются, чтобы избежать цели пространства имен конфликтов (есть пользовательская функция с именем asm и т. Д.)

3 голосов

asm против __asm__ в GCC

asm не работает с -std=c99, у вас есть две альтернативы:

  • использование__asm__
  • использование -std=gnu99

Подробнее: ошибка: «asm» необъявлен (первое использование в этой функции)

__asm против __asm__ в GCC

Я не смог найти, где __asm задокументировано (в частности, не упоминается в https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Alternate-Keywords.html#Alternate-Keywords), но из GCC 8.1 source они точно такие же:

  { "__asm",        RID_ASM,    0 },
  { "__asm__",      RID_ASM,    0 },

, поэтому я бы просто использовал __asm__, что задокументировано.

...