Як я можу множити і ділити, використовуючи лише зміщення та додавання бітів?


86

Як я можу множити і ділити, використовуючи лише зміщення та додавання бітів?


20
Як би ви робили це на папері в середній школі, використовуючи лише двійковий, а не десятковий.
Паскаль Куок,

1
@mtk: Чого не вистачає у цій відповіді ? Ви шукаєте реалізацію C або збірки, конкретні ширини операндів, конкретний метод поділу (наприклад, відновлення проти не відновлення)?
njuffa

Чи віднімання в порядку? Здається, все накрито
mksteve

У чому полягає необхідність цього питання? Процесори перекладають операції множення та ділення на бітове зміщення та додавання або віднімання, і якщо це так, якщо компілятор цього ще не зробив.
Kelly S. French

@ KellyS.French Просто цікавість, це більше спосіб уявити, як компілятор може працювати з обмеженим набором інструкцій.
Spidfire

Відповіді:


77

Щоб помножити з точки зору додавання та зсуву, ви хочете розкласти одне з чисел на степені двох, наприклад:

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)

(_2 означає базу 2)

Як бачите, множення можна розкласти на додавання та переміщення та назад. Ось чому множення займає більше часу, ніж зсув бітів або додавання - це O (n ^ 2), а не O (n) у кількості бітів. Реальні комп’ютерні системи (на відміну від теоретичних комп’ютерних систем) мають кінцеву кількість бітів, тому множення займає постійне число, кратне часу порівняно з додаванням і зсувом. Якщо я правильно пам'ятаю, сучасні процесори, якщо їх конвеєр правильно виконаний, можуть виконувати множення приблизно так само швидко, як додавання, возившись із використанням ALU (арифметичних одиниць) у процесорі.


4
Я знаю, що це було деякий час тому, але чи можете ви навести приклад з поділом? Дякую
GniruT

42

Відповідь Ендрю Тулузи можна поширити на поділ.

Поділ на цілі константи детально розглянуто в книзі "Захоплення Хакера" Генрі С. Уоррена (ISBN 9780201914658).

Перша ідея реалізації ділення полягає в тому, щоб записати обернене значення знаменника в базі два.

Наприклад, 1/3 = (base-2) 0.0101 0101 0101 0101 0101 0101 0101 0101 .....

Тому, a/3 = (a >> 2) + (a >> 4) + (a >> 6) + ... + (a >> 30) для 32-розрядної арифметики.

Комбінуючи терміни очевидним чином, ми можемо зменшити кількість операцій:

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

b += (b >> 4)

b += (b >> 8)

b += (b >> 16)

Є більш захоплюючі способи обчислення ділення та залишків.

EDIT1:

Якщо OP означає множення та ділення довільних чисел, а не ділення на постійне число, тоді цей потік може бути корисним: https://stackoverflow.com/a/12699549/1182653

EDIT2:

Одним із найшвидших способів поділу на цілі константи є використання модульної арифметики та редукції Монтгомері: Який найшвидший спосіб розділити ціле число на 3?


Велике спасибі за посилання Hacker's Delight!
alecxe

2
Так, ця відповідь (ділення на константу) є лише частково правильною. Якщо ви спробуєте зробити «3/3», то в кінцевому результаті ви отримаєте 0. У Hacker's Delight вони насправді пояснюють, що є помилка, яку ви повинні компенсувати. У цьому випадку: b += r * 11 >> 5с r = a - q * 3. Посилання: hackersdelight.org/divcMore.pdf сторінка 2+.
атласт

30

X * 2 = 1 біт зсув вліво
X / 2 = 1 біт зсув вправо
X * 3 = зсув вліво 1 біт, а потім додайте X


4
Ти маєш на увазі add Xтой останній?
Марк Байерс

1
Це все ще неправильно - останній рядок повинен читати: "X * 3 = зсунути ліворуч на 1 біт, а потім додати X"
Paul R

