Skip to content

Come convertire un numero intero binario in una stringa esadecimale?

Questo gruppo specializzato dopo giorni di lavoro e raccolta dati, abbiamo trovato la soluzione, la nostra speranza è che possa esserti utile nel tuo piano.

Soluzione:

correlato: Versione a 16 bit che converte 1 byte in 2 cifre esadecimali che si possono stampare o memorizzare in un buffer. E Converting bin to hex in assembly ha un'altra versione a 16 bit con abbondanti spiegazioni testuali nella metà della risposta che copre la parte int -> hex-string del problema.

Se si ottimizza per la dimensione del codice invece che per la velocità, c'è una soluzione che utilizza DAS per risparmiare alcuni byte.


16 è una potenza di 2. A differenza del decimale o di altre basi che non sono una potenza di 2, non abbiamo bisogno della divisione e possiamo estrarre per prima la cifra più significativa (cioè in ordine di stampa). Altrimenti si può ottenere solo la cifra meno significativa (e il suo valore dipende da tutti i bit del numero) e si deve andare a ritroso: si veda Come stampare un intero in programmazione a livello Assembly senza printf dalla libreria c? per basi che non sono potenze di 2.

Ogni gruppo di 4 bit corrisponde a una cifra esadecimale. Si possono usare shift o rotazioni e maschere AND per estrarre ogni gruppo di 4 bit dell'input come un intero a 4 bit.

Purtroppo le cifre esadecimali 0..9 a..f non sono contigue nell'insieme di caratteri ASCII. (http://www.asciitable.com/). Abbiamo bisogno di un comportamento condizionale (un branch o un cmov) oppure possiamo usare una tabella di lookup.

Una tabella di ricerca è in genere la più efficiente per quanto riguarda il numero di istruzioni e le prestazioni, poiché si tratta di un'operazione ripetuta; le CPU moderne hanno cache L1d molto veloci che rendono molto economici i carichi ripetuti di byte vicini. L'esecuzione pipelined / out-of-order nasconde la latenza di circa 5 cicli di un caricamento della cache L1d.

;; NASM syntax, i386 System V calling convention
global itohex      ; inputs: char* output,  unsigned number
itohex:
    push   edi           ; save a call-preserved register for scratch space
    mov    edi, [esp+8]  ; out pointer
    mov    eax, [esp+12] ; number

    mov    ecx, 8        ; 8 hex digits, fixed width zero-padded
.digit_loop:             ; do {
    rol    eax, 4          ; rotate the high 4 bits to the bottom

    mov    edx, eax
    and    edx, 0x0f       ; and isolate 4-bit integer in EDX

    movzx  edx, byte [hex_lut + edx]
    mov    [edi], dl       ; copy a character from the lookup table
    inc    edi             ; loop forward in the output buffer

    dec    ecx
    jnz    .digit_loop   ; }while(--ecx)

    pop    edi
    ret

section .rodata
    hex_lut:  db  "0123456789abcdef"

Per adattarsi a x86-64, la convenzione di chiamata passerà gli argomenti nei registri invece che nello stack, ad esempio RDI e ESI per x86-64 System V (non Windows). È sufficiente rimuovere la parte che carica dallo stack e modificare il ciclo per usare ESI invece di EAX. (E rendere le modalità di indirizzamento a 64 bit. Potrebbe essere necessario LEA il parametro hex_lut in un registro esterno al ciclo; vedere questo e questo).

Questa versione si converte in esadecimale con zeri iniziali. Se si desidera eliminarli, bit_scan(input)/4 come lzcnt o __builtin_clz sull'ingresso, o SIMD compare -> pmovmksb -> tzcnt sulla stringa ASCII in uscita vi dirà quante cifre 0 avete (e quindi potrete stampare o copiare partendo dal primo non-zero). Oppure si può convertire partendo dal nibble basso e procedendo a ritroso, fermandosi quando uno shift a destra rende il valore zero, come mostrato nella seconda versione che utilizza cmov invece di una tabella di ricerca.

Fino a BMI2 (shrx / rorx), x86 non dispone di un'istruzione copy-and-shift, quindi la rotazione in-place e poi copy/AND è difficile da battere.1. I moderni x86 (Intel e AMD) hanno una latenza di 1 ciclo per le rotazioni (https://agner.org/optimize/ e https://uops.info/), quindi questa catena di dipendenze trasportata dal loop non diventa un collo di bottiglia (ci sono troppe istruzioni nel loop perché possa funzionare a 1 ciclo per iterazione anche su Ryzen a 5 larghezze).

