Il modo più veloce per calcolare l'ordine di grandezza nell'assemblaggio x86


12

Il compito è semplice: scrivere un assieme che calcola l'ordine di grandezza di un numero intero usando il minor numero di cicli di clock possibile.

  • L'ordine di grandezza è definito come log10, non log2.
  • La gamma di input valido è 0a , compreso. Il comportamento per l'input al di fuori di tale intervallo non è definito.1012
  • I valori devono essere arrotondati per difetto all'intero più vicino, ad eccezione del fatto che dato input 0dovrebbe essere output 0. (Puoi considerare questa come la migliore rappresentazione dell'infinito negativo che è possibile negli interi senza segno).
  • Deve essere un assembly x86.
  • L'intero deve essere un valore di runtime , non un intero statico / in linea. Quindi non sappiamo cosa sia al momento della compilazione.
  • Supponiamo di avere già un numero intero caricato in un registro. (Ma includi l'impostazione del valore nel registro nella risposta per chiarezza).
  • Impossibile chiamare librerie o funzioni esterne.
  • Liberi di utilizzare una qualsiasi delle istruzioni disponibili nei documenti Intel .
  • No C.
  • Le architetture Intel Core ~ ​​7 sono accettabili (elencate a pagina 10 ). Idealmente Nehalem (Intel Core i7).

La risposta vincente è quella che utilizza il minor numero di cicli di clock possibile. Cioè, può avere il maggior numero di operazioni al secondo. I riepiloghi approssimativi del ciclo dell'orologio sono qui: http://www.agner.org/optimize/instruction_tables.pdf . Il calcolo dei cicli di clock può avvenire dopo l'invio della risposta.


Sono consentiti 'FYL2X' e altre istruzioni FPU?
Digital Trauma

1
Il risultato dovrebbe essere un numero intero? In tal caso, come dovrebbe essere arrotondato?
Trauma digitale

3
Quindi input e output sono entrambi numeri interi, sì? Ma l'ingresso può essere grande come 10 ^ 12, quindi è troppo grande per un int a 32 bit. Quindi supponiamo un input intero a 64 bit?
Paul R

3
Il criterio vincente si basa sul numero massimo o medio di cicli? E se è nella media, qual è la distribuzione degli input?
Runer112

2
Quale processore è mirato? Il documento collegato elenca più di 20 processi diversi (AMD, Intel, altri) e vi è una notevole variazione nelle latenze.
Trauma digitale

Risposte:


8

7 cicli, tempo costante

Ecco una soluzione basata sulla mia risposta a questa domanda SO . Usa BSR per contare quanti bit sono necessari per contenere il numero. Cerca quante cifre decimali sono necessarie per rappresentare il numero più grande che possono contenere molti bit. Quindi sottrae 1 se il numero effettivo è inferiore alla potenza più vicina di 10 con quel numero di cifre.

    .intel_syntax noprefix
    .globl  main
main:
    mov rdi, 1000000000000              #;your value here
    bsr rax, rdi
    movzx   eax, BYTE PTR maxdigits[1+rax]
    cmp rdi, QWORD PTR powers[0+eax*8]
    sbb al, 0
    ret
maxdigits:
    .byte   0
    .byte   0
    .byte   0
    .byte   0
    .byte   1
    .byte   1
    .byte   1
    .byte   2
    .byte   2
    .byte   2
    .byte   3
    .byte   3
    .byte   3
    .byte   3
    .byte   4
    .byte   4
    .byte   4
    .byte   5
    .byte   5
    .byte   5
    .byte   6
    .byte   6
    .byte   6
    .byte   6
    .byte   7
    .byte   7
    .byte   7
    .byte   8
    .byte   8
    .byte   8
    .byte   9
    .byte   9
    .byte   9
    .byte   9
    .byte   10
    .byte   10
    .byte   10
    .byte   11
    .byte   11
    .byte   11
    .byte   12
powers:
    .quad   0
    .quad   10
    .quad   100
    .quad   1000
    .quad   10000
    .quad   100000
    .quad   1000000
    .quad   10000000
    .quad   100000000
    .quad   1000000000
    .quad   10000000000
    .quad   100000000000
    .quad   1000000000000

Compila su GCC 4.6.3 per Ubuntu e restituisce il valore nel codice di uscita.

Non sono sicuro di interpretare la tabella dei cicli per qualsiasi processore moderno, ma usando il metodo di @ DigitalTrauma, sul processore Nehalim, ottengo 7 ?

ins        uOps Latency
---        -    - 
BSR r,r  : 1    3
MOVZX r,m: 1    -
CMP m,r/i: 1    1 
SBB r,r/i: 2    2

Oh, ho visto DigitalTrauma dopo che ho iniziato a scrivere la mia. Idee simili. Usando la sua metodologia di conteggio penso che ottenga 3,1,1,1 = 6 per BSR, MOV, CMP, SBB
AShelly

Sì, penso che il tuo batte il mio. Va solo a mostrare a) Non sono un programmatore di assemblaggi eb) Il montaggio è meglio lasciarci soli da noi semplici mortali ;-)
Digital Trauma

