【问题标题】:How can I multiply and divide using only bit shifting and adding?如何仅使用位移和加法进行乘法和除法?
【发布时间】:2011-02-16 02:24:44
【问题描述】:

如何仅使用位移和加法进行乘法和除法?

【问题讨论】:

  • 就像你在中学纸上做的那样,只使用二进制而不是十进制。
  • @mtk:this answer 缺少什么?您是否在寻找 C 或汇编实现、特定的操作数宽度、特定的除法方法(例如恢复与非恢复)?
  • 减法可以吗?一切似乎都被覆盖了
  • 这个问题背后的需求是什么? CPU 已经将乘法和除法运算转换为位移位和加法或减法,如果编译器还没有这样做的话。
  • @KellyS.French 只是好奇,它更像是一种想象编译器如何使用受限指令集的方式。

标签: c assembly bit-manipulation division multiplication


【解决方案1】:

使用移位和加法来除法整数的过程可以直接从小学教的十进制手写除法中推导出来。每个商位的选择被简化,因为该位是 0 和 1:如果当前余数大于或等于除数,则部分商的最低有效位为 1。

就像十进制的普通除法一样,被除数的位数从最高位到最低位,一次一位。这很容易通过二进制除法的左移来实现。此外,通过将当前商位左移一位,然后附加新的商位来收集商位。

在经典排列中,这两个左移组合为一个寄存器对的左移。上半部分持有当前余额,下半部分初始持有股息。由于被除数位通过左移传送到余数寄存器,因此下半部分未使用的最低有效位用于累加商位。

以下是该算法的 x86 汇编语言和 C 实现。这种特殊的移位和加法变体有时被称为“非执行”变体,因为除非余数大于或等于除数,否则不会执行从当前余数中减去除数的操作(Otto Spaniol, “计算机算术:逻辑与设计。”奇切斯特:威利 1981 年,第 144 页)。在 C 语言中,在寄存器对左移中没有汇编版本使用的进位标志的概念。相反,它是模拟的,基于这样的观察结果,即模 2n 的加法结果可以小于只有在有进位时任何一个加数的结果。

#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

