Méthode la plus rapide pour calculer l'ordre de grandeur dans un assemblage x86

12

La tâche est simple: écrire un assemblage qui calcule l'ordre de grandeur d'un entier en utilisant le moins de cycles d'horloge possible.

  • L'ordre de grandeur est défini comme log10non log2.
  • La plage d'entrées valides est 0de , inclus. Le comportement pour l'entrée en dehors de cette plage n'est pas défini.1012
  • Les valeurs doivent être arrondies à l'entier le plus proche, à l'exception de l'entrée donnée, 0la sortie doit l'être 0. (Vous pouvez considérer que c'est la meilleure représentation de l'infini négatif possible dans les entiers non signés).
  • Doit être un assemblage x86.
  • L'entier doit être une valeur d' exécution , pas un entier statique / en ligne. Nous ne savons donc pas ce que c'est au moment de la compilation.
  • Supposons que vous avez déjà un entier chargé dans un registre. (Mais inclure la définition de la valeur dans le registre dans la réponse pour plus de clarté).
  • Impossible d'appeler des bibliothèques ou des fonctions externes.
  • Libre d'utiliser toutes les instructions disponibles dans la documentation Intel .
  • Non C.
  • N'importe laquelle des ~ 7 architectures Intel Core est acceptable (répertoriée à la page 10 ). Idéalement Nehalem (Intel Core i7).

La réponse gagnante est celle qui utilise le moins de cycles d'horloge possible. Autrement dit, il peut avoir le plus d'opérations par seconde. Les résumés approximatifs du cycle d'horloge sont ici: http://www.agner.org/optimize/instruction_tables.pdf . Le calcul des cycles d'horloge peut se produire après la publication de la réponse.

Lance Pollard
la source
Les instructions «FYL2X» et autres FPU sont-elles autorisées?
Digital Trauma
1
Le résultat doit-il être un entier? Si oui, comment devrait-il être arrondi?
Digital Trauma
3
Donc, l'entrée et la sortie sont toutes deux des entiers, oui? Mais l'entrée peut être aussi grande que 10 ^ 12, donc c'est trop grand pour un 32 bits int. Supposons-nous donc une entrée entière de 64 bits?
Paul R
3
Le critère gagnant est-il basé sur le nombre maximum ou moyen de cycles? Et si c'est moyen, quelle est la distribution des intrants?
Runer112
2
Quel processeur est ciblé? Le document lié répertorie plus de 20 processus différents (AMD, Intel, autres) et les latences varient considérablement.
Digital Trauma

Réponses:

8

7 cycles, temps constant

Voici une solution basée sur ma réponse à cette question SO . Il utilise BSR pour compter le nombre de bits nécessaires pour contenir le nombre. Il recherche le nombre de chiffres décimaux nécessaires pour représenter le plus grand nombre pouvant contenir de nombreux bits. Ensuite, il soustrait 1 si le nombre réel est inférieur à la puissance la plus proche de 10 avec autant de chiffres.

    .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

Compile sur GCC 4.6.3 pour ubuntu et renvoie la valeur dans le code de sortie.

Je ne suis pas sûr d'interpréter cette table de cycles pour n'importe quel processeur moderne, mais en utilisant la méthode de @ DigitalTrauma, sur le processeur Nehalim, j'obtiens 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
AShelly
la source
Oh - j'ai vu DigitalTrauma après avoir commencé à écrire le mien. Idées similaires. En utilisant sa méthodologie de comptage, je pense obtenir 3,1,1,1 = 6 pour BSR, MOV, CMP, SBB
AShelly
Oui, je pense que le vôtre bat le mien. Va juste pour montrer a) Je ne suis pas un programmeur d'assemblage et b) l'assemblage est mieux laissé seul par nous, simples mortels ;-)
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.
cat
1
à droite, et la ligne suivante dit: "Supposons que vous avez déjà un entier chargé dans un registre. (Mais incluez la définition de la valeur dans le registre dans la réponse pour plus de clarté)." C'est ce que j'ai fait.
AShelly
remplacer le movzx eax par mov al. Les 24 premiers bits de eax seront déjà à zéro, donc le zx est redondant (et c'est cher).
peter ferrie
6

Meilleur cas 8 cycles, pire cas 12 cycles

Comme ce n'est pas clair dans la question, je fonde cela sur les latences d'Ivy Bridge.

L'approche ici consiste à utiliser l' bsrinstruction (bit scan reverse) comme log2 () du pauvre. Le résultat est utilisé comme index dans une table de sauts qui contient des entrées pour les bits 0 à 42. Je suppose que, étant donné que l'opération sur les données 64 bits est implicitement requise, alors l'utilisation de l' bsrinstruction est OK.

Dans le meilleur des cas, l'entrée jumptable peut mapper le bsrrésultat directement à l'amplitude. Par exemple, pour les entrées dans la plage 32-63, le bsrrésultat sera 5, qui est mappé à une amplitude de 1. Dans ce cas, le chemin d'instruction est:

Instruction    Latency

bsrq                 3
jmp                  2
movl                 1
jmp                  2

total                8

Dans le pire des cas, les entrées bsrseront mappées à deux amplitudes possibles, de sorte que l'entrée de saut peut en faire une supplémentaire cmppour vérifier si l'entrée est> 10 n . Par exemple, pour les entrées dans la plage 64-127, le bsrrésultat sera 6. L'entrée de pontage correspondante vérifie ensuite si l'entrée> 100 et définit la grandeur de sortie en conséquence.

De plus, pour le pire des cas, nous avons une instruction mov supplémentaire pour charger une valeur immédiate de 64 bits à utiliser dans le cmp, donc le pire des instructions est:

Instruction    Latency

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

total               12

Voici le code:

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

Cela a été principalement généré à partir de la sortie de l'assembleur gcc pour le code C de preuve de concept que j'ai écrit . Notez que le code C utilise un goto calculable pour implémenter la table de saut. Il utilise également la fonction __builtin_clzll()intégrée gcc, qui compile l' bsrinstruction (plus un xor).


J'ai envisagé plusieurs solutions avant d'arriver à celle-ci:

  • FYL2Xpour calculer le logarithme naturel, puis FMULpar la constante nécessaire. Ce serait probablement gagner si c'était un concours [tag: instruction: golf]. Mais FYL2Xa une latence de 90-106 pour Ivy bridge.

  • Recherche binaire codée en dur. Cela peut en fait être compétitif - je laisse le soin à quelqu'un d'autre de l'implémenter :).

  • Tableau de recherche complet des résultats. Je suis sûr que cela est théoriquement le plus rapide, mais nécessiterait une table de recherche de 1 To - pas encore pratique - peut-être dans quelques années si la loi de Moore continue de tenir.

Traumatisme numérique
la source
Si nécessaire, je peux calculer une latence moyenne pour toutes les entrées autorisées.
Digital Trauma
jmpet jccn'ont pas de latence, seulement des coûts de débit. La prédiction de branche + l'exécution spéculative signifie que les dépendances de contrôle ne font pas partie des chaînes de dépendance des données.
Peter Cordes