Como posso multiplicar e dividir usando apenas deslocamento e adição de bits?


86

Como posso multiplicar e dividir usando apenas deslocamento e adição de bits?


20
Como você faria no papel no ensino médio, apenas usando binário em vez de decimal.
Pascal Cuoq

1
@mtk: O que está faltando nesta resposta ? Você está procurando uma implementação de C ou de montagem, larguras de operandos particulares, um método de divisão específico (por exemplo, restauração versus não restauração)?
njuffa

A subtração está OK? Tudo parece estar coberto
mksteve,

Qual é a necessidade por trás dessa pergunta? Os CPU's traduzem as operações de multiplicação e divisão em bitshifting e adição ou subtração já, e se isso acontecer se o compilador ainda não tiver feito isso.
Kelly S. French

@ KellyS.French Só por curiosidade, é mais uma maneira de imaginar como um compilador pode trabalhar com um conjunto de instruções restrito.
Spidfire de

Respostas:


76

Para multiplicar em termos de adição e deslocamento, você deseja decompor um dos números em potências de dois, assim:

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)

( _2significa base 2)

Como você pode ver, a multiplicação pode ser decomposta em soma e deslocamento e vice-versa. É também por isso que a multiplicação leva mais tempo do que deslocamentos de bits ou adição - é O (n ^ 2) em vez de O (n) no número de bits. Os sistemas de computador reais (em oposição aos sistemas de computador teóricos) têm um número finito de bits, então a multiplicação leva um múltiplo constante de tempo em comparação com a adição e o deslocamento. Se bem me lembro, os processadores modernos, se canalizados corretamente, podem fazer multiplicação quase tão rápido quanto a adição, mexendo com a utilização das ALUs (unidades aritméticas) no processador.


4
Eu sei que foi há um tempo, mas você poderia dar um exemplo com divisão? Obrigado
GniruT

42

A resposta de Andrew Toulouse pode ser estendida à divisão.

A divisão por constantes inteiras é considerada em detalhes no livro "Hacker's Delight" de Henry S. Warren (ISBN 9780201914658).

A primeira ideia para implementar a divisão é escrever o valor inverso do denominador na base dois.

Por exemplo, 1/3 = (base-2) 0.0101 0101 0101 0101 0101 0101 0101 0101 .....

Portanto, a/3 = (a >> 2) + (a >> 4) + (a >> 6) + ... + (a >> 30) para aritmética de 32 bits.

Ao combinar os termos de maneira óbvia, podemos reduzir o número de operações:

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

b += (b >> 4)

b += (b >> 8)

b += (b >> 16)

Existem maneiras mais interessantes de calcular a divisão e os remanescentes.

EDIT1:

Se o OP significa multiplicação e divisão de números arbitrários, não a divisão por um número constante, este tópico pode ser útil: https://stackoverflow.com/a/12699549/1182653

EDIT2:

Uma das maneiras mais rápidas de dividir por constantes inteiras é explorar a aritmética modular e a redução de Montgomery: Qual é a maneira mais rápida de dividir um inteiro por 3?


Muito obrigado pela referência do Hacker's Delight!
alecxe

2
Ehm sim, esta resposta (divisão por constante) é apenas parcialmente correta. Se você tentar fazer '3/3', acabará com 0. No Hacker's Delight, eles explicam que há um erro que você deve compensar. Neste caso: b += r * 11 >> 5com r = a - q * 3. Link: hackersdelight.org/divcMore.pdf página 2+.
atlaste de

29

X * 2 = deslocamento de 1 bit para a esquerda
X / 2 = deslocamento de 1 bit para a direita
X * 3 = deslocamento para a esquerda 1 bit e, em seguida, adicionar X


4
Você quer dizer add Xpor aquele último?
Mark Byers

1
Ainda está errado - a última linha deve ser: "X * 3 = deslocar para a esquerda 1 bit e, em seguida, adicionar X"
Paul R

1
"X / 2 = 1 bit shift right", não inteiramente, ele arredonda para o infinito, ao invés de até 0 (para números negativos), que é a implementação usual da divisão (pelo menos até onde eu vi).
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

Você pode usar essas mudanças para fazer qualquer operação de multiplicação. Por exemplo:

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

Para dividir um número por uma não potência de dois, não conheço nenhuma maneira fácil, a menos que você queira implementar alguma lógica de baixo nível, usar outras operações binárias e usar alguma forma de iteração.


@IVlad: Como você combinaria as operações acima para realizar, digamos, dividir por 3?
Paul R

@Paul R - verdade, isso é mais difícil. Eu esclareci minha resposta.
IVlad

a divisão por uma constante não é muito difícil (multiplique por constante mágica e depois divida pela potência de 2), mas a divisão por uma variável é um pouco mais complicada.
Paul R

1
não deveria x * 14 == x * 16 - x * 2 == (x << 4) - (x << 2) realmente acabar sendo (x << 4) - (x << 1) já que x < <1 está multiplicando por x por 2?
Alex Spencer