The integer must be a runtime value, not a static/inline integer. So we don't know what it is at compile time.
gatto,

1
a destra, e la riga successiva dice: "Supponiamo che tu abbia già un numero intero caricato in un registro (ma includi l'impostazione del valore nel registro nella risposta per chiarezza)." Questo è quello che ho fatto.
AShelly,

sostituire il movzx eax con mov al. I primi 24 bit di eax saranno già zero, quindi zx è ridondante (ed è costoso).
peter ferrie,

6

Caso migliore 8 cicli, caso peggiore 12 cicli

Dal momento che non è chiaro nella domanda, lo sto basando sulle latenze di Ivy Bridge.

L'approccio qui è usare l' bsristruzione (bit scan reverse) come log2 () di un uomo povero. Il risultato viene utilizzato come indice in una tabella di salto che contiene voci per i bit da 0 a 42. Presumo che dato che l'operazione sui dati a 64 bit è implicitamente richiesta, l'uso bsrdell'istruzione è OK.

Nel migliore dei casi, la voce jumptable può mappare il bsrrisultato direttamente sulla grandezza. ad es. per gli ingressi nell'intervallo 32-63, il bsrrisultato sarà 5, che è mappato su una grandezza di 1. In questo caso, il percorso dell'istruzione è:

Instruction    Latency

bsrq                 3
jmp                  2
movl                 1
jmp                  2

total                8

Nei casi peggiori, il bsrrisultato verrà mappato su due possibili magnitudini, quindi la voce jumptable fa un'ulteriore cmpper verificare se l'ingresso è> 10 n . Ad esempio, per gli ingressi nell'intervallo 64-127, il bsrrisultato sarà 6. La voce jumptable corrispondente controlla quindi se l'ingresso> 100 e imposta la grandezza di uscita di conseguenza.

Inoltre per il percorso del caso peggiore, abbiamo un'istruzione mov aggiuntiva per caricare un valore immediato a 64 bit da utilizzare in the cmp, quindi il percorso dell'istruzione nel caso peggiore è:

Instruction    Latency

bsrq                 3
jmp                  2
movabsq              1
cmpq                 1
ja                   2
movl                 1
jmp                  2

total               12

Ecco il codice:

    /* Input is loaded in %rdi */
    bsrq    %rdi, %rax
    jmp *jumptable(,%rax,8)
.m0:
    movl    $0, %ecx
    jmp .end
.m0_1:
    cmpq    $9, %rdi
    ja  .m1
    movl    $0, %ecx
    jmp .end
.m1:
    movl    $1, %ecx
    jmp .end
.m1_2:
    cmpq    $99, %rdi
    ja  .m2
    movl    $1, %ecx
    jmp .end
.m2:
    movl    $2, %ecx
    jmp .end
.m2_3:
    cmpq    $999, %rdi
    ja  .m3
    movl    $2, %ecx
    jmp .end
.m3:
    movl    $3, %ecx
    jmp .end
.m3_4:
    cmpq    $9999, %rdi
    ja  .m4
    movl    $3, %ecx
    jmp .end
