Làm thế nào tôi có thể nhân và chia chỉ sử dụng dịch chuyển và cộng bit?


86

Làm thế nào tôi có thể nhân và chia chỉ sử dụng dịch chuyển và cộng bit?


20
Giống như bạn sẽ làm điều đó trên giấy ở trường trung học, chỉ sử dụng nhị phân thay vì thập phân.
Pascal Cuoq

1
@mtk: Câu trả lời này còn thiếu điều gì? Bạn đang tìm kiếm C hoặc thực thi hợp ngữ, độ rộng toán hạng cụ thể, một phương pháp phân chia cụ thể (ví dụ: khôi phục so với không khôi phục)?
njuffa

Phép trừ có được không? Tất cả dường như được bao phủ
mksteve

Cần gì đằng sau câu hỏi này? CPU đã dịch các phép toán nhân và chia thành chuyển bit và cộng hoặc trừ, và nếu đó là nếu trình biên dịch chưa làm như vậy.
Kelly S. French

@ KellyS.French Chỉ là sự tò mò, đó là một cách để tưởng tượng cách một trình biên dịch có thể hoạt động với một tập lệnh hạn chế.
Spidfire

Câu trả lời:


77

Để nhân về phép cộng và chuyển, bạn muốn phân tích một trong các số thành lũy thừa của hai, như sau:

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)

( _2nghĩa là cơ số 2)

Như bạn thấy, phép nhân có thể được phân tách thành phép cộng và dịch chuyển và lùi lại. Đây cũng là lý do tại sao phép nhân mất nhiều thời gian hơn so với dịch chuyển bit hoặc cộng - đó là O (n ^ 2) chứ không phải là O (n) về số lượng bit. Các hệ thống máy tính thực (trái ngược với các hệ thống máy tính lý thuyết) có số lượng bit hữu hạn, vì vậy phép nhân cần một bội số thời gian không đổi so với phép cộng và phép chuyển. Nếu tôi nhớ lại một cách chính xác, các bộ xử lý hiện đại, nếu được ghép nối đúng cách, có thể thực hiện phép nhân nhanh như phép cộng, bằng cách sử dụng các ALU (đơn vị số học) trong bộ xử lý.


4
Tôi biết đó là một thời gian trước đây, nhưng bạn có thể cho một ví dụ với phép chia không? Cảm ơn
GniruT 14/10/15

42

Câu trả lời của Andrew Toulouse có thể được mở rộng thành phân chia.

Sự phân chia theo hằng số nguyên được xem xét chi tiết trong cuốn sách "Hacker's Delight" của Henry S. Warren (ISBN 9780201914658).

Ý tưởng đầu tiên để thực hiện phép chia là viết giá trị nghịch đảo của mẫu số trong cơ số hai.

Ví dụ, 1/3 = (base-2) 0.0101 0101 0101 0101 0101 0101 0101 0101 .....

Vì vậy, a/3 = (a >> 2) + (a >> 4) + (a >> 6) + ... + (a >> 30) đối với số học 32 bit.

Bằng cách kết hợp các thuật ngữ một cách rõ ràng, chúng ta có thể giảm số lượng hoạt động:

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

b += (b >> 4)

b += (b >> 8)

b += (b >> 16)

Có nhiều cách thú vị hơn để tính phép chia và phần dư.

CHỈNH SỬA1:

Nếu OP có nghĩa là nhân và chia các số tùy ý, không phải là phép chia cho một số không đổi, thì chủ đề này có thể được sử dụng: https://stackoverflow.com/a/12699549/1182653

EDIT2:

Một trong những cách nhanh nhất để chia cho các hằng số nguyên là khai thác số học mô-đun và giảm Montgomery: Cách nhanh nhất để chia một số nguyên cho 3 là gì?


Cảm ơn rất nhiều vì đã tham khảo Hacker's Delight!
alecxe

2
Ehm vâng, câu trả lời này (chia cho hằng số) chỉ đúng một phần . Nếu bạn cố gắng thực hiện '3/3', bạn sẽ nhận được kết quả là 0. Trong Hacker's Delight, họ thực sự giải thích rằng có một lỗi mà bạn phải bồi thường. Trong trường hợp này: b += r * 11 >> 5với r = a - q * 3. Liên kết: hackerdelight.org/divcMore.pdf trang 2+.
atlaste

