Comment puis-je multiplier et diviser en utilisant uniquement le décalage et l'ajout de bits?


86

Comment puis-je multiplier et diviser en utilisant uniquement le décalage et l'ajout de bits?


20
Comme vous le feriez sur papier au collège, en utilisant uniquement binaire au lieu de décimal.
Pascal Cuoq

1
@mtk: Que manque-t-il dans cette réponse ? Recherchez-vous une implémentation en C ou en assemblage, des largeurs d'opérandes particulières, une méthode de division spécifique (par exemple, restauration vs non restauration)?
njuffa

La soustraction est-elle acceptable? Tout semble être couvert
mksteve

Quel est le besoin derrière cette question? Les processeurs traduisent déjà les opérations de multiplication et de division en décalage de bits et en addition ou soustraction, et si c'est le cas si le compilateur ne l'a pas déjà fait.
Kelly

@ KellyS.French Juste curiosité, c'est plutôt une façon d'imaginer comment un compilateur peut travailler avec un jeu d'instructions restreint.
Spidfire le

Réponses:


77

Pour multiplier en termes d'ajout et de décalage, vous voulez décomposer l'un des nombres par des puissances de deux, comme ceci:

21 * 5 = 10101_2 * 101_2             (Initial step)
       = 10101_2 * (1 * 2^2  +  0 * 2^1  +  1 * 2^0)
       = 10101_2 * 2^2 + 10101_2 * 2^0 
       = 10101_2 << 2 + 10101_2 << 0 (Decomposed)
       = 10101_2 * 4 + 10101_2 * 1
       = 10101_2 * 5
       = 21 * 5                      (Same as initial expression)

( _2signifie base 2)

Comme vous pouvez le voir, la multiplication peut être décomposée en ajout et en décalage et inversement. C'est aussi pourquoi la multiplication prend plus de temps que les décalages de bits ou l'addition - c'est O (n ^ 2) plutôt que O (n) dans le nombre de bits. Les systèmes informatiques réels (par opposition aux systèmes informatiques théoriques) ont un nombre fini de bits, de sorte que la multiplication prend un multiple constant de temps par rapport à l'addition et au décalage. Si je me souviens bien, les processeurs modernes, s'ils sont correctement mis en pipeline, peuvent faire la multiplication à peu près aussi vite que l'addition, en jouant avec l'utilisation des ALU (unités arithmétiques) dans le processeur.


4
Je sais que c'était il y a quelque temps, mais pourriez-vous donner un exemple avec division? Merci
GniruT

42

La réponse d'Andrew Toulouse peut être étendue à la division.

La division par constantes entières est examinée en détail dans le livre "Hacker's Delight" de Henry S. Warren (ISBN 9780201914658).

La première idée pour implémenter la division est d'écrire la valeur inverse du dénominateur en base deux.

Par exemple, 1/3 = (base-2) 0.0101 0101 0101 0101 0101 0101 0101 0101 .....

Donc, a/3 = (a >> 2) + (a >> 4) + (a >> 6) + ... + (a >> 30) pour l'arithmétique 32 bits.

En combinant les termes de manière évidente, nous pouvons réduire le nombre d'opérations:

b = (a >> 2) + (a >> 4)

b += (b >> 4)

b += (b >> 8)

b += (b >> 16)

Il existe des méthodes plus intéressantes pour calculer la division et les restes.

EDIT1:

Si l'OP signifie multiplication et division de nombres arbitraires, pas la division par un nombre constant, alors ce fil pourrait être utile: https://stackoverflow.com/a/12699549/1182653

EDIT2:

L'un des moyens les plus rapides de diviser par des constantes entières est d'exploiter l'arithmétique modulaire et la réduction de Montgomery: quel est le moyen le plus rapide de diviser un entier par 3?


Merci beaucoup pour la référence Hacker's Delight!
alecxe

2
Eh oui, cette réponse (division par constante) n'est que partiellement correcte. Si vous essayez de faire «3/3», vous vous retrouverez avec 0. Dans Hacker's Delight, ils expliquent en fait qu'il y a une erreur que vous devez compenser. Dans ce cas: b += r * 11 >> 5avec r = a - q * 3. Lien: hackersdelight.org/divcMore.pdf page 2+.
atlaste le

30

X * 2 = décalage de 1 bit vers la gauche
X / 2 = décalage de 1 bit vers la droite
X * 3 = décalage de 1 bit vers la gauche, puis additionnez X


