52 votes

Diviser par 10 en utilisant des décalages de bits ?

Est-il possible de diviser un nombre entier non signé par 10 en utilisant des décalages de bits purs, des additions, des soustractions et des opérations d'écriture ? peut-être se multiplier ? En utilisant un processeur aux ressources très limitées et à la division lente.

0 votes

C'est possible (une soustraction répétée est une division), mais la question est de savoir si c'est plus rapide que la division lente.

0 votes

@esnyder. Désolé, je n'arrive pas à vous comprendre. Parlez-vous en base 17 ou en base 22 ?

0 votes

Base large deux. Le décalage vers la droite divise par 2^n ce qui résoudrait votre question si par "10" vous voulez dire 16 décimal ou 10h.

63voto

John Källén Points 3680

Note de l'éditeur : c'est no ce que font réellement les compilateurs, et donne une mauvaise réponse pour les grands nombres entiers positifs se terminant par 9, en commençant par div10(1073741829) = 107374183 pas 107374182. Il est exact pour les entrées plus petites, cependant, ce qui peut être suffisant pour certaines utilisations.

Les compilateurs (y compris MSVC) utilisent des inverses multiplicatives en virgule fixe pour les diviseurs constants, mais ils utilisent une constante magique différente et un décalage sur le résultat de la moitié supérieure pour obtenir un résultat exact pour toutes les entrées possibles, ce qui correspond à ce que la machine abstraite C exige. Voir Article de Granlund & Montgomery sur l'algorithme.

Ver Pourquoi GCC utilise-t-il la multiplication par un nombre étrange pour implémenter la division d'un nombre entier ? pour des exemples de l'asm x86 que font gcc, clang, MSVC, ICC et d'autres compilateurs modernes.


Il s'agit d'une approximation rapide qui est inexacte pour les entrées importantes.

C'est même plus rapide que la division exacte via multiplier + décalage à droite que les compilateurs utilisent.

Vous pouvez utiliser la moitié supérieure du résultat d'une multiplication pour les divisions par de petites constantes intégrales. Supposons une machine 32 bits (le code peut être adapté en conséquence) :

int32_t div10(int32_t dividend)
{
    int64_t invDivisor = 0x1999999A;
    return (int32_t) ((invDivisor * dividend) >> 32);
}

Ce qui se passe ici, c'est que nous multiplions par une approximation proche de 1/10 * 2^32 et ensuite nous enlevons les 2^32. Cette approche peut être adaptée à différents diviseurs et différentes largeurs de bits.

Cela fonctionne très bien pour l'architecture ia32, puisque son instruction IMUL placera le produit 64 bits dans edx:eax, et la valeur de edx sera la valeur voulue. Viz (en supposant que le dividende est passé dans eax et le quotient retourné dans eax)

div10 proc 
    mov    edx,1999999Ah    ; load 1/10 * 2^32
    imul   eax              ; edx:eax = dividend / 10 * 2 ^32
    mov    eax,edx          ; eax = dividend / 10
    ret
    endp

Même sur une machine avec une instruction de multiplication lente, cela sera plus rapide qu'une division logicielle ou même matérielle.

16 votes

+1, et je tiens à souligner que le compilateur le fera automatiquement pour vous lorsque vous écrirez "x/10".

1 votes

Hmm, n'y a-t-il pas une certaine inexactitude numérique ici ?

3 votes

Vous aurez toujours des imprécisions numériques lorsque vous divisez des entiers : Qu'obtenez-vous lorsque vous divisez 28 par 10 en utilisant des nombres entiers ? Réponse : 2.

41voto

realtime Points 600

Bien que les réponses données jusqu'à présent correspondent à la question réelle, elles ne correspondent pas au titre. Voici donc une solution fortement inspirée par Le plaisir du hacker qui n'utilise que des décalages de bits.

unsigned divu10(unsigned n) {
    unsigned q, r;
    q = (n >> 1) + (n >> 2);
    q = q + (q >> 4);
    q = q + (q >> 8);
    q = q + (q >> 16);
    q = q >> 3;
    r = n - (((q << 2) + q) << 1);
    return q + (r > 9);
}