30

X * 2 = 1 bit dịch sang trái
X / 2 = 1 bit dịch sang phải
X * 3 = dịch sang trái 1 bit và sau đó thêm X


4
Ý bạn là người add Xcuối cùng?
Đánh dấu Byers

1
Nó vẫn sai - dòng cuối cùng nên đọc: "X * 3 = shift sang trái 1 bit và sau đó thêm X"
Paul R

1
"X / 2 = 1 bit shift right", không phải hoàn toàn, nó làm tròn xuống vô cùng, thay vì lên đến 0 (đối với số âm), là cách thực hiện thông thường của phép chia (ít nhất là theo như tôi đã thấy).
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

Bạn có thể sử dụng các thay đổi này để thực hiện bất kỳ hoạt động nhân nào. Ví dụ:

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

Để chia một số cho lũy thừa của hai, tôi không biết có cách nào dễ dàng, trừ khi bạn muốn thực hiện một số logic cấp thấp, hãy sử dụng các phép toán nhị phân khác và sử dụng một số dạng lặp.


@IVlad: Bạn sẽ kết hợp các thao tác trên để thực hiện chia cho 3 như thế nào?
Paul R

@Paul R - đúng, điều đó khó hơn. Tôi đã làm rõ câu trả lời của mình.
IVlad

phép chia cho một hằng số không quá khó (nhân với hằng số ma thuật và sau đó chia cho lũy thừa của 2), nhưng phép chia cho một biến thì khó hơn một chút.
Paul R

1
không nên x * 14 == x * 16 - x * 2 == (x << 4) - (x << 2) thực sự trở thành (x << 4) - (x << 1) vì x < <1 là nhân x với 2?
Alex Spencer

18
  1. Dịch chuyển sang trái 1 vị trí tương tự như nhân với 2. Dịch chuyển sang phải tương tự như chia cho 2.
  2. Bạn có thể thêm vào một vòng lặp để nhân. Bằng cách chọn biến vòng lặp và biến bổ sung một cách chính xác, bạn có thể ràng buộc hiệu suất. Khi bạn đã khám phá ra điều đó, bạn nên sử dụng Peasant Multiplication

9
+1: Nhưng sự dịch chuyển bên trái không chỉ tương tự như nhân với 2. Nó đang nhân với 2. Ít nhất là cho đến khi tràn ...
Don Roby

Phép phân chia mang lại kết quả không chính xác cho các số âm.
David

6

Tôi đã dịch mã Python sang C. Ví dụ được đưa ra có một lỗ hổng nhỏ. Nếu giá trị cổ tức chiếm tất cả 32 bit, quá trình chuyển đổi sẽ không thành công. Tôi chỉ sử dụng các biến 64-bit nội bộ để khắc phục sự cố:

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

Còn số âm thì sao? Tôi đã thử nghiệm -12345 với 10 bằng cách sử dụng eclipse + CDT, nhưng kết quả không tốt như vậy.
kenmux

Bạn có thể vui lòng cho tôi biết tại sao bạn làm ullDivisor >>= 1trước whilevòng lặp không? Ngoài ra, sẽ không nPos >= 0làm thủ thuật?
Vivekanand V

@kenmux Bạn phải chỉ xem xét độ lớn của các số liên quan, trước tiên, thực hiện thuật toán và sau đó sử dụng một số câu lệnh đưa ra quyết định thích hợp, trả về dấu thích hợp cho thương / phần dư!
Vivekanand V

1
@VivekanandV Ý bạn là thêm dấu - sau? Có nó hoạt động.
kenmux

5

Thủ tục chia số nguyên sử dụng dịch chuyển và phép cộng có thể được rút ra theo cách đơn giản từ phép chia số thập phân như được dạy ở trường tiểu học. Việc lựa chọn từng chữ số thương được đơn giản hóa, vì chữ số là 0 và 1: nếu phần dư hiện tại lớn hơn hoặc bằng số chia, bit có nghĩa nhỏ nhất của thương từng phần là 1.

Cũng giống như với phép chia tay dài thập phân, các chữ số của số bị chia được coi là từ quan trọng nhất đến quan trọng nhất, một chữ số tại một thời điểm. Điều này có thể dễ dàng thực hiện bằng cách dịch chuyển sang trái trong phép chia nhị phân. Ngoài ra, các bit thương được tập hợp bằng cách dịch sang trái các bit thương hiện tại theo một vị trí, sau đó nối thêm bit thương mới.