【讨论】:

    【解决方案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 这样的小数值。 (除非您使用定点而不是整数,但您没有指定)
    【解决方案3】:

    取自here

    这仅用于除法:

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

    【讨论】:

      【解决方案4】:

      下面的方法是考虑到两个数字都是正数的二进制除法的实现。如果减法是一个问题,我们也可以使用二元运算符来实现。

      代码

      -(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?
      【解决方案5】:

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

      【讨论】:

      • 负数呢?我用 eclipse + CDT 测试了 -12345 和 10,但结果不是那么好。
      • 你能告诉我你为什么在while循环之前做ullDivisor &gt;&gt;= 1吗?另外,nPos &gt;= 0 不会成功吗?
      • @kenmux 您只需要考虑所涉及数字的大小,首先,执行算法,然后使用一些适当的决策语句,将正确的符号返回商/余数!
      • @VivekanandV 你的意思是添加标志 - 稍后?是的,它有效。
      【解决方案6】:

      对于任何对 16 位 x86 解决方案感兴趣的人,JasonKnight 在这里1 有一段代码(他还包括一个带符号的乘法段,我没有测试过)。但是,该代码在输入较大时存在问题,“add 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"
      );
      

      【讨论】:

        【解决方案7】:

        The answer by Andrew Toulouse可以扩展为除法。

        Henry S. Warren 的“Hacker's Delight”一书(ISBN 9780201914658)详细讨论了整数常量的除法。

        实现除法的第一个想法是将分母的倒数写为底数。

        例如, 1/3 = (base-2) 0.0101 0101 0101 0101 0101 0101 0101 0101 .....

        所以, a/3 = (a &gt;&gt; 2) + (a &gt;&gt; 4) + (a &gt;&gt; 6) + ... + (a &gt;&gt; 30) 用于 32 位算术。

        通过以一种明显的方式组合这些术语,我们可以减少操作的数量:

        b = (a &gt;&gt; 2) + (a &gt;&gt; 4)

        b += (b &gt;&gt; 4)

        b += (b &gt;&gt; 8)

        b += (b &gt;&gt; 16)

        还有更多令人兴奋的方法来计算除法和余数。

        编辑1:

        如果 OP 表示任意数字的乘法和除法,而不是除以常数,那么这个线程可能有用:https://stackoverflow.com/a/12699549/1182653

        EDIT2:

        除以整数常量的最快方法之一是利用模运算和蒙哥马利约简:What's the fastest way to divide an integer by 3?

        【讨论】:

        • 非常感谢 Hacker's Delight 参考!
        • 嗯,是的,这个答案(除以常数)只是部分正确。如果您尝试执行“3/3”,您最终会得到 0。在 Hacker's Delight 中,他们实际上解释说存在您必须补偿的错误。在这种情况下:b += r * 11 &gt;&gt; 5r = a - q * 3。链接:hackersdelight.org/divcMore.pdf page 2+。
        【解决方案8】:

        x &lt;&lt; k == x multiplied by 2 to the power of k
        x &gt;&gt; k == x divided by 2 to the power of k

        您可以使用这些移位来执行任何乘法运算。例如:

        x * 14 == x * 16 - x * 2 == (x &lt;&lt; 4) - (x &lt;&lt; 1)
        x * 12 == x * 8 + x * 4 == (x &lt;&lt; 3) + (x &lt;&lt; 2)

        要将一个数字除以非 2 的幂,我不知道有什么简单的方法,除非您想实现一些低级逻辑,使用其他二进制运算并使用某种形式的迭代。

        【讨论】:

        • @IVlad:你将如何结合上述操作来执行,比如除以 3 ?
        • @Paul R - 是的,这更难。我已经澄清了我的答案。
        • 除以常数不是太难(乘以魔法常数,然后除以 2 的幂),但除以变量有点棘手。
        • 不应该 x * 14 == x * 16 - x * 2 == (x
        【解决方案9】:

        试试这个。 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])
        

        【讨论】:

        • 这看起来像 python。该问题是针对汇编和/或 C 提出的。
        【解决方案10】:

        这应该适用于乘法:

        .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
        

        【讨论】:

        • 这是 MIPS 程序集,如果这是您要的。我想我用 MARS 来编写/运行它。
        【解决方案11】:

        X * 2 = 1 位左移
        X / 2 = 1 位右移
        X * 3 = 左移 1 位,然后加上 X

        【讨论】:

        • 最后一个是add X吗?
        • 还是错了——最后一行应该是:“X * 3 = shift left 1 bit and then add X”
        • "X / 2 = 1 bit shift right",不完全是,它向下舍入到无穷大,而不是向上舍入到 0(对于负数),这是除法的通常实现(至少作为据我所知)。
        【解决方案12】:

        要在加法和移位方面相乘,您需要将其中一个数字分解为 2 的幂,如下所示:

        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(算术单元)的利用率来进行乘法运算和加法运算一样快。

        【讨论】:

        • 我知道那是很久以前的事了,但你能举个除法的例子吗?谢谢
        【解决方案13】:

        取两个数字,比如 9 和 10,将它们写成二进制 - 1001 和 1010。

        从结果 R 开始,为 0。

        取其中一个数字,在这种情况下为 1010,我们将其称为 A,然后将其右移一位,如果移出一个,将第一个数字,我们将其称为 B,添加到 R .

        现在将 B 左移一位并重复,直到所有位都移出 A。

        如果你看到它写出来更容易看到发生了什么,这就是例子:

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

        【讨论】:

        • 这似乎是最快的,只需要一点额外的编码来循环遍历最小数字的位并计算结果。
        【解决方案14】:
        1. 左移 1 个位置类似于乘以 2。右移类似于除以 2。
        2. 您可以在循环中添加乘法。通过正确选择循环变量和加法变量,您可以约束性能。一旦你探索了这一点,你应该使用Peasant Multiplication

        【讨论】:

        • +1:但左移不只是类似于乘以 2。它乘以 2。至少直到溢出...
        • Shift-division 对负数产生不正确的结果。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-08-05
        • 1970-01-01
        • 1970-01-01
        • 2018-10-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多