Impostare tutti i bit nel registro della CPU su 1 in modo efficiente

Per cancellare tutti i bit si vede spesso un esclusivo o come in XOR eax, eax . C’è anche un trucco per il contrario?

Tutto quello che posso pensare è di invertire gli zeri con un’istruzione extra.

Per la maggior parte delle architetture con istruzioni a larghezza fissa, la risposta sarà probabilmente un noioso mov di istruzione di un segno esteso o invertito immediato o di una coppia alta / alta. ad es. su ARM, mvn r0, #0 (sposta-non). Vedi output gcc asm per x86, ARM, ARM64 e MIPS, nel explorer del compilatore Godbolt . IDK qualsiasi cosa su zseries asm o codice macchina.

In ARM, eor r0,r0,r0 è significativamente peggiore di un mov-immediato. Dipende dal vecchio valore, senza la gestione di casi speciali. Le regole di ordinamento della dipendenza dalla memoria impediscono a un uarch ARM di rivestirlo in modo speciale anche se lo volessero. Lo stesso vale per la maggior parte degli altri ISA RISC con memoria debolmente ordinata ma che non richiedono barriere per memory_order_consume (nella terminologia C ++ 11).


x86 xor-zeroing è speciale a causa del suo set di istruzioni a lunghezza variabile. Storicamente, 8086 xor ax,ax era veloce direttamente perché era piccolo. Dato che l’idioma è diventato ampiamente utilizzato (e l’azzeramento è molto più comune di tutti), i progettisti di CPU gli hanno dato un supporto speciale, e ora xor eax,eax è più veloce di mov eax,0 su Intel Sandybridge-family e alcune altre CPU, anche senza considerare effetti di dimensione del codice diretti e indiretti. Vedere Qual è il modo migliore per impostare un registro su zero nell’assembly x86: xor, mov o e? per tutti i vantaggi micro-architettonici di cui sono stato in grado di recuperare.

Se x86 avesse un set di istruzioni a larghezza fissa, mi chiedo se il mov reg, 0 avrebbe ottenuto un trattamento speciale come quello di xor-zeroing? Forse, perché la rottura delle dipendenze prima di scrivere il low8 o il low16 è importante.


Le opzioni standard per le migliori prestazioni:

  • mov eax, -1 : 5 byte, usando la codifica mov r32, imm32 . (Non ci sono segni che estendono mov r32, imm8 , sfortunatamente). Prestazioni eccellenti su tutte le CPU. 6 byte per r8-r15 (prefisso REX).
  • mov rax, -1 : 7 byte, usando la codifica mov r/m64, sign-extended-imm32 . (Non la versione REX.W = 1 della versione eax . Sarebbe mov r64, imm64 byte mov r64, imm64 ). Prestazioni eccellenti su tutte le CPU.

Le opzioni bizzarre che salvano alcune dimensioni del codice di solito a scapito delle prestazioni :

  • xor eax,eax / dec rax (o not rax ): 5 byte (4 per eax 32 bit). Lato negativo: due UOP per il front-end. Ancora un solo uop di dominio non utilizzato per le unità di pianificazione / esecuzione su Intel recente in cui viene gestito l’ xor-azzeramento nel front-end. mov sempre ha bisogno di un’unità di esecuzione. (Ma il throughput intero di ALU è raramente un collo di bottiglia per le istruzioni che possono utilizzare qualsiasi porta, la pressione extra front-end è il problema)
  • xor ecx,ecx / lea eax, [rcx-1] 5 byte totali per 2 costanti (6 byte per rax ): lascia un registro a zero separato . Se vuoi già un registro azzerato, non c’è quasi nessun svantaggio in questo. lea può funzionare su un numero inferiore di porte rispetto a mov r,i sulla maggior parte delle CPU, ma poiché questo è l’inizio di una nuova catena di dipendenze, la CPU può eseguirle in qualsiasi ciclo di porta di esecuzione di riserva dopo il rilascio.

    Lo stesso trucco funziona per due costanti vicine, se si esegue il primo con mov reg, imm32 e il secondo con lea r32, [base + disp8] . disp8 ha un intervallo da -128 a +127, altrimenti è necessario un disp32 .

  • or eax, -1 : 3 byte (4 per rax ), usando la codifica or r/m32, sign-extended-imm8 . Lato negativo: falsa dipendenza dal vecchio valore del registro.

  • push -1 / pop rax : 3 byte. Lento ma piccolo. Consigliato solo per exploit / code-golf. Funziona per qualsiasi sign-extended-imm8 , a differenza della maggior parte degli altri.

    Svantaggi:

    • usa il negozio e carica le unità di esecuzione, non l’ALU. (Probabilmente un vantaggio del throughput in casi rari sulla famiglia AMD Bulldozer in cui ci sono solo due pipe di esecuzione intera, ma il throughput di decodifica / rilascio / ritiro è più alto di quello. Ma non provarlo senza test.)
    • store / reload latency significa che rax non sarà pronto per ~ 5 cicli dopo che questo è stato eseguito su Skylake, ad esempio.
    • (Intel): mette lo stack-engine in modalità rsp-modified, quindi la prossima volta che leggerete rsp ci vorrà uno stack-sync uop. (ad esempio per add rsp, 28 o per mov eax, [rsp+8] ).
    • Il negozio potrebbe perdere la cache, innescando ulteriore traffico di memoria. (Possibile se non hai toccato la pila all’interno di un loop lungo).

