Qual è la differenza tra “asm”, “__asm” e “__asm__”?

Per quanto ne so, l’unica differenza tra __asm { ... }; e __asm__("..."); è che il primo usa mov eax, var e il secondo usa movl %0, %%eax con :"=r" (var) alla fine. Quali altre differenze ci sono? E per quanto riguarda solo asm ?

Quello che usi dipende dal tuo compilatore. Questo non è standard come il linguaggio C.

C’è una grande differenza tra MSVC in linea asm e GNU C in linea asm. La syntax GCC è progettata per un output ottimale senza istruzioni inutili, per il wrapping di una singola istruzione o qualcosa del genere. La syntax MSVC è progettata per essere abbastanza semplice, ma AFAICT è imansible da usare senza la latenza e le istruzioni extra di un round trip attraverso la memoria per i tuoi input e output.

Se si utilizza asm in linea per motivi di prestazioni, ciò rende MSVC in linea asm ansible solo se si scrive un intero ciclo interamente in asm, non per il wrapping di brevi sequenze in una funzione inline. L’esempio seguente (il wrapping di idiv con una funzione) è il tipo di cosa a cui MSVC non va bene: ~ 8 extra store / istruzioni di caricamento.

MSVC in linea asm (usato da MSVC e probabilmente icc, forse anche disponibile in alcuni compilatori commerciali):

  • guarda il tuo ASM per capire su quale registro registri il tuo codice.
  • può solo trasferire dati tramite memoria. I dati che erano in diretta nei registri sono memorizzati dal compilatore per preparare il tuo mov ecx, shift_count , ad esempio. Quindi, usare una singola istruzione asm che il compilatore non genererà per te implica un viaggio di andata e ritorno nella memoria lungo la via d’ingresso e d’uscita.
  • più adatto ai principianti, ma spesso imansible evitare i costi generali per ottenere i dati in entrata / uscita . Anche oltre alle limitazioni della syntax, l’ottimizzatore nelle attuali versioni di MSVC non è in grado di ottimizzare i blocchi asm inline.

GNU C in linea asm non è un buon modo per imparare asm . Devi capire molto bene asm in modo da poter dire al compilatore del tuo codice. E devi capire cosa devono sapere i compilatori. Questa risposta ha anche collegamenti con altre guide in linea e domande e risposte. Il wiki del tag x86 ha un sacco di cose buone per ASM in generale, ma solo collegamenti a questo per GNU in linea asm. (La roba in quella risposta è applicabile anche a GNU in linea asm su piattaforms non x86).

La syntax inline asm di GNU C è usata da gcc, clang, icc e forse alcuni compilatori commerciali che implementano GNU C:

  • Devi dire al compilatore cosa stai sbavando. In caso contrario, si verificherà la rottura del codice circostante in modi non ovvi e difficili da debugare.
  • Potente ma difficile da leggere, imparare e usare la syntax per dire al compilatore come fornire gli input e dove trovare gli output. per esempio "c" (shift_count) farà in modo che il compilatore metta la variabile ecx in ecx prima che il tuo asm in linea sia eseguito.
  • extra clunky per grandi blocchi di codice, perché asm deve essere all’interno di una costante di stringa. Quindi di solito hai bisogno

     "insn %[inputvar], %%reg\n\t" // comment "insn2 %%reg, %[outputvar]\n\t" 
  • molto spietato / più duro, ma consente di es. per il confezionamento di singole istruzioni . (il wrapping di istruzioni singole era l’intento progettuale originale, ed è per questo che devi dire al compilatore in modo specifico dei primi clobbers per impedirgli di usare lo stesso registro per un input e output se questo è un problema.)


Esempio: divisione intero a larghezza intera ( div )

In una CPU a 32 bit, la divisione di un intero a 64 bit con un numero intero a 32 bit o di un multiplo completo (32×32-> 64) può trarre vantaggio da asm in linea. gcc e clang non approfittano di idiv per (int64_t)a / (int32_t)b , probabilmente perché l’istruzione fa un errore se il risultato non si adatta a un registro a 32 bit. Quindi, a differenza di questo Q & A sull’ottenere il quoziente e il resto da un div , questo è un caso d’uso per in linea asm. (A meno che non ci sia un modo per informare il compilatore che il risultato andrà bene, quindi idiv non avrà colpa.)

Useremo convenzioni di chiamata che inseriscono alcuni argomenti nei registri (con hi anche nel registro di destra ), per mostrare una situazione più vicina a ciò che si vedrebbe quando si insinua una funzione minuscola come questa.


MSVC

Fai attenzione con le convenzioni di call register-arg quando usi inline-asm. Apparentemente il supporto in linea-asm è progettato e implementato in modo così sfavorevole che il compilatore potrebbe non salvare / ripristinare i registri di arg attorno alla linea asm, se questi argomenti non sono usati in linea asm . Grazie a @RossRidge per averlo indicato.

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

