Qual é a diferença entre ‘asm’, ‘__asm’ e ‘__asm__’?

Tanto quanto eu posso dizer, a única diferença entre __asm { ... }; e __asm__("..."); é que o primeiro usa mov eax, var e o segundo usa movl %0, %%eax com :"=r" (var) no final. Quais outras diferenças existem? E o que dizer apenas asm ?

Qual deles você usa depende do seu compilador. Isso não é padrão como a linguagem C.

Existe uma enorme diferença entre o MSVC inline asm e o GNU C inline asm. A syntax do GCC é projetada para a saída ideal sem instruções desperdiçadas, por envolver uma única instrução ou algo assim. A syntax do MSVC é projetada para ser bastante simples, mas é impossível usar o AFAICT sem a latência e instruções extras de uma viagem de ida e volta pela memory para suas inputs e saídas.

Se você estiver usando o asline inline por motivos de desempenho, isso torna o MSVC inline asm viável somente se você gravar um loop inteiro inteiramente em asm, não para agrupar seqüências curtas em uma function inline. O exemplo abaixo (envolvendo idiv com uma function) é o tipo de coisa em que o MSVC é ruim: ~ 8 instruções extras de armazenamento / carregamento.

MSVC inline asm (usado pelo MSVC e provavelmente icc, talvez também disponível em alguns compiladores comerciais):

  • olha para o seu asm para descobrir qual registra os passos do seu código.
  • só pode transferir dados via memory. Os dados que estavam mov ecx, shift_count nos registradores são armazenados pelo compilador para se preparar para o seu mov ecx, shift_count , por exemplo. Portanto, o uso de uma única instrução asm que o compilador não gera para você envolve uma viagem de ida e volta pela memory ao entrar e sair.
  • mais amigável ao iniciante, mas muitas vezes é impossível evitar sobrecarga de input / saída de dados . Mesmo além das limitações de syntax, o otimizador nas versões atuais do MSVC também não é bom para otimizar blocos em linha.

O GNU C inline asm não é uma boa maneira de aprender asm . Você tem que entender asm muito bem, então você pode dizer ao compilador sobre o seu código. E você precisa entender o que os compiladores precisam saber. Essa resposta também tem links para outros guias inline-asm e perguntas e respostas. O wiki da tag x86 tem muitas coisas boas para asm em geral, mas apenas links para o GNU inline asm. (As coisas nessa resposta são aplicáveis ​​ao GNU inline asm em plataformas não-x86, também).

A syntax inline do GNU C é usada pelo gcc, clang, icc e talvez alguns compiladores comerciais que implementam o GNU C:

  • Você tem que dizer ao compilador o que você destrói. Não fazer isso levará à quebra do código adjacente em maneiras difíceis de depurar não óbvias.
  • Poderoso, mas difícil de ler, aprender e usar a syntax para informar ao compilador como fornecer inputs e onde encontrar saídas. Por exemplo, "c" (shift_count) irá fazer com que o compilador coloque a variável shift_count em ecx antes que seu asline seja executado.
  • extra desajeitado para grandes blocos de código, porque o asm tem que estar dentro de uma constante de string. Então você normalmente precisa

     "insn %[inputvar], %%reg\n\t" // comment "insn2 %%reg, %[outputvar]\n\t" 
  • muito implacável / mais difícil, mas permite menor sobrecarga esp. para embrulhar instruções únicas . (envolver instruções únicas era a intenção original do projeto, e é por isso que você tem que dizer especialmente ao compilador sobre clobbers iniciais para impedir que ele use o mesmo registrador para uma input e saída, se isso for um problema.)


Exemplo: divisão inteira inteira ( div )

Em uma CPU de 32 bits, dividir um inteiro de 64 bits por um inteiro de 32 bits, ou fazer uma multiplicação completa (32×32-> 64), pode se beneficiar do conjunto in-line. O gcc e o clang não se aproveitam do idiv para (int64_t)a / (int32_t)b , provavelmente porque a instrução falha se o resultado não couber em um registrador de 32 bits. Portanto, ao contrário desta questão sobre como obter quociente e restante de uma div , esse é um caso de uso para o in-line asm. (A menos que haja uma maneira de informar ao compilador que o resultado vai caber, então o idiv não irá falhar.)

Usaremos convenções de chamada que colocam alguns args nos registradores (com hi mesmo no registro direito ), para mostrar uma situação mais próxima do que você veria ao inserir uma function minúscula como essa.


MSVC

Tenha cuidado com as convenções de chamada register-arg ao usar inline-asm. Aparentemente, o suporte inline-asm é tão mal projetado / implementado que o compilador pode não salvar / restaurar os registros arg ao redor do conjunto in-line, se esses argumentos não forem usados ​​no conjunto in-line . Obrigado @RossRidge por apontar isto.

 // 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 } 