.m4:
    movl    $4, %ecx
    jmp .end
.m4_5:
    cmpq    $99999, %rdi
    ja  .m5
    movl    $4, %ecx
    jmp .end
.m5:
    movl    $5, %ecx
    jmp .end
.m5_6:
    cmpq    $999999, %rdi
    ja  .m6
    movl    $5, %ecx
    jmp .end
.m6:
    movl    $6, %ecx
    jmp .end
.m6_7:
    cmpq    $9999999, %rdi
    ja  .m7
    movl    $6, %ecx
    jmp .end
.m7:
    movl    $7, %ecx
    jmp .end
.m7_8:
    cmpq    $99999999, %rdi
    ja  .m8
    movl    $7, %ecx
    jmp .end
.m8:
    movl    $8, %ecx
    jmp .end
.m8_9:
    cmpq    $999999999, %rdi
    ja  .m9
    movl    $8, %ecx
    jmp .end
.m9:
    movl    $9, %ecx
    jmp .end
.m9_10:
    movabsq $9999999999, %rax
    cmpq    %rax, %rdi
    ja  .m10
    movl    $9, %ecx
    jmp .end
.m10:
    movl    $10, %ecx
    jmp .end
.m10_11:
    movabsq $99999999999, %rax
    cmpq    %rax, %rdi
    ja  .m11
    movl    $10, %ecx
    jmp .end
.m11:
    movl    $11, %ecx
    jmp .end
.m11_12:
    movabsq $999999999999, %rax
    cmpq    %rax, %rdi
    ja  .m12
    movl    $11, %ecx
    jmp .end
.m12:
    movl    $12, %ecx
    jmp .end

jumptable:
    .quad   .m0
    .quad   .m0
    .quad   .m0
    .quad   .m0_1
    .quad   .m1
    .quad   .m1
    .quad   .m1_2
    .quad   .m2
    .quad   .m2
    .quad   .m2_3
    .quad   .m3
    .quad   .m3
    .quad   .m3
    .quad   .m3_4
    .quad   .m4
    .quad   .m4
    .quad   .m4_5
    .quad   .m5
    .quad   .m5
    .quad   .m5_6
    .quad   .m6
    .quad   .m6
    .quad   .m6
    .quad   .m6_7
    .quad   .m7
    .quad   .m7
    .quad   .m7_8
    .quad   .m8
    .quad   .m8
    .quad   .m8_9
    .quad   .m9
    .quad   .m9
    .quad   .m9
    .quad   .m9_10
    .quad   .m10
    .quad   .m10
    .quad   .m10_11
    .quad   .m11
    .quad   .m11
    .quad   .m11_12
    .quad   .m12
    .quad   .m12
    .quad   .m12

.end:
/* output is given in %ecx */

Ciò è stato in gran parte generato dall'output dell'assembler gcc per il codice C di prova del concetto che ho scritto . Si noti che il codice C utilizza un goto calcolabile per implementare la tabella di salto. Utilizza anche il __builtin_clzll()comando incorporato gcc, che viene compilato per l' bsristruzione (più un xor).


Ho considerato diverse soluzioni prima di arrivare a questo:

  • FYL2Xper calcolare il registro naturale, quindi FMULper la costante necessaria. Questo presumibilmente vincerebbe questo se fosse un concorso [tag: istruzioni: golf]. Ma FYL2Xha una latenza di 90-106 per il ponte Ivy.

  • Ricerca binaria codificata. Questo potrebbe effettivamente essere competitivo - lo lascerò a qualcun altro da implementare :).

  • Tabella di ricerca completa dei risultati. Questo sono sicuro che sia teoricamente più veloce, ma richiederebbe una tabella di ricerca da 1 TB - non ancora pratica - forse tra qualche anno se la Legge di Moore continuerà a essere valida.


Se necessario, posso calcolare una latenza media per tutti gli input consentiti.
Digital Trauma

jmpe jccnon hanno latenza, ma solo costi di elaborazione. Predizione del ramo + esecuzione speculativa significano che le dipendenze di controllo non fanno parte delle catene di dipendenza dei dati.
Peter Cordes,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.