I reg di vettore sono diversi

L’impostazione di registri vettoriali su tutti-uni con pcmpeqd xmm0,xmm0 è speciale nella maggior parte delle CPU come pcmpeqd xmm0,xmm0 delle dipendenze (non Silvermont / KNL), ma ha ancora bisogno di un’unità di esecuzione per scrivere effettivamente quelle. pcmpeqb/w/d/q tutto funziona, ma q è più lento su alcune CPU.

La versione AVX / AVX2 di questo è anche la scelta migliore. Il modo più veloce per impostare il valore __m256 su tutti i bit ONE


I confronti dell’AVX512 sono disponibili solo con un registro maschera (come k0 ) come destinazione, quindi i compilatori utilizzano attualmente vpternlogd zmm0,zmm0,zmm0, 0xff come idioma 512b all-ones. (0xff rende ogni elemento della tabella di verità a 3 input a 1 ). Questo non è un caso speciale come rottura delle dipendenze su KNL o SKL, ma ha un throughput a 2 ore su Skylake-AVX512. Questo batte usando un AVX allarmante che riduce le dipendenze e trasmette o mescola.

Se è necessario ri-generare tutti quelli all’interno di un ciclo, ovviamente il modo più efficace è usare un vmov* per copiare un registro di tutti i vmov* . Questo non usa nemmeno un’unità di esecuzione su CPU moderne (ma richiede ancora larghezza di banda di emissione front-end). Ma se sei fuori dai registri vettoriali, caricare una costante o [v]pcmpeq[b/w/d] sono buone scelte.

Per AVX512, vale la pena provare VPMOVM2D zmm0, k0 o forse VPBROADCASTD zmm0, eax . Ciascuno ha solo un throughput di 1c , ma dovrebbero interrompere le dipendenze dal vecchio valore di zmm0 (diversamente da vpternlogd ). Richiedono una maschera o un registro intero che è stato inizializzato all’esterno del ciclo con kxnorw k1,k0,k0 o mov eax, -1 .


Per i registri di maschere AVX512 , kxnorw k1,k0,k0 funziona, ma non è una dipendenza che si infrange sulle attuali CPU. Il manuale di ottimizzazione di Intel suggerisce di usarlo per generare un tutto prima di un’istruzione di raccolta, ma raccomanda di evitare di utilizzare lo stesso registro di input dell’output. Ciò evita che un gather altrimenti indipendente dipenda da uno precedente in un ciclo. Poiché k0 è spesso inutilizzato, di solito è una buona scelta da cui leggere.

Penso che vpcmpeqd k1, zmm0,zmm0 funzionerebbe, ma probabilmente non è un caso speciale come un idioma k0 = 1 senza dipendenza da zmm0. (Per impostare tutti i 64 bit anziché solo i 16 bassi, utilizzare AVX512BW vpcmpeqb )

Su Skylake-AVX512, le istruzioni che operano sui registri di maschere funzionano solo su una singola porta , anche semplici come kandw . (Si noti inoltre che Skylake-AVX512 non eseguirà gli user uops su port1 quando ci sono operazioni 512b nella pipe, quindi il throughput delle unità di esecuzione può essere un vero collo di bottiglia.)

Non ci sono kmov k0, imm , solo mosse dal numero intero o dalla memoria. Probabilmente non ci sono istruzioni k dove lo stesso, lo stesso viene rilevato come speciale, quindi l’hardware nella fase di rilascio / rinominazione non lo cerca per k registri.