17
  1. Um deslocamento para a esquerda por 1 posição é análogo a multiplicar por 2. Um deslocamento para a direita é análogo a dividir por 2.
  2. Você pode adicionar um loop para multiplicar. Ao escolher a variável de loop e a variável de adição corretamente, você pode limitar o desempenho. Depois de explorar isso, você deve usar a Multiplicação Camponesa

9
+1: Mas o desvio para a esquerda não é apenas análogo a multiplicar por 2. Ele está multiplicando por 2. Pelo menos até estouro ...
Don Roby

A divisão por turnos produz resultados incorretos para números negativos.
David

6

Traduzi o código Python para C. O exemplo dado tinha uma pequena falha. Se o valor do dividendo ocupasse todos os 32 bits, a mudança falharia. Acabei de usar variáveis ​​de 64 bits internamente para contornar o problema:

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;
}

E quanto ao número negativo? Testei -12345 com 10 usando eclipse + CDT, mas o resultado não foi tão bom.
kenmux

5

Um procedimento para dividir inteiros que usa deslocamentos e adições pode ser derivado de forma direta da divisão decimal à mão, conforme ensinado na escola primária. A seleção de cada dígito quociente é simplificada, pois o dígito é 0 e 1: se o resto atual for maior ou igual ao divisor, o bit menos significativo do quociente parcial é 1.

Assim como na divisão longa decimal, os dígitos do dividendo são considerados do mais significativo para o menos significativo, um dígito de cada vez. Isso é facilmente realizado por um deslocamento à esquerda na divisão binária. Além disso, os bits de quociente são reunidos deslocando-se para a esquerda os bits de quociente atuais em uma posição e, em seguida, acrescentando o novo bit de quociente.

Em um arranjo clássico, esses dois deslocamentos à esquerda são combinados no deslocamento à esquerda de um par de registros. A metade superior mantém o restante atual, a metade inferior inicial contém o dividendo. À medida que os bits do dividendo são transferidos para o registro restante pelo deslocamento para a esquerda, os bits menos significativos não usados ​​da metade inferior são usados ​​para acumular os bits quocientes.

Abaixo está a linguagem assembly x86 e as implementações C desse algoritmo. Esta variante particular de uma divisão shift & add é algumas vezes referida como a variante "sem desempenho", uma vez que a subtração do divisor do resto atual não é executada a menos que o resto seja maior ou igual ao divisor. Em C, não há noção do sinalizador de transporte usado pela versão do assembly no deslocamento à esquerda do par de registradores. Em vez disso, ele é emulado, com base na observação de que o resultado de um módulo de adição 2 n pode ser menor que qualquer um dos adendos apenas se houver um carry out.

#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 Obrigado pelo ponteiro, você está correto, eu misturei o dividendo com o quociente. Eu resolvo isso.
njuffa

4

Pegue dois números, digamos 9 e 10, escreva-os como binários - 1001 e 1010.

Comece com um resultado, R, de 0.

Pegue um dos números, 1010 neste caso, vamos chamá-lo de A, e desloque-o um bit à direita, se você deslocar um, adicione o primeiro número, vamos chamá-lo de B, para R.

Agora mude B para a esquerda um bit e repita até que todos os bits tenham sido deslocados de A.

É mais fácil ver o que está acontecendo se você vir escrito, este é o exemplo:

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

Isso parece mais rápido, requer apenas um pouco de codificação extra para percorrer os bits do menor número e calcular o resultado.
Hellonearthis

2

Retirado daqui .

Isso é apenas para divisão:

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

é basicamente multiplicar e dividir com a potência de base 2

deslocar para a esquerda = x * 2 ^ y

deslocar para a direita = x / 2 ^ y

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

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


eaxnão pode conter um valor fracionário como 1/4. (A menos que você esteja usando ponto fixo em vez de inteiro, mas você não especificou isso)
Peter Cordes

1

Isso deve funcionar para multiplicação:

.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

Que sabor de montagem?
Keith Pinson de

1
É montagem MIPS, se é isso que você está perguntando. Acho que usei o MARS para escrever / executá-lo.
Melsi,

1

O método abaixo é a implementação da divisão binária considerando que ambos os números são positivos. Se a subtração for uma preocupação, podemos implementar isso também usando operadores binários.

Código

-(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);
}

Para multiplicação:

-(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;
}

Qual é essa sintaxe? -(int)multiplyNumber:(int)num1 withNumber:(int)num2?
SS Anne,

0

Para qualquer pessoa interessada em uma solução x86 de 16 bits, há um trecho de código de JasonKnight aqui 1 (ele também inclui um trecho de multiplicação assinado, que não testei). No entanto, esse código tem problemas com entradas grandes, onde a parte "add bx, bx" estouraria.

A versão fixa:

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 o mesmo na montagem em linha do 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

Experimente isso. 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
Isso se parece com python. A pergunta foi feita para a assembleia e / ou C.
nulo em
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.