Je pense que c'est la meilleure solution pour les architectures qui ne disposent pas d'une instruction de multiplication.

0 votes

Le pdf n'est plus disponible

0 votes

Comment pouvons-nous l'adapter pour 10^N ?

1 votes

Le site original est mort, le lien pointe maintenant vers la version archivée dans la Wayback Machine. Dans le PDF lié, vous trouverez le code pour la division par 100 et 1000. Veuillez noter que ces codes contiennent toujours une opération de multiplication qui devrait être remplacée par des décalages et des additions. De plus, le code de divu100 et divu1000 contient de nombreux décalages qui ne sont pas un multiple de 8, donc si vous êtes sur une architecture qui n'a ni décalage de barillet ni instruction de multiplication, vous feriez mieux d'appliquer divu10 à plusieurs reprises.

21voto

Alois Kraus Points 6179

Bien sûr, vous pouvez le faire si vous pouvez vivre avec une certaine perte de précision. Si vous connaissez la plage de valeurs de vos valeurs d'entrée, vous pouvez proposer un décalage de bits et une multiplication qui sont exacts. Voici quelques exemples de la façon dont vous pouvez diviser par 10, 60, ... comme cela est décrit dans ce blog pour mettre en forme le temps le plus rapide possible.

temp = (ms * 205) >> 11;  // 205/2048 is nearly the same as /10

4 votes

Il faut savoir que la valeur intermédiaire (ms * 205) peut déborder.

2 votes

Si vous faites int ms = 205 * (i >> 11) ; vous obtiendrez de mauvaises valeurs si les nombres sont petits. Vous avez besoin d'une suite de tests pour vous assurer que dans une plage de valeurs donnée, les résultats sont corrects.

2 votes

Ceci est précis pour ms = 0..1028

5voto

Sam Mason Points 1389

Pour développer un peu la réponse d'Alois, nous pouvons développer la proposition de y = (x * 205) >> 11 pour quelques multiples/shifts de plus :

y = (ms *        1) >>  3 // first error 8
y = (ms *        2) >>  4 // 8
y = (ms *        4) >>  5 // 8
y = (ms *        7) >>  6 // 19
y = (ms *       13) >>  7 // 69
y = (ms *       26) >>  8 // 69
y = (ms *       52) >>  9 // 69
y = (ms *      103) >> 10 // 179
y = (ms *      205) >> 11 // 1029
y = (ms *      410) >> 12 // 1029
y = (ms *      820) >> 13 // 1029
y = (ms *     1639) >> 14 // 2739
y = (ms *     3277) >> 15 // 16389
y = (ms *     6554) >> 16 // 16389
y = (ms *    13108) >> 17 // 16389
y = (ms *    26215) >> 18 // 43699
y = (ms *    52429) >> 19 // 262149
y = (ms *   104858) >> 20 // 262149
y = (ms *   209716) >> 21 // 262149
y = (ms *   419431) >> 22 // 699059
y = (ms *   838861) >> 23 // 4194309
y = (ms *  1677722) >> 24 // 4194309
y = (ms *  3355444) >> 25 // 4194309
y = (ms *  6710887) >> 26 // 11184819
y = (ms * 13421773) >> 27 // 67108869

chaque ligne est un calcul unique et indépendant, et vous verrez votre premier résultat "erroné"/incorrect à la valeur indiquée dans le commentaire. il est généralement préférable de prendre le plus petit décalage pour une valeur d'erreur donnée car cela minimisera les bits supplémentaires nécessaires pour stocker la valeur intermédiaire dans le calcul, par ex. (x * 13) >> 7 est "meilleur" que (x * 52) >> 9 car il nécessite deux bits de moins d'overhead, tandis que les deux commencent à donner des réponses erronées au-dessus de 68.

Si vous voulez en calculer davantage, le code suivant (Python) peut être utilisé :

def mul_from_shift(shift):
    mid = 2**shift + 5.
    return int(round(mid / 10.))