Aggiornamento: apparentemente lasciando un valore in eax o edx:eax e quindi cadendo dalla fine di una funzione non vuota (senza un return ) è supportato, anche quando si esegue l’inlining . Presumo che funzioni solo se non c’è alcun codice dopo l’istruzione asm . Questo evita lo store / ricarica per l’output (almeno per il quotient ), ma non possiamo fare nulla per gli input. In una funzione non in linea con stack arg, saranno già in memoria, ma in questo caso d’uso stiamo scrivendo una piccola funzione che potrebbe essere utile inline.


Compilato con MSVC 19.00.23026 /O2 su rextester (con un main() che trova la directory di exe e scarica l’output asm del compilatore su 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 

Ci sono un sacco di istruzioni di movimento extra, e il compilatore non si avvicina nemmeno ad ottimizzarle. Ho pensato che forse avrebbe visto e capito il mov tmp, edx all’interno della linea asm, e reso quel negozio premainder . Ma ciò richiederebbe il caricamento del premainder dalla pila in un registro prima del blocco asm in linea, suppongo.

Questa funzione è in realtà peggiore con _vectorcall che con il normale ABI tutto-in-the-stack. Con due ingressi in registri, li memorizza in memoria in modo che l’asm in linea possa caricarli da variabili denominate. Se questo fosse in linea, ancora più parametri potrebbero essere nei regs, e dovrebbe memorizzarli tutti, quindi gli assi avrebbero degli operandi di memoria! Quindi, a differenza di gcc, non guadagniamo molto dall’integrazione di questo.

Fare *premainder = tmp all’interno del blocco asm significa più codice scritto in asm, ma evita il percorso store / load / store totalmente braindead per il resto. Ciò riduce il conteggio delle istruzioni di 2 totali, fino a 11 (escluso il ret ).

Sto cercando di ottenere il miglior codice ansible da MSVC, non di “usarlo male” e di creare un argomento da uomo di paglia. Ma AFAICT è orribile per avvolgere sequenze molto brevi. Presumibilmente esiste una funzione intrinseca per la divisione 64/32 -> 32 che consente al compilatore di generare un buon codice per questo caso particolare, quindi l’intera premessa di utilizzare asm in linea per questo su MSVC potrebbe essere un argomento da uomo di paglia . Ma mostra che le intrinseche sono molto meglio di inline asm per MSVC.


GNU C (gcc / clang / icc)

Gcc fa anche meglio dell’output mostrato qui quando si incorpora div64, perché in genere può organizzare il codice precedente per generare il numero intero a 64 bit in edx: eax in primo luogo.

Non riesco a ottenere gcc per compilare l’ABI vectorcall a 32 bit. Clang può, ma fa schifo in linea con i vincoli "rm" (provalo sul link Godbolt: rimbalza la funzione arg attraverso la memoria invece di usare l’opzione register nel vincolo). La convenzione di chiamata MS a 64 bit si avvicina alla chiamata vettoriale a 32 bit, con i primi due parametri in edx, ecx. La differenza è che altri 2 parametri entrano in regs prima di usare lo stack (e che il callee non fa scoppiare gli args dallo stack, che è ciò che riguardava il ret 8 nell’output di 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; } 

compilato con gcc -m64 -O3 -mabi=ms -fverbose-asm . Con -m32 hai solo 3 carichi, idiv e un negozio, come puoi vedere cambiando roba in quel collegamento Godmo.

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

Per la vectorcall a 32 bit, gcc farebbe qualcosa del genere

 ## 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 utilizza 13 istruzioni (escluso il ret), rispetto a gcc’s 4. Con inlining, come ho detto, potenzialmente può essere compilato solo a uno, mentre MSVC potrebbe comunque utilizzare probabilmente 9. (Non sarà necessario prenotare lo stack o caricare premainder : sto presupponendo che debba ancora memorizzare circa 2 dei 3 input, quindi li ricarica all’interno di asm, esegue idiv , memorizza due output e li ricarica fuori idiv , quindi 4 input / carichi per l’input e un altro 4 per l’output.)

Con il compilatore gcc, non è una grande differenza. asm o __asm o __asm__ sono gli stessi, usano solo per evitare lo scopo dello spazio dei nomi di conflitto (c’è una funzione definita dall’utente che chiama asm, ecc.)

asm vs __asm__ in GCC

asm non funziona con -std=c99 , hai due alternative:

  • usa __asm__
  • usa -std=gnu99

Ulteriori dettagli: errore: ‘asm’ non dichiarato (primo utilizzo in questa funzione)

__asm vs __asm__ in GCC

Non sono riuscito a trovare dove __asm sia documentato (in particolare non è menzionato su https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Alternate-Keywords.html#Alternate-Keywords ), ma dalla sorgente GCC 8.1 sono esattamente gli stessi:

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

quindi vorrei solo usare __asm__ che è documentato.