4
Voulez-vous dire add Xpour ce dernier?
Mark Byers

1
C'est toujours faux - la dernière ligne devrait se lire: "X * 3 = décaler vers la gauche de 1 bit puis ajouter X"
Paul R

1
"X / 2 = décalage de 1 bit à droite", pas entièrement, il s'arrondit à l'infini, plutôt qu'à 0 (pour les nombres négatifs), ce qui est l'implémentation habituelle de la division (du moins pour autant que j'ai vu).
Leif Andersen

25

x << k == x multiplied by 2 to the power of k
x >> k == x divided by 2 to the power of k

Vous pouvez utiliser ces décalages pour effectuer n'importe quelle opération de multiplication. Par exemple:

x * 14 == x * 16 - x * 2 == (x << 4) - (x << 1)
x * 12 == x * 8 + x * 4 == (x << 3) + (x << 2)

Pour diviser un nombre par une non-puissance de deux, je ne connais aucun moyen facile, à moins que vous ne souhaitiez implémenter une logique de bas niveau, utiliser d'autres opérations binaires et utiliser une forme d'itération.


@IVlad: Comment combineriez-vous les opérations ci-dessus pour effectuer, disons, diviser par 3?
Paul R

@Paul R - vrai, c'est plus difficile. J'ai clarifié ma réponse.
IVlad

la division par une constante n'est pas trop difficile (multiplier par une constante magique puis diviser par une puissance de 2), mais la division par une variable est un peu plus délicate.
Paul R

1
ne devrait pas x * 14 == x * 16 - x * 2 == (x << 4) - (x << 2) vraiment finir par être (x << 4) - (x << 1) puisque x < <1 multiplie par x par 2?
Alex Spencer

18
  1. Un décalage vers la gauche de 1 position équivaut à une multiplication par 2. Un décalage vers la droite équivaut à une division par 2.
  2. Vous pouvez ajouter une boucle pour multiplier. En sélectionnant correctement la variable de boucle et la variable d'addition, vous pouvez lier les performances. Une fois que vous avez exploré cela, vous devriez utiliser la multiplication paysanne

9
+1: Mais le décalage vers la gauche est non seulement analogue à la multiplication par 2. Il est multiplier par 2. Au moins jusqu'à ce débordement ...
Don Roby

La division par décalage donne des résultats incorrects pour les nombres négatifs.
David

6

J'ai traduit le code Python en C. L'exemple donné avait un petit défaut. Si la valeur de dividende occupait tous les 32 bits, le décalage échouerait. Je viens d'utiliser des variables 64 bits en interne pour contourner le problème:

int No_divide(int nDivisor, int nDividend, int *nRemainder)
{
    int nQuotient = 0;
    int nPos = -1;
    unsigned long long ullDivisor = nDivisor;
    unsigned long long ullDividend = nDividend;

    while (ullDivisor <  ullDividend)
    {
        ullDivisor <<= 1;
        nPos ++;
    }

    ullDivisor >>= 1;

    while (nPos > -1)
    {
        if (ullDividend >= ullDivisor)
        {
            nQuotient += (1 << nPos);
            ullDividend -= ullDivisor;
        }

        ullDivisor >>= 1;
        nPos -= 1;
    }

    *nRemainder = (int) ullDividend;

    return nQuotient;
}

Et le nombre négatif? J'ai testé -12345 avec 10 en utilisant eclipse + CDT, mais le résultat n'était pas si bon.
kenmux

Pouvez-vous me dire pourquoi vous faites ullDivisor >>= 1avant la whileboucle? De plus, ne fera pas nPos >= 0l'affaire?
Vivekanand V le

@kenmux Vous devez considérer uniquement l'ampleur des nombres impliqués, d'abord, faites l'algorithme et ensuite en utilisant des déclarations de prise de décision appropriées, renvoyez le signe approprié au quotient / reste!
Vivekanand V le

1
@VivekanandV Vous voulez dire ajouter le signe - plus tard? Oui cela fonctionne.
kenmux le

5

Une procédure de division des nombres entiers qui utilise des décalages et des ajouts peut être dérivée de manière directe de la division décimale à la main comme enseignée à l'école élémentaire. La sélection de chaque chiffre du quotient est simplifiée, car le chiffre est soit 0 soit 1: si le reste courant est supérieur ou égal au diviseur, le bit le moins significatif du quotient partiel est 1.

Tout comme pour la division décimale à la main, les chiffres du dividende sont considérés du plus significatif au moins significatif, un chiffre à la fois. Ceci est facilement accompli par un décalage vers la gauche dans la division binaire. De plus, les bits de quotient sont rassemblés en décalant vers la gauche les bits de quotient actuels d'une position, puis en ajoutant le nouveau bit de quotient.

Dans un agencement classique, ces deux décalages vers la gauche sont combinés en un décalage vers la gauche d'une paire de registres. La moitié supérieure détient le reste actuel, la moitié inférieure initiale détient le dividende. Lorsque les bits de dividende sont transférés vers le registre de reste par décalage vers la gauche, les bits les moins significatifs non utilisés de la moitié inférieure sont utilisés pour accumuler les bits de quotient.

Vous trouverez ci-dessous le langage d'assemblage x86 et les implémentations C de cet algorithme. Cette variante particulière d'une division shift & add est parfois appelée variante "non performante", car la soustraction du diviseur du reste actuel n'est pas effectuée à moins que le reste soit supérieur ou égal au diviseur. En C, il n'y a pas de notion du drapeau de retenue utilisé par la version d'assemblage dans le décalage gauche de la paire de registres. Au lieu de cela, il est émulé, sur la base de l'observation que le résultat d'une addition modulo 2 n peut être plus petit que l'ajout uniquement s'il y a eu une exécution.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#define USE_ASM 0

#if USE_ASM
uint32_t bitwise_division (uint32_t dividend, uint32_t divisor)
{
    uint32_t quot;
    __asm {
        mov  eax, [dividend];// quot = dividend
        mov  ecx, [divisor]; // divisor
        mov  edx, 32;        // bits_left
        mov  ebx, 0;         // rem
    $div_loop:
        add  eax, eax;       // (rem:quot) << 1
        adc  ebx, ebx;       //  ...
        cmp  ebx, ecx;       // rem >= divisor ?
        jb  $quot_bit_is_0;  // if (rem < divisor)
    $quot_bit_is_1:          // 
        sub  ebx, ecx;       // rem = rem - divisor
        add  eax, 1;         // quot++
    $quot_bit_is_0:
        dec  edx;            // bits_left--
        jnz  $div_loop;      // while (bits_left)
        mov  [quot], eax;    // quot
    }            
    return quot;
}
#else
uint32_t bitwise_division (uint32_t dividend, uint32_t divisor)
{
    uint32_t quot, rem, t;
    int bits_left = CHAR_BIT * sizeof (uint32_t);

    quot = dividend;
    rem = 0;
    do {
            // (rem:quot) << 1
            t = quot;
            quot = quot + quot;
            rem = rem + rem + (quot < t);

            if (rem >= divisor) {
                rem = rem - divisor;
                quot = quot + 1;
            }
            bits_left--;
    } while (bits_left);
    return quot;
}
#endif

@greybeard Merci pour le pointeur, vous avez raison, j'ai mélangé le dividende avec le quotient. Je le réparerai.
njuffa

4

Prenez deux nombres, disons 9 et 10, écrivez-les en binaire - 1001 et 1010.

Commencez avec un résultat, R, de 0.

Prenez l'un des nombres, 1010 dans ce cas, nous l'appellerons A, et décalerons-le d'un bit vers la droite, si vous en décalez un, ajoutez le premier numéro, nous l'appellerons B, à R.

Maintenant, décalez B vers la gauche d'un bit et répétez jusqu'à ce que tous les bits aient été décalés de A.

Il est plus facile de voir ce qui se passe si vous le voyez écrit, voici l'exemple:

      0
   0000      0
  10010      1
 000000      0
1001000      1
 ------
1011010

Cela semble le plus rapide, nécessite juste un peu de codage supplémentaire pour parcourir les bits du plus petit nombre et calculer le résultat.
Hellonearthis

2

Pris d' ici .

Ceci est uniquement pour la division:

int add(int a, int b) {
        int partialSum, carry;
        do {
            partialSum = a ^ b;
            carry = (a & b) << 1;
            a = partialSum;
            b = carry;
        } while (carry != 0);
        return partialSum;
}

int subtract(int a, int b) {
    return add(a, add(~b, 1));
}

int division(int dividend, int divisor) {
        boolean negative = false;
        if ((dividend & (1 << 31)) == (1 << 31)) { // Check for signed bit
            negative = !negative;
            dividend = add(~dividend, 1);  // Negation
        }
        if ((divisor & (1 << 31)) == (1 << 31)) {
            negative = !negative;
            divisor = add(~divisor, 1);  // Negation
        }
        int quotient = 0;
        long r;
        for (int i = 30; i >= 0; i = subtract(i, 1)) {
            r = (divisor << i);
           // Left shift divisor until it's smaller than dividend
            if (r < Integer.MAX_VALUE && r >= 0) { // Avoid cases where comparison between long and int doesn't make sense
                if (r <= dividend) { 
                    quotient |= (1 << i);    
                    dividend = subtract(dividend, (int) r);
                }
            }
        }
        if (negative) {
            quotient = add(~quotient, 1);
        }
        return quotient;
}

2

c'est essentiellement multiplier et diviser avec la puissance de base 2

décalage gauche = x * 2 ^ y

décalage vers la droite = x / 2 ^ y

shl eax, 2 = 2 * 2 ^ 2 = 8

shr eax, 3 = 2/2 ^ 3 = 1/4


eaxne peut pas contenir une valeur fractionnaire comme 1/4. (Sauf si vous utilisez la virgule fixe au lieu d'un entier, mais vous ne l'avez pas spécifié)
Peter Cordes

1

Cela devrait fonctionner pour la multiplication:

.data

.text
.globl  main

main:

# $4 * $5 = $2

    addi $4, $0, 0x9
    addi $5, $0, 0x6

    add  $2, $0, $0 # initialize product to zero

Loop:   
    beq  $5, $0, Exit # if multiplier is 0,terminate loop
    andi $3, $5, 1 # mask out the 0th bit in multiplier
    beq  $3, $0, Shift # if the bit is 0, skip add
    addu $2, $2, $4 # add (shifted) multiplicand to product

Shift: 
    sll $4, $4, 1 # shift up the multiplicand 1 bit
    srl $5, $5, 1 # shift down the multiplier 1 bit
    j Loop # go for next  

Exit: #


EXIT: 
li $v0,10
syscall

Quelle saveur d'assemblage?
Keith Pinson

1
C'est l'assemblage MIPS, si c'est ce que vous demandez. Je pense que j'ai utilisé MARS pour l'écrire / l'exécuter.
Melsi

1

La méthode ci-dessous est la mise en œuvre de la division binaire en considérant que les deux nombres sont positifs. Si la soustraction est un problème, nous pouvons l'implémenter également en utilisant des opérateurs binaires.

Code

-(int)binaryDivide:(int)numerator with:(int)denominator
{
    if (numerator == 0 || denominator == 1) {
        return numerator;
    }

    if (denominator == 0) {

        #ifdef DEBUG
            NSAssert(denominator==0, @"denominator should be greater then 0");
        #endif
        return INFINITY;
    }

    // if (numerator <0) {
    //     numerator = abs(numerator);
    // }

    int maxBitDenom = [self getMaxBit:denominator];
    int maxBitNumerator = [self getMaxBit:numerator];
    int msbNumber = [self getMSB:maxBitDenom ofNumber:numerator];

    int qoutient = 0;

    int subResult = 0;

    int remainingBits = maxBitNumerator-maxBitDenom;

    if (msbNumber >= denominator) {
        qoutient |=1;
        subResult = msbNumber - denominator;
    }
    else {
        subResult = msbNumber;
    }

    while (remainingBits > 0) {
        int msbBit = (numerator & (1 << (remainingBits-1)))>0?1:0;
        subResult = (subResult << 1) | msbBit;
        if(subResult >= denominator) {
            subResult = subResult - denominator;
            qoutient= (qoutient << 1) | 1;
        }
        else{
            qoutient = qoutient << 1;
        }
        remainingBits--;

    }
    return qoutient;
}

-(int)getMaxBit:(int)inputNumber
{
    int maxBit = 0;
    BOOL isMaxBitSet = NO;
    for (int i=0; i<sizeof(inputNumber)*8; i++) {
        if (inputNumber & (1<<i)) {
            maxBit = i;
            isMaxBitSet=YES;
        }
    }
    if (isMaxBitSet) {
        maxBit+=1;
    }
    return maxBit;
}


-(int)getMSB:(int)bits ofNumber:(int)number
{
    int numbeMaxBit = [self getMaxBit:number];
    return number >> (numbeMaxBit - bits);
}

Pour la multiplication:

-(int)multiplyNumber:(int)num1 withNumber:(int)num2
{
    int mulResult = 0;
    int ithBit;

    BOOL isNegativeSign = (num1<0 && num2>0) || (num1>0 && num2<0);
    num1 = abs(num1);
    num2 = abs(num2);


    for (int i=0; i<sizeof(num2)*8; i++)
    {
        ithBit =  num2 & (1<<i);
        if (ithBit>0) {
            mulResult += (num1 << i);
        }

    }

    if (isNegativeSign) {
        mulResult =  ((~mulResult)+1);
    }

    return mulResult;
}

Quelle est cette syntaxe? -(int)multiplyNumber:(int)num1 withNumber:(int)num2?
SS Anne

0

Pour toute personne intéressée par une solution x86 16 bits, il existe un morceau de code de JasonKnight ici 1 (il comprend également un morceau de multiplication signé, que je n'ai pas testé). Cependant, ce code a des problèmes avec des entrées volumineuses, où la partie "add bx, bx" déborderait.

La version fixe:

softwareMultiply:
;    INPUT  CX,BX
;   OUTPUT  DX:AX - 32 bits
; CLOBBERS  BX,CX,DI
    xor   ax,ax     ; cheap way to zero a reg
    mov   dx,ax     ; 1 clock faster than xor
    mov   di,cx
    or    di,bx     ; cheap way to test for zero on both regs
    jz    @done
    mov   di,ax     ; DI used for reg,reg adc
@loop:
    shr   cx,1      ; divide by two, bottom bit moved to carry flag
    jnc   @skipAddToResult
    add   ax,bx
    adc   dx,di     ; reg,reg is faster than reg,imm16
@skipAddToResult:
    add   bx,bx     ; faster than shift or mul
    adc   di,di
    or    cx,cx     ; fast zero check
    jnz   @loop
@done:
    ret

Ou la même chose dans l'assemblage en ligne GCC:

asm("mov $0,%%ax\n\t"
    "mov $0,%%dx\n\t"
    "mov %%cx,%%di\n\t"
    "or %%bx,%%di\n\t"
    "jz done\n\t"
    "mov %%ax,%%di\n\t"
    "loop:\n\t"
    "shr $1,%%cx\n\t"
    "jnc skipAddToResult\n\t"
    "add %%bx,%%ax\n\t"
    "adc %%di,%%dx\n\t"
    "skipAddToResult:\n\t"
    "add %%bx,%%bx\n\t"
    "adc %%di,%%di\n\t"
    "or %%cx,%%cx\n\t"
    "jnz loop\n\t"
    "done:\n\t"
    : "=d" (dx), "=a" (ax)
    : "b" (bx), "c" (cx)
    : "ecx", "edi"
);

-1

Essaye ça. https://gist.github.com/swguru/5219592

import sys
# implement divide operation without using built-in divide operator
def divAndMod_slow(y,x, debug=0):
    r = 0
    while y >= x:
            r += 1
            y -= x
    return r,y 


# implement divide operation without using built-in divide operator
def divAndMod(y,x, debug=0):

    ## find the highest position of positive bit of the ratio
    pos = -1
    while y >= x:
            pos += 1
            x <<= 1
    x >>= 1
    if debug: print "y=%d, x=%d, pos=%d" % (y,x,pos)

    if pos == -1:
            return 0, y

    r = 0
    while pos >= 0:
            if y >= x:
                    r += (1 << pos)                        
                    y -= x                
            if debug: print "y=%d, x=%d, r=%d, pos=%d" % (y,x,r,pos)

            x >>= 1
            pos -= 1

    return r, y


if __name__ =="__main__":
    if len(sys.argv) == 3:
        y = int(sys.argv[1])
        x = int(sys.argv[2])
     else:
            y = 313271356
            x = 7

print "=== Slow Version ...."
res = divAndMod_slow( y, x)
print "%d = %d * %d + %d" % (y, x, res[0], res[1])

print "=== Fast Version ...."
res = divAndMod( y, x, debug=1)
print "%d = %d * %d + %d" % (y, x, res[0], res[1])

5
Cela ressemble à du python. La question a été posée pour le montage et / ou C.
void
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.