et j'ai fait la chose la plus évidente pour calculer quand cette approximation commence à se tromper avec :

def first_err(mul, shift):
    i = 1
    while True:
        y = (i * mul) >> shift
        if y != i // 10:
            return i
        i += 1

(notez que // est utilisé pour la division "entière", c'est-à-dire qu'il tronque/arrondit vers zéro)

la raison du schéma d'erreurs "3/1" (c'est-à-dire que 8 se répète 3 fois suivi de 9) semble être due au changement de bases, c'est-à-dire que les erreurs de base ne sont pas les mêmes. log2(10) est ~3.32. Si nous traçons les erreurs, nous obtenons ce qui suit :

errors

où l'erreur relative est donnée par : mul_from_shift(shift) / (1<<shift) - 0.1

0 votes

Qu'est-ce que ms dans votre test ?

0 votes

@Alexis J'ai emprunté ce nom à la réponse d'Alois, c'est juste la valeur que vous voulez diviser. Peut-être que c'est un raccourci pour "multiplier le décalage" ?

0 votes

Je comprends mais quel est l'intérêt de commenter à chaque ligne alors ?

3voto

chmike Points 2514

Vu la réponse de Kuba Ober, il y en a une autre dans la même veine. Elle utilise une approximation itérative du résultat, mais je ne m'attendrais pas à des performances surprenantes.

Disons que nous devons trouver x donde x = v / 10 .

Nous allons utiliser l'opération inverse v = x * 10 parce qu'il a la propriété agréable que lorsque x = a + b entonces x * 10 = a * 10 + b * 10 .

Laissez-nous utiliser x comme variable détenant la meilleure approximation du résultat jusqu'à présent. Lorsque la recherche se termine, x Tiendra le résultat. On va mettre chaque bit b de x du plus significatif au moins significatif, un par un, pour finir de comparer. (x + b) * 10 avec v . Si elle est inférieure ou égale à v alors le bit b est fixé dans x . Pour tester le bit suivant, il suffit de déplacer b d'une position vers la droite (diviser par deux).

Nous pouvons éviter la multiplication par 10 en maintenant x * 10 y b * 10 dans d'autres variables.

Cela donne l'algorithme suivant pour diviser v par 10.

uin16_t x = 0, x10 = 0, b = 0x1000, b10 = 0xA000;
while (b != 0) {
    uint16_t t = x10 + b10;
    if (t <= v) {
        x10 = t;
        x |= b;
    }
    b10 >>= 1;
    b >>= 1;
}
// x = v / 10

Editar: pour obtenir l'algorithme de Kuba Ober qui évite le besoin de variable x10 on peut soustraire b10 de v y v10 à la place. Dans ce cas x10 n'est plus nécessaire. L'algorithme devient

uin16_t x = 0, b = 0x1000, b10 = 0xA000;
while (b != 0) {
    if (b10 <= v) {
        v -= b10;
        x |= b;
    }
    b10 >>= 1;
    b >>= 1;
}
// x = v / 10

La boucle peut être déroulée et les différentes valeurs de b y b10 peuvent être précalculés comme des constantes.

0 votes

Euh c'est juste une division longue (oui, ce truc que vous avez appris à l'école primaire) pour le binaire plutôt que le décimal.

0 votes

Je ne sais pas ce que vous appelez une longue division. Ce dont je suis sûr, c'est que je n'ai pas appris ça à l'école. Ce que j'apprends à l'école est une méthode différente.

0 votes

Je veux dire fr.wikipedia.org/wiki/Long_division#Méthode Mais lorsque la méthode vous demande d'"obtenir le plus grand nombre entier qui est un multiple du diviseur", gardez à l'esprit que le multiple ne peut être que 1 ou 0 lorsque vous travaillez en base 2. Votre test pour b10 <= v vérifie simplement si le multiple est égal à 1. Quoi qu'il en soit, c'est ainsi que j'ai enseigné la division longue dans le cadre d'un cours d'architecture de systèmes informatiques il y a quelques années. Quelle méthode de division longue décimale avez-vous apprise à l'école ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X