Update: aparentemente deixando um valor em eax ou edx:eax e, em seguida, cair no final de uma function não-vazia (sem return ) é suportado, mesmo quando inlining . Suponho que isso funcione apenas se não houver código após a instrução asm . Isso evita armazenar / recarregar a saída (pelo menos para o quotient ), mas não podemos fazer nada sobre as inputs. Em uma function não-inline com argumentos de pilha, eles já estarão na memory, mas neste caso de uso, estamos escrevendo uma function minúscula que poderia ser útil em linha.


Compilado com MSVC 19.00.23026 /O2 no rextester (com um main() que encontra o diretório do exe e copia a saída do asm do compilador para 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 

Há uma tonelada de instruções extra mov, e o compilador nem chega perto de otimizar qualquer uma delas. Eu pensei que talvez fosse ver e entender o mov tmp, edx dentro do inline asm, e fazer disso uma loja para premainder . Mas isso exigiria o carregamento do premainder da pilha em um registrador antes do bloco asm inline, eu acho.

Esta function é realmente pior com _vectorcall que com a ABI normal de tudo na pilha. Com duas inputs em registradores, ele as armazena na memory para que o asline possa carregá-las a partir de variables ​​nomeadas. Se isso fosse embutido, mais parâmetros ainda poderiam estar nos regs, e teria que armazená-los todos, então o asm teria operandos de memory! Portanto, ao contrário do gcc, não ganhamos muito com isso.

Fazer *premainder = tmp dentro do bloco asm significa mais código escrito em asm, mas evita o caminho store / load / store totalmente braindead para o restante. Isso reduz a contagem de instruções em 2 no total, até 11 (sem include o ret ).

Eu estou tentando obter o melhor código possível de MSVC, não “use errado” e crie um argumento de homem-palha. Mas o AFAICT é horrível para embrulhar seqüências muito curtas. Presumivelmente, existe uma function intrínseca para a divisão 64/32 -> 32 que permite que o compilador gere um bom código para este caso específico, de modo que toda a premissa de usar o inline asm para isso no MSVC poderia ser um argumento palpitante . Mas isso mostra que os intrínsecos são muito melhores do que o inline asm para o MSVC.


GNU C (gcc / clang / icc)

O Gcc é ainda melhor do que a saída mostrada aqui quando se está embutindo o div64, porque normalmente ele pode organizar o código anterior para gerar o inteiro de 64 bits no edx: eax em primeiro lugar.

Não consigo fazer o gcc compilar para a ABI vectorcall de 32 bits. Clang pode, mas é uma droga in-line com restrições "rm" (tente no link de Godbolt: ele salta function arg através da memory ao invés de usar a opção register na restrição). A convenção de chamada MS de 64 bits está próxima da chamada vectorial de 32 bits, com os dois primeiros parâmetros em edx, ecx. A diferença é que mais 2 params entram em regs antes de usar a pilha (e que o receptor não lança os argumentos da pilha, que é o que o ret 8 estava na saída do 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; } 

compilado com gcc -m64 -O3 -mabi=ms -fverbose-asm . Com o -m32, você obtém apenas 3 cargas, idiv e uma loja, como você pode ver ao alterar as coisas nesse link de Godbolt.

 mov eax, ecx # lo, lo idivl r9d # divisor mov DWORD PTR [r8], edx # *premainder_7(D), rem ret 

Para 32bit vectorcall, o gcc faria algo como

 ## 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 

O MSVC usa 13 instruções (sem include o ret), comparado ao gcc 4. Com inlining, como eu disse, ele potencialmente compila para apenas um, enquanto o MSVC ainda usaria provavelmente 9. (Não será necessário reservar espaço de pilha ou carregar premainder ; Suponho que ainda tenha que armazenar cerca de 2 das 3 inputs. Em seguida, ele recarrega-as dentro do asm, executa idiv , armazena duas saídas e as recarrega fora do conjunto. Então, são 4 cargas / armazenamentos para input e outros 4 para saída.)

Com o compilador gcc, não é uma grande diferença. asm ou __asm ou __asm__ são os mesmos, eles apenas usam para evitar o propósito de namespace de conflito (não há function definida pelo usuário que nome asm, etc.)

asm vs __asm__ no GCC

asm não funciona com -std=c99 , você tem duas alternativas:

  • use __asm__
  • use -std=gnu99

Mais detalhes: error: ‘asm’ não declarado (primeiro uso nesta function)

__asm vs __asm__ no GCC

Eu não consegui encontrar onde __asm está documentado (notavelmente não mencionado em https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Alternate-Keywords.html#Alternate-Keywords ), mas a partir da fonte do GCC 8.1 Eles são exatamente os mesmos:

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

então eu usaria apenas __asm__ que está documentado.