web-dev-qa-db-fra.com

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

Est-il possible de diviser un entier non signé par 10 en utilisant des décalages de bits purs, l'addition, la soustraction et peut-être multiplier? Utiliser un processeur avec des ressources très limitées et une division lente.

40
Thomas O

Voici ce que fait le compilateur Microsoft lors de la compilation de divisions par petites constantes intégrales. Supposons une machine 32 bits (le code peut être ajusté 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, puis supprimons 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, car son instruction IMUL mettra le produit 64 bits dans edx: eax, et la valeur edx sera la valeur souhaitée. Viz (en supposant que le dividende est passé en eax et le quotient retourné en 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, ce sera plus rapide qu'une division logicielle.

55
John Källén

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 de Hacker's Delight qui n'utilise vraiment 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 manquent d'une instruction multiplier.

30
realtime

Bien sûr, vous pouvez 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 trouver un décalage de bits et une multiplication qui est exacte. Quelques exemples comment vous pouvez diviser par 10, 60, ... comme il est décrit dans ce blog au format temps le plus rapide possible.

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

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

Disons que nous devons trouver xx = v / 10.

Nous allons utiliser l'opération inverse v = x * 10 car il a la propriété Nice quand x = a + b, puis x * 10 = a * 10 + b * 10.

Soit x comme variable contenant la meilleure approximation du résultat jusqu'à présent. Une fois la recherche terminée, x contiendra le résultat. Nous allons définir chaque bit b de x du plus significatif au moins significatif, un par un, pour terminer la comparaison (x + b) * 10 avec v. Si sa valeur est inférieure ou égale à v, le bit b est défini dans x. Pour tester le bit suivant, il suffit de décaler b d'une position vers la droite (diviser par deux).

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

Cela produit 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

Edit: pour obtenir l'algorithme de Kuba Ober qui évite le besoin de variable x10, nous pouvons soustraire b10 de v et v10 au lieu. 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 et b10 peut être précalculé sous forme de constantes.

3
chmike

Eh bien, la division est une soustraction, alors oui. Décaler à droite de 1 (diviser par 2). Soustrayez maintenant 5 du résultat, en comptant le nombre de fois que vous effectuez la soustraction jusqu'à ce que la valeur soit inférieure à 5. Le résultat est le nombre de soustractions que vous avez faites. Oh, et la division va probablement être plus rapide.

Une stratégie hybride de décalage à droite puis de division par 5 en utilisant la division normale pourrait vous permettre d'améliorer les performances si la logique du diviseur ne le fait pas déjà pour vous.

2
tvanfosson

Sur les architectures qui ne peuvent changer qu'un seul endroit à la fois, une série de comparaisons explicites contre des puissances décroissantes de deux multipliées par 10 pourrait mieux fonctionner que la solution du plaisir des pirates. En supposant un dividende de 16 bits:

uint16_t div10(uint16_t dividend) {
  uint16_t quotient = 0;
  #define div10_step(n) \
    do { if (dividend >= (n*10)) { quotient += n; dividend -= n*10; } } while (0)
  div10_step(0x1000);
  div10_step(0x0800);
  div10_step(0x0400);
  div10_step(0x0200);
  div10_step(0x0100);
  div10_step(0x0080);
  div10_step(0x0040);
  div10_step(0x0020);
  div10_step(0x0010);
  div10_step(0x0008);
  div10_step(0x0004);
  div10_step(0x0002);
  div10_step(0x0001);
  #undef div10_step
  if (dividend >= 5) ++quotient; // round the result (optional)
  return quotient;
}
2
Kuba Ober

pour développer un peu la réponse d'Alois, nous pouvons étendre la y = (x * 205) >> 11 suggérée pour quelques multiples/décalages supplémentaires:

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 "erreur"/résultat 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 a besoin de deux bits de moins, alors que les deux commencent à donner de mauvaises réponses au-dessus de 68.

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

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

et j'ai fait la chose évidente pour calculer quand cette approximation commence à mal tourner 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 motif "3/1" dans les erreurs (c'est-à-dire 8 répétitions 3 fois suivies de 9) semble être due au changement de bases, c'est-à-dire que 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

2
Sam Mason