1
"X / 2 = 1 бітний зсув вправо", не повністю, він округлюється до нескінченності, а не до 0 (для від'ємних чисел), що є звичайною реалізацією ділення (принаймні, наскільки я бачив).
Лейф Андерсен

25

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

Ви можете використовувати ці зрушення для будь-якої операції множення. Наприклад:

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

Щоб розділити число на нерівневе з двох, я не знаю жодного простого способу, якщо ви не хочете реалізувати якусь логіку низького рівня, використовувати інші двійкові операції та використовувати якусь форму ітерації.


@IVlad: Як би ви поєднали вищезазначені операції, щоб виконати, скажімо, ділити на 3?
Paul R,

@Paul R - правда, це важче. Я пояснив свою відповідь.
IVlad

ділення на константу не надто важке (помножте на магічну константу, а потім розділіть на потужність 2), але ділення на змінну трохи складніше.
Paul R

1
не повинно x * 14 == x * 16 - x * 2 == (x << 4) - (x << 2) насправді бути (x << 4) - (x << 1), оскільки x < <1 множиться на х на 2?
Алекс Спенсер

18
  1. Зсув ліворуч на 1 положення аналогічний множенню на 2. Правий зсув аналогічний діленню на 2.
  2. Ви можете додати в циклі множення. Правильно підібравши змінну циклу та змінну додавання, ви можете обмежити продуктивність. Після того, як ви це вивчите, вам слід скористатися селянським множенням

9
+1: Але лівий зсув не тільки аналогічний множення на 2. Це НЕ множачи на 2. Принаймні , до переливу ...
Дон Робі

Поділ на зсув дає неправильні результати для від’ємних чисел.
Девід

6

Я переклав код Python на C. Наведений приклад мав незначний недолік. Якби значення дивіденду, яке зайняло всі 32 біти, зсув провалився. Я просто використовував 64-розрядні змінні внутрішньо, щоб вирішити проблему:

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

А як щодо від’ємного числа? Я протестував -12345 з 10 за допомогою eclipse + CDT, але результат був не такий хороший.
kenmux

Підкажіть, будь ласка, чому ви це робите ullDivisor >>= 1до whileциклу? Крім того, не nPos >= 0зробить фокус?
Вівекананд V

@kenmux Ви повинні враховувати лише величину задіяних чисел, спочатку виконайте алгоритм, а потім, використовуючи відповідні твердження для прийняття рішень, поверніть належний знак до частки / залишку!
Vivekanand V

1
@VivekanandV Ви маєте на увазі додати знак - пізніше? Так, це працює.
kenmux

5

Процедура ділення цілих чисел, яка використовує зсуви та додавання, може бути отримана прямо з десяткового довгострокового ділення, як її викладають у початковій школі. Вибір кожної цифрової частки спрощується, оскільки цифра дорівнює 0 або 1: якщо поточний залишок більший або дорівнює дільнику, найменш значущим бітом часткового часткового значення є 1.

Подібно до десяткового довготермінового поділу, цифри дивіденду вважаються від найбільш значущих до найменш значущих, по одній цифрі. Це легко досягається зсувом лівого бінарного ділення. Крім того, часткові біти збираються, зміщуючи поточні часткові біти вліво на одну позицію, потім додаючи новий частковий біт.

У класичному розташуванні ці два ліві зсуви поєднуються в ліві зсуви однієї пари регістрів. У верхній половині міститься поточний залишок, у нижній половині - початковий дивіденд. Оскільки біти дивідендів переносяться в регістр залишків за допомогою лівого зсуву, невикористані найменш значущі біти нижньої половини використовуються для накопичення часткових бітів.

Нижче наведено асемблерну мову x86 та реалізації цього алгоритму C. Цей конкретний варіант поділу зсуву та додавання іноді називають варіантом "без виконання", оскільки віднімання дільника від поточного залишку не виконується, якщо залишок не більший або дорівнює дільнику. У C немає поняття прапора переносу, що використовується версією збірки, в лівій зсуві пари регістрів. Натомість його імітують, виходячи із спостереження, що результат додавання за модулем 2 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 Дякую за вказівник, ви праві, я змішав дивіденд із часткою. Я це виправлю.
нюффа

4

Візьміть два числа, скажімо, 9 і 10, запишіть їх як двійкові - 1001 і 1010.

Почніть з результату, R, 0.

Візьміть одне з чисел, у цьому випадку 1010, ми будемо називати його A, і зміщуємо вправо на один біт, якщо ви зміщуєте одиницю, додайте перше число, ми будемо називати B, до R.

Тепер змістіть B ліворуч на один біт і повторюйте, поки всі біти не будуть зміщені з A.

Це легше зрозуміти, що відбувається, якщо ви бачите, як це виписано, ось приклад:

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

Це здається найшвидшим, просто потрібно трохи додаткового кодування, щоб прокрутити біти найменшого числа та обчислити результат.
Hellonearthis

2

Взято звідси .

Це лише для поділу:

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

це в основному множення і ділення з базовою потужністю 2

зміщення вліво = x * 2 ^ y

зміщення вправо = x / 2 ^ y

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

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


eaxне може утримувати дробове значення типу 1/4. (Якщо ви не використовуєте фіксовану точку замість цілого, але ви цього не вказали)
Пітер Кордес,

1

Це має працювати для множення:

.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

Який смак складання?
Кіт Пінсон,

1
Це збірка MIPS, якщо ви про це питаєте. Думаю, я використовував MARS для написання / запуску.
Мельсі,

1

Наведений нижче метод - це реалізація двійкового розділення, враховуючи, що обидва числа є позитивними. Якщо віднімання викликає занепокоєння, ми можемо реалізувати це, використовуючи також двійкові оператори.

Код

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

Для множення:

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

Що це за синтаксис? -(int)multiplyNumber:(int)num1 withNumber:(int)num2?
SS Anne

0

Для всіх, хто цікавиться 16-розрядним рішенням x86, є фрагмент коду JasonKnight тут 1 (він також включає підписаний множник , який я не тестував). Однак цей код має проблеми з великими входами, де частина "додати bx, bx" переповнюється.

Виправлена ​​версія:

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

Або те саме в рядковій збірці 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

Спробуйте це. 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
Це схоже на python. Запитання було задано для зборів та / або С.
недійсне
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.