Ho usato mov ecx,8 e dec ecx/jnz per la leggibilità umana; lea ecx, [edi+8] in alto e cmp edi, ecx / jb .digit_loop come ramo del ciclo è più piccolo in termini di dimensioni complessive del codice macchina e più efficiente su più CPU. dec/jcc La fusione di macro in un singolo uop avviene solo sulla famiglia Intel Sandybridge; AMD fonde solo jcc con cmp o test. Questa ottimizzazione ridurrebbe a 7 uop per il front-end su Ryzen, come per Intel, che è comunque più di quanto possa fare in un ciclo.

Nota 1: Potremmo usare SWAR (SIMD all'interno di un registro) per fare l'AND prima dello shift: x & 0x0f0f0f0f i nibble bassi e shr(x,4) & 0x0f0f0f0f nibble alti e poi si srotola in modo efficace elaborando alternativamente un byte da ciascun registro. (Senza un modo efficiente per fare un equivalente di punpcklbw o di mappare gli interi ai codici ASCII non contigui, dobbiamo ancora elaborare ogni byte separatamente. Ma si potrebbe srotolare l'estrazione dei byte e leggere AH e poi AL (con movzx) per risparmiare istruzioni di shift. La lettura di registri high-8 può aggiungere latenza, ma credo che non costi uops in più sulle CPU attuali. La scrittura di registri high-8 di solito non va bene sulle CPU Intel: costa un uop di fusione in più per leggere l'intero registro, con un ritardo di front-end per inserirlo. Quindi, ottenere store più ampi mescolando i registri probabilmente non è una buona cosa. Nel codice del kernel non si possono usare i registri XMM, ma si può usare BMI2 se disponibile, pdep potrebbe espandere i nibble in byte, ma questo è probabilmente peggiore del semplice mascheramento a due vie).

Programma di prova:

// hex.c   converts argv[1] to integer and passes it to itohex
#include 
#include 

void itohex(char buf[8], unsigned num);

int main(int argc, char**argv) {
    unsigned num = strtoul(argv[1], NULL, 0);  // allow any base
    char buf[9] = {0};
    itohex(buf, num);   // writes the first 8 bytes of the buffer, leaving a 0-terminated C string
    puts(buf);
}

compilare con:

nasm -felf32 -g -Fdwarf itohex.asm
gcc -g -fno-pie -no-pie -O3 -m32 hex.c itohex.o

esegue il test:

$ ./a.out 12315
0000301b
$ ./a.out 12315123
00bbe9f3
$ ./a.out 999999999
3b9ac9ff
$ ./a.out 9999999999   # apparently glibc strtoul saturates on overflow
ffffffff
$ ./a.out 0x12345678   # strtoul with base=0 can parse hex input, too
12345678

Implementazioni alternative:

Condizionale invece di tabella di ricerca: richiede molte più istruzioni e probabilmente sarà più lento. Ma non ha bisogno di dati statici.

Si potrebbe fare con la ramificazione invece che con cmovma nella maggior parte dei casi sarebbe ancora più lento. (Non prevede bene, ipotizzando un mix casuale di cifre 0..9 e a..f.) https://codegolf.stackexchange.com/questions/193793/little-endian-number-to-string-conversion/193842#193842 mostra una versione ottimizzata per le dimensioni del codice. (A parte un bswap all'inizio, è un normale uint32_t -> hex con zero padding).

Solo per divertimento, questa versione inizia alla fine del buffer e decrementa un puntatore. (Si potrebbe fare in modo che si fermi quando EDX diventa zero e usare EDI+1 come inizio del numero, se non si vogliono zeri iniziali.

Utilizzo di un cmp eax,9 / ja invece di cmov è lasciato come esercizio al lettore. Una versione a 16 bit potrebbe utilizzare registri diversi (come ad esempio BX come temporaneo) per consentire comunque l'uso di lea cx, [bx + 'a'-10] copia e aggiungi. Oppure add/cmp e jccse si vuole evitare cmov per la compatibilità con le vecchie CPU che non supportano le estensioni P6.

;; NASM syntax, i386 System V calling convention
itohex:   ; inputs: char* output,  unsigned number
itohex_conditional:
    push   edi             ; save a call-preserved register for scratch space
    push   ebx
    mov    edx, [esp+16]   ; number
    mov    ebx, [esp+12]   ; out pointer

    lea    edi, [ebx + 7]   ; First output digit will be written at buf+7, then we count backwards
.digit_loop:                ; do {
    mov    eax, edx
    and    eax, 0x0f            ; isolate the low 4 bits in EAX
    lea    ecx, [eax + 'a'-10]  ; possible a..f value
    add    eax, '0'             ; possible 0..9 value
    cmp    ecx, 'a'
    cmovae eax, ecx             ; use the a..f value if it's in range.
                                ; for better ILP, another scratch register would let us compare before 2x LEA,
                                ;  instead of having the compare depend on an LEA or ADD result.

    mov    [edi], al        ; *ptr-- = c;
    dec    edi

    shr    edx, 4

    cmp    edi, ebx         ; alternative:  jnz on flags from EDX to not write leading zeros.
    jae    .digit_loop      ; }while(ptr >= buf)

    pop    ebx
    pop    edi
    ret

Potremmo esporre ancora più ILP all'interno di ogni iterazione utilizzando 2x lea + cmp/cmov. cmp ed entrambi i LEA dipendono solo dal valore del nibble, con cmov che consuma tutti e 3 i risultati. Ma c'è un sacco di ILP tra le iterazioni con solo il metodo shr edx,4 e il decremento del puntatore come dipendenze trasportate dal ciclo. Avrei potuto risparmiare 1 byte di dimensione del codice organizzandomi in modo da poter usare cmp al, 'a' o qualcosa del genere. E/o add al,'0' se non mi interessassero le CPU che rinominano AL separatamente da EAX.

Testcase che controlla gli errori off-by-1 utilizzando un numero che ha entrambi i valori 9 e a nelle sue cifre esadecimali:

$ nasm -felf32 -g -Fdwarf itohex.asm && gcc -g -fno-pie -no-pie -O3 -m32 hex.c itohex.o && ./a.out 0x19a2d0fb
19a2d0fb

SIMD con SSE2, SSSE3, AVX2 o AVX512F, e ~2 istruzioni con AVX512VBMI

Con SSSE3 e successive, è meglio usare un byte shuffle come tabella di ricerca dei nibble.

La maggior parte di queste versioni SIMD può essere utilizzata con due interi a 32 bit impacchettati come input, con gli 8 byte bassi e alti del vettore dei risultati che contengono risultati separati che possono essere memorizzati separatamente con movq e movhps.
A seconda del controllo di rimescolamento, questo è esattamente come usarlo per un intero a 64 bit.

SSSE3 pshufb tabella di ricerca parallela. Non c'è bisogno di fare confusione con i loop, possiamo farlo con poche operazioni SIMD, su CPU che hanno pshufb. (SSSE3 non è una linea di base nemmeno per x86-64; era nuovo con Intel Core2 e AMD Bulldozer).

pshufb è uno shuffle di byte controllato da un vettore, non da un immediato (a differenza di tutti i precedenti shuffle SSE1/SSE2/SSE3). Con una destinazione fissa e un controllo variabile dello shuffle, possiamo usarlo come tabella di ricerca parallela per fare ricerche 16x in parallelo (da una tabella di 16 voci di byte in un vettore).

Quindi carichiamo l'intero intero in un registro vettoriale e scompattiamo i suoi nibbles in byte con un bit-shift e punpcklbw. Quindi utilizziamo un pshufb per mappare i nibble in cifre esadecimali.

Questo ci lascia con le cifre ASCII in un registro XMM con la cifra meno significativa come byte più basso del registro. Poiché x86 è little-endian, non c'è un modo libero per memorizzarle nell'ordine opposto, con l'MSB per primo.

Possiamo utilizzare un ulteriore pshufb per riordinare i byte ASCII in ordine di stampa, oppure usare bswap sull'ingresso in un registro intero (e invertire lo spacchettamento nibble -> byte). Se l'intero proviene dalla memoria, il passaggio da un registro intero per bswap fa un po' schifo (soprattutto per la famiglia AMD Bulldozer), ma se si ha l'intero in un registro GP in primo luogo è abbastanza buono.

;; NASM syntax, i386 System V calling convention

section .rodata
 align 16
    hex_lut:  db  "0123456789abcdef"
    low_nibble_mask: times 16 db 0x0f
    reverse_8B: db 7,6,5,4,3,2,1,0,   15,14,13,12,11,10,9,8
    ;reverse_16B: db 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0

section .text

global itohex_ssse3    ; tested, works
itohex_ssse3:
    mov    eax,  [esp+4]    ; out pointer
    movd   xmm1, [esp+8]    ; number

    movdqa xmm0, xmm1
    psrld  xmm1, 4          ; right shift: high nibble -> low  (with garbage shifted in)
    punpcklbw xmm0, xmm1    ; interleave low/high nibbles of each byte into a pair of bytes
    pand   xmm0, [low_nibble_mask]   ; zero the high 4 bits of each byte (for pshufb)
    ; unpacked to 8 bytes, each holding a 4-bit integer

    movdqa xmm1, [hex_lut]
    pshufb xmm1, xmm0       ; select bytes from the LUT based on the low nibble of each byte in xmm0

    pshufb xmm1, [reverse_8B]  ; printing order is MSB-first

    movq   [eax], xmm1      ; store 8 bytes of ASCII characters
    ret
;; The same function for 64-bit integers would be identical with a movq load and a movdqu store.
;; but you'd need reverse_16B instead of reverse_8B to reverse the whole reg instead of each 8B half

È possibile racchiudere la maschera AND e il controllo pshufb in un unico vettore di 16 byte, simile a itohex_AVX512F sotto.

AND_shuffle_mask: times 8 db 0x0f       ; low half: 8-byte AND mask
                   db 7,6,5,4,3,2,1,0   ; high half: shuffle constant that will grab the low 8 bytes in reverse order

Caricarlo in un registro vettoriale e usarlo come maschera AND, quindi usarlo come un pshufb per afferrare gli 8 byte bassi in ordine inverso, lasciandoli negli 8 alti. Il risultato finale (8 cifre esadecimali ASCII) si troverà nella metà superiore di un registro XMM, quindi utilizzare movhps [eax], xmm1. Sulle CPU Intel, questo è ancora solo 1 uop a dominio fuso, quindi è altrettanto economico di movq. Ma su Ryzen, costa uno shuffle oltre a uno store. Inoltre, questo trucco è inutile se si vogliono convertire due interi in parallelo o un intero a 64 bit.

SSE2, garantito disponibile in x86-64:

Senza SSSE3 pshufbdobbiamo affidarci a scalari bswap per mettere i byte nel giusto ordine di stampa e punpcklbw per interlacciare con il nibble alto di ogni coppia.

Invece di una tabella di ricerca, aggiungiamo semplicemente '0'e aggiungere un altro 'a' - ('0'+10) per le cifre superiori a 9 (per inserirle nella tabella 'a'..'f' ). SSE2 ha un confronto a byte impacchettato per il maggiore di, pcmpgtb. Insieme a un AND bitwise, è tutto ciò di cui abbiamo bisogno per aggiungere condizionatamente qualcosa.

itohex:             ; tested, works.
global itohex_sse2
itohex_sse2:
    mov    edx,  [esp+8]    ; number
    mov    ecx,  [esp+4]    ; out pointer
    ;; or enter here for fastcall arg passing.  Or rdi, esi for x86-64 System V.  SSE2 is baseline for x86-64
    bswap  edx
    movd   xmm0, edx

    movdqa xmm1, xmm0
    psrld  xmm1, 4          ; right shift: high nibble -> low  (with garbage shifted in)
    punpcklbw xmm1, xmm0    ; interleave high/low nibble of each byte into a pair of bytes
    pand   xmm1, [low_nibble_mask]   ; zero the high 4 bits of each byte
    ; unpacked to 8 bytes, each holding a 4-bit integer, in printing order

    movdqa  xmm0, xmm1
    pcmpgtb xmm1, [vec_9]
    pand    xmm1, [vec_af_add] ; digit>9 ?  'a'-('0'+10)  :  0

    paddb   xmm0, [vec_ASCII_zero]
    paddb   xmm0, xmm1      ; conditional add for digits that were outside the 0..9 range, bringing them to 'a'..'f'

    movq   [ecx], xmm0      ; store 8 bytes of ASCII characters
    ret
    ;; would work for 64-bit integers with 64-bit bswap, just using movq + movdqu instead of movd + movq

section .rodata
align 16
    vec_ASCII_zero: times 16 db '0'
    vec_9:          times 16 db 9
    vec_af_add:     times 16 db 'a'-('0'+10)
    ; 'a' - ('0'+10) = 39 = '0'-9, so we could generate this from the other two constants, if we were loading ahead of a loop
    ; 'A'-('0'+10) = 7 = 0xf >> 1.  So we could generate this on the fly from an AND.  But there's no byte-element right shift.

    low_nibble_mask: times 16 db 0x0f

Questa versione ha bisogno di più costanti vettoriali rispetto alla maggior parte delle altre. 4x 16 byte sono 64 byte, che si adattano a una riga di cache. Si consiglia di align 64 prima del primo vettore, invece di align 16in modo che provengano tutti dalla stessa riga di cache.

Questo potrebbe anche essere implementato solo con MMX, usando solo costanti a 8 byte, ma in questo caso sarebbe necessario un emms quindi probabilmente sarebbe una buona idea solo su CPU molto vecchie che non hanno SSE2, o che dividono le operazioni a 128 bit in metà a 64 bit (ad esempio Pentium-M o K8). Sulle CPU moderne con l'eliminazione dei mov per i registri vettoriali (come Bulldozer e IvyBrige), funziona solo sui registri XMM, non MMX. Ho sistemato l'utilizzo dei registri in modo che il secondo movdqa sia fuori dal percorso critico, ma non l'ho fatto per il primo.


AVX può salvare un movdqa, ma più interessante è con AVX2 possiamo potenzialmente produrre 32 byte di cifre esadecimali alla volta da input di grandi dimensioni. 2x interi a 64 bit o 4x interi a 32 bit; utilizzare un carico broadcast a 128->256 bit per replicare i dati di ingresso in ogni corsia. Da qui, in corsia vpshufb ymm con un vettore di controllo che legge dalla metà bassa o alta di ciascuna corsia a 128 bit, dovrebbe fornire i nibble per i 64 bit bassi dell'ingresso spacchettati nella corsia bassa e i nibble per i 64 bit alti dell'ingresso spacchettati nella corsia alta.

Oppure, se i numeri di ingresso provengono da fonti diverse, forse vinserti128 quello alto potrebbe valere la pena su alcune CPU, rispetto all'esecuzione di operazioni separate a 128 bit.


AVX512VBMI (Cannonlake/IceLake, non presente in Skylake-X) dispone di un byte shuffle a due registri vpermt2b che potrebbe combinare il puncklbw interleaving con l'inversione dei byte. O ancora meglio, abbiamo VPMULTISHIFTQB che può estrarre 8 campi di bit a 8 bit non allineati da ogni qword della sorgente.

Possiamo usarlo per estrarre direttamente i nibble che vogliamo nell'ordine desiderato, evitando un'istruzione separata di spostamento a destra. (Viene ancora fornito con i bit di spazzatura, ma vpermb ignora la spazzatura alta).

Per utilizzare questo metodo per gli interi a 64 bit, utilizzare una sorgente broadcast e un controllo multishift che scomponga i 32 bit alti della qword in ingresso nella parte inferiore del vettore e i 32 bit bassi nella parte superiore del vettore. (Assumendo un ingresso little-endian)

Per utilizzare questa funzione per più di 64 bit di input, usare vpmovzxdq per estendere a zero ogni dword in ingresso in una qword., impostando per vpmultishiftqb con lo stesso schema di controllo 28,24,...,4,0 in ogni qword. (ad esempio, producendo un vettore di output zmm da un vettore di input a 256 bit, o quattro dword -> una reg ymm per evitare i limiti di velocità di clock e altri effetti dell'esecuzione di un'istruzione AVX512 a 512 bit).

Attenzione al fatto che un'istruzione più ampia vpermb utilizza 5 o 6 bit di ogni byte di controllo, il che significa che dovrete trasmettere l'hexLUT a un registro ymm o zmm, o ripeterlo in memoria.

itohex_AVX512VBMI:                         ;  Tested with SDE
    vmovq          xmm1, [multishift_control]
    vpmultishiftqb xmm0, xmm1, qword [esp+8]{1to2}    ; number, plus 4 bytes of garbage.  Or a 64-bit number
    mov    ecx,  [esp+4]            ; out pointer

     ;; VPERMB ignores high bits of the selector byte, unlike pshufb which zeroes if the high bit is set
     ;; and it takes the bytes to be shuffled as the optionally-memory operand, not the control
    vpermb  xmm1, xmm0, [hex_lut]   ; use the low 4 bits of each byte as a selector

    vmovq   [ecx], xmm1     ; store 8 bytes of ASCII characters
    ret
    ;; For 64-bit integers: vmovdqa load [multishift_control], and use a vmovdqu store.

section .rodata
align 16
    hex_lut:  db  "0123456789abcdef"
    multishift_control: db 28, 24, 20, 16, 12, 8, 4, 0
    ; 2nd qword only needed for 64-bit integers
                        db 60, 56, 52, 48, 44, 40, 36, 32
# I don't have an AVX512 CPU, so I used Intel's Software Development Emulator
$ /opt/sde-external-8.4.0-2017-05-23-lin/sde -- ./a.out 0x1235fbac
1235fbac

vpermb xmm non è un attraversamento di corsia perché è coinvolta una sola corsia (a differenza di vpermb ymm o zmm). Ma sfortunatamente su CannonLake (secondo i risultati di instlatx64), ha ancora una latenza di 3 cicli quindi pshufb sarebbe meglio per la latenza. Ma pshufb si azzera condizionatamente in base al bit alto, quindi richiede il mascheramento del vettore di controllo. Questo lo rende peggiore per la velocità di trasmissione, supponendo che vpermb xmm sia solo 1 uop. In un ciclo in cui è possibile mantenere le costanti del vettore nei registri (invece che negli operandi di memoria), si risparmia solo 1 istruzione anziché 2.

(Aggiornamento: sì, https://uops.info/ conferma vpermb è 1 uop con 3c di latenza, 1c di throughput su Cannon Lake e Ice Lake. ICL ha un throughput di 0,5c per vpshufb xmm/ymm)


AVX2 variable-shift o AVX512F merge-masking per salvare un interleave

Con AVX512F, possiamo usare il merge-masking per spostare a destra una dword lasciando inalterata l'altra, dopo aver trasmesso il numero in un registro XMM.

Oppure si può usare un AVX2 a spostamento variabile vpsrlvd per fare esattamente la stessa cosa con un vettore di shift-count di [4, 0, 0, 0]. Intel Skylake e successivi hanno una funzione single-uop vpsrlvd; Haswell/Broadwell hanno più uops (2p0 + p5). Ryzen vpsrlvd xmm è di 1 uop, 3c di latenza, 1 per 2 clock di throughput. (Peggio degli spostamenti immediati).

Allora abbiamo bisogno solo di un byte shuffle a singolo registro, vpshufbper interlacciare i nibble e l'inversione dei byte. Ma poi serve una costante in un registro di maschera che richiede un paio di istruzioni per essere creata. Sarebbe un vantaggio maggiore in un ciclo che converte più numeri interi in esadecimale.

Per una versione stand-alone non in loop della funzione, ho usato due metà di una costante a 16 byte per cose diverse: set1_epi8(0x0f) nella metà superiore e 8 byte di pshufb nella metà inferiore. Questo non risparmia molto, perché gli operandi della memoria broadcast di EVEX consentono vpandd xmm0, xmm0, dword [AND_mask]{1to4}e richiedono solo 4 byte di spazio per una costante.

itohex_AVX512F:       ;; Saves a punpcklbw.  tested with SDE
    vpbroadcastd  xmm0, [esp+8]    ; number.  can't use a broadcast memory operand for vpsrld because we need merge-masking into the old value
    mov     edx, 1<<3             ; element #3
    kmovd   k1, edx
    vpsrld  xmm0{k1}, xmm0, 4      ; top half:  low dword: low nibbles unmodified (merge masking).  2nd dword: high nibbles >> 4
      ; alternatively, AVX2 vpsrlvd with a [4,0,0,0] count vector.  Still doesn't let the data come from a memory source operand.

    vmovdqa xmm2, [nibble_interleave_AND_mask]
    vpand   xmm0, xmm0, xmm2     ; zero the high 4 bits of each byte (for pshufb), in the top half
    vpshufb xmm0, xmm0, xmm2     ; interleave nibbles from the high two dwords into the low qword of the vector

    vmovdqa xmm1, [hex_lut]
    vpshufb xmm1, xmm1, xmm0       ; select bytes from the LUT based on the low nibble of each byte in xmm0

    mov      ecx,  [esp+4]    ; out pointer
    vmovq   [ecx], xmm1       ; store 8 bytes of ASCII characters
    ret

section .rodata
align 16
    hex_lut:  db  "0123456789abcdef"
    nibble_interleave_AND_mask: db 15,11, 14,10, 13,9, 12,8  ; shuffle constant that will interleave nibbles from the high half
                      times 8 db 0x0f              ; high half: 8-byte AND mask

Se hai trovato utile il nostro articolo, sarebbe molto utile se lo condividessi con più ragazzi in questo modo contribuisci a diffondere questo contenuto.



Utilizzate il nostro motore di ricerca

Ricerca
Generic filters

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.