Trong một cách sắp xếp cổ điển, hai sự dịch chuyển trái này được kết hợp thành sự dịch chuyển trái của một cặp thanh ghi. Nửa trên giữ phần còn lại hiện tại, nửa dưới ban đầu giữ cổ tức. Khi các bit cổ tức được chuyển sang thanh ghi phần còn lại bằng cách dịch chuyển sang trái, các bit có ý nghĩa nhỏ nhất không được sử dụng của nửa dưới được sử dụng để tích lũy các bit thương.

Dưới đây là hợp ngữ x86 và các triển khai C của thuật toán này. Biến thể cụ thể này của phép chia shift & cộng đôi khi được gọi là biến thể "không hoạt động", vì phép trừ số chia khỏi phần dư hiện tại không được thực hiện trừ khi phần dư lớn hơn hoặc bằng số chia. Trong C, không có khái niệm cờ thực hiện được sử dụng bởi phiên bản hợp ngữ trong dịch chuyển trái của cặp thanh ghi. Thay vào đó, nó được mô phỏng, dựa trên quan sát rằng kết quả của một modulo cộng 2 n có thể nhỏ hơn mà một trong hai chỉ được bổ sung nếu có thực hiện.

#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 Cảm ơn con trỏ, bạn nói đúng, tôi đã trộn cổ tức với thương số. Tôi sẽ sửa chữa nó.
njuffa

4

Lấy hai số, giả sử là 9 và 10, viết chúng dưới dạng nhị phân - 1001 và 1010.

Bắt đầu với kết quả, R, là 0.

Lấy một trong các số, 1010 trong trường hợp này, chúng ta sẽ gọi nó là A và dịch nó sang phải một bit, nếu bạn chuyển ra một số, hãy thêm số đầu tiên, chúng tôi sẽ gọi nó là B, thành R.

Bây giờ dịch chuyển B sang trái một bit và lặp lại cho đến khi tất cả các bit đã được dịch chuyển ra khỏi A.

Sẽ dễ dàng hơn để xem những gì đang xảy ra nếu bạn thấy nó được viết ra, đây là ví dụ:

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

Điều này có vẻ nhanh nhất, chỉ cần thêm một chút mã hóa để lặp qua các bit của số nhỏ nhất và tính toán kết quả.
Hellonearthis

2

Lấy từ đây .

Điều này chỉ dành cho phân chia:

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

Về cơ bản nó là nhân và chia với lũy thừa cơ bản 2

dịch sang trái = x * 2 ^ y

dịch sang phải = x / 2 ^ y

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

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


eaxkhông thể giữ một giá trị phân số như 1/4. (Trừ khi bạn đang sử dụng điểm cố định thay vì số nguyên, nhưng bạn không chỉ định điều đó)
Peter Cordes

1

Điều này sẽ hoạt động để nhân:

.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

Những gì hương vị của hội?
Keith Pinson

1
Đó là lắp ráp MIPS, nếu đây là những gì bạn đang hỏi. Tôi nghĩ rằng tôi đã sử dụng MARS để viết / chạy nó.
Melsi

1

Phương pháp dưới đây là thực hiện phép chia nhị phân xem xét cả hai số đều dương. Nếu phép trừ là một mối quan tâm, chúng ta có thể thực hiện điều đó bằng cách sử dụng các toán tử nhị phân.

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

Đối với phép nhân:

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

Cú pháp này là gì? -(int)multiplyNumber:(int)num1 withNumber:(int)num2?
SS Anne

0

Đối với bất kỳ ai quan tâm đến giải pháp x86 16-bit, có một đoạn mã của JasonKnight ở đây 1 (anh ấy cũng bao gồm một đoạn nhân có chữ ký, mà tôi chưa thử nghiệm). Tuy nhiên, mã đó có vấn đề với đầu vào lớn, trong đó phần "thêm bx, bx" sẽ tràn.

Phiên bản cố định:

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

Hoặc tương tự trong lắp ráp nội tuyến 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

Thử đi. 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
Nó trông giống như con trăn. Câu hỏi được đặt ra cho sự lắp ráp và / hoặc C.
void
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.