使用位运算符进行乘法操作

3
我想知道如何使用位运算符来乘以一系列二进制位。但是,我想用它来找到二进制值的十进制小数值。以下是我想要做的示例:
给出一个二进制数1010010,
我想使用每个单独的位,使其计算为:
1*(2^-1) + 0*(2^-2) + 1*(2^-3) + 0*(2^-4).....
虽然我想在ARM汇编中执行此操作,但在C/C++中提供示例也会有所帮助。
我考虑使用循环和计数器,每次循环迭代时,计数器将递增,该值将被逻辑左移,以便获取第一个位,并乘以2 ^ -counter。
但是,我不确定如何仅获取第一个位/ MSB进行乘法,并且我对如何将该值乘以2的负幂感到困惑。
我知道逻辑左移将用2的底数乘以它,但这些通常具有正指数。例如,LSL r0,2表示r0中的值将乘以2 ^ 2 = 4。
谢谢!

这不就是(float)x / (1 << 7)吗? - Oliver Charlesworth
5个回答

3
使用位运算符(AND,OR,XOR,<<,>>)仅通过位运算来计算两个数字的乘积是完全可行的,尽管可能不是非常高效。您可能想阅读相关的维基百科文章Adder (electronics)Binary multiplier
一种自下而上的乘法方法是先创建一个二进制加法器。对于最低位(位0),half adder可以很好地工作。S表示总和,C表示进位。

Half adder

对于剩余的位,每个位都需要一个全加器。Cin表示“进位”,Cout表示“进位输出”:

Full adder

将多个比特位相加的最简单逻辑电路称为串行进位加法器

Ripple-carry adder

Ripple-carry加法器基本上是一系列全加器,进位被传递给计算下一个更高有效位的全加器。虽然存在其他更高效的方法,但由于简单起见,我将跳过它们。现在我们有了一个二进制加法器。
二进制乘法器是一个更难的情况。但是由于我认为这更多是一个概念的证明而不是实际乘两个数字的实用方式,所以让我们走一条更简单的路线。
假设我们想要计算ab的乘积,a = 100b = 5ab都是16位无符号整数(也可以是定点数)。我们可以创建一个求和数组,在其中写入a(100)b(5)次的值,或反之亦然。由于在16位中可表示的最高无符号值为2^16-1(65535),我们希望创建一个大小为65535的无符号整数数组,并填充零。然后,我们需要使用位运算将数组的某5个值设置为100。
我们可以这样做:首先,我们用值为100的a填充一个数组(称其为a_array)。然后,我们想要根据b的值将a_array中的一些值清零,使得a_array的b个值保持不变,其余值都被清零。为此,我们使用一个二进制掩码和AND位运算。
所以我们循环遍历b的比特位。对于b的每一位,我们根据b中该位的值创建一个二进制掩码。创建这样的二进制掩码只需要进行位移(<<,>>)位运算,按位AND和按位OR。
0 -> 0b0000 0000 0000 0000
1 -> 0b1111 1111 1111 1111
所以,现在我们有了一个二进制掩码。但是我们如何使用它呢?嗯,b 的第 0 位对应着数字值 0 或 1。b 的第 1 位对应着数字值 0 或 2。b 的第 2 位对应着数字值 0 或 4。因此,b 的第 n 位对应着数字值 0 或 2^n。所以,当我们遍历 b 的每一位并为每一位创建一个二进制掩码时,我们将 a_array 的 2^n 值与相应的二进制掩码进行 AND 运算。 a_array 中对应的值可能会被清零或不变。在 C 代码中,我使用 for 循环通过递增和递减计数器进行 AND 运算。递增和递减不是位运算。但是 for 循环并不是必需的,它仅用于可读性(从人类角度来看)。实际上,我首先在 x86-64 汇编语言中编写了一个 4 位 * 4 位 = 4 位乘法器来尝试这个概念,只使用 andorxorshl(左移位)和 shr(右移位)以及 callcall 是函数或过程调用,也就是说,不是位运算,但是您可以内联所有这些函数或程序,并使用只有 ANDORXOR<<>> 的方法计算乘积。因此,对于 b 的每一位,您可以使用基于相应的 b 位的位掩码的按位掩码来 AND n(n = 1、2、4、8...)个对应的 a_array 值。对于一个 16 位 * 16 位 = 16 位乘法,需要 65535 条 AND 命令(没有循环)。计算机处理此类输入没有问题,但人们往往无法阅读此类代码。出于这个原因,使用 for 循环。
现在我们有一个由ba值填充而成的a_array数组,其余为零。剩下的很简单:我们只需要使用位加器(即下面C代码中的函数my_add)将a_array的所有值相加。

这是16位* 16位= 16位无符号整数乘法的代码。请注意,函数memset16假设小端架构。将memset16转换为大端架构应该很容易。此代码也适用于定点乘法,您只需要在结尾处添加位移。转换为不同的变量大小以及实现溢出检测也应该很容易。任务留给读者。使用GCC编译,在x86-64 Linux上进行了测试。

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

#define NUMBER_OF_BITS 16
#define MAX_VALUE 65535

typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;

typedef int8_t s8;
typedef int16_t s16;
typedef int32_t s32;

typedef struct result_struct{
    u16 result;
    u16 carry;
} result_struct;

u16 extend_lowest_bit(u16 a)
{
    /* extends lowest bit (bit 0) to all bits. */
    u16 a_extended;
    a = (a & 1);
    a_extended = a | (a << 1) | (a << 2) | (a << 3) | (a << 4);
    a_extended = a_extended | (a << 5) | (a << 6) | (a << 7) | (a << 8);
    a_extended = a_extended | (a << 9) | (a << 10) | (a << 11) | (a << 12);
    a_extended = a_extended | (a << 13) | (a << 14) | (a << 15);
    return a_extended;
}

result_struct my_add(u16 a, u16 b)
{
    /* computes (a + b). */
    result_struct add_results;

    u16 a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15;
    u16 b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15;
    u16 carry, result = 0;
    /* prepare for bitwise addition by separating
     * each bit of summands a and b using bitwise AND. */
    a0 = a & 1;
    a1 = a & (1 << 1);
    a2 = a & (1 << 2);
    a3 = a & (1 << 3);
    a4 = a & (1 << 4);
    a5 = a & (1 << 5);
    a6 = a & (1 << 6);
    a7 = a & (1 << 7);
    a8 = a & (1 << 8);
    a9 = a & (1 << 9);
    a10 = a & (1 << 10);
    a11 = a & (1 << 11);
    a12 = a & (1 << 12);
    a13 = a & (1 << 13);
    a14 = a & (1 << 14);
    a15 = a & (1 << 15);

    b0 = b & 1;
    b1 = b & (1 << 1);
    b2 = b & (1 << 2);
    b3 = b & (1 << 3);
    b4 = b & (1 << 4);
    b5 = b & (1 << 5);
    b6 = b & (1 << 6);
    b7 = b & (1 << 7);
    b8 = b & (1 << 8);
    b9 = b & (1 << 9);
    b10 = b & (1 << 10);
    b11 = b & (1 << 11);
    b12 = b & (1 << 12);
    b13 = b & (1 << 13);
    b14 = b & (1 << 14);
    b15 = b & (1 << 15);

    add_results.result = a0 ^ b0;
    /* result: 0000 0000 0000 000x */
    carry = (a0 & b0) << 1;
    add_results.result = add_results.result | (a1 ^ b1 ^ carry);
    /* result: 0000 0000 0000 00xx */
    carry = ((carry & (a1 ^ b1)) | (a1 & b1)) << 1;
    add_results.result = add_results.result | (a2 ^ b2 ^ carry);
    /* result: 0000 0000 0000 0xxx */
    carry = ((carry & (a2 ^ b2)) | (a2 & b2)) << 1;
    add_results.result = add_results.result | (a3 ^ b3 ^ carry);
    /* result: 0000 0000 0000 xxxx */
    carry = ((carry & (a3 ^ b3)) | (a3 & b3)) << 1;
    add_results.result = add_results.result | (a4 ^ b4 ^ carry);
    /* result: 0000 0000 000x xxxx */
    carry = ((carry & (a4 ^ b4)) | (a4 & b4)) << 1;
    add_results.result = add_results.result | (a5 ^ b5 ^ carry);
    /* result: 0000 0000 00xx xxxx */
    carry = ((carry & (a5 ^ b5)) | (a5 & b5)) << 1;
    add_results.result = add_results.result | (a6 ^ b6 ^ carry);
    /* result: 0000 0000 0xxx xxxx */
    carry = ((carry & (a6 ^ b6)) | (a6 & b6)) << 1;
    add_results.result = add_results.result | (a7 ^ b7 ^ carry);
    /* result: 0000 0000 xxxx xxxx */
    carry = ((carry & (a7 ^ b7)) | (a7 & b7)) << 1;
    add_results.result = add_results.result | (a8 ^ b8 ^ carry);
    /* result: 0000 000x xxxx xxxx */
    carry = ((carry & (a8 ^ b8)) | (a8 & b8)) << 1;
    add_results.result = add_results.result | (a9 ^ b9 ^ carry);
    /* result: 0000 00xx xxxx xxxx */
    carry = ((carry & (a9 ^ b9)) | (a9 & b9)) << 1;
    add_results.result = add_results.result | (a10 ^ b10 ^ carry);
    /* result: 0000 0xxx xxxx xxxx */
    carry = ((carry & (a10 ^ b10)) | (a10 & b10)) << 1;
    add_results.result = add_results.result | (a11 ^ b11 ^ carry);
    /* result: 0000 xxxx xxxx xxxx */
    carry = ((carry & (a11 ^ b11)) | (a11 & b11)) << 1;
    add_results.result = add_results.result | (a12 ^ b12 ^ carry);
    /* result: 000x xxxx xxxx xxxx */
    carry = ((carry & (a12 ^ b12)) | (a12 & b12)) << 1;
    add_results.result = add_results.result | (a13 ^ b13 ^ carry);
    /* result: 00xx xxxx xxxx xxxx */
    carry = ((carry & (a13 ^ b13)) | (a13 & b13)) << 1;
    add_results.result = add_results.result | (a14 ^ b14 ^ carry);
    /* result: 0xxx xxxx xxxx xxxx */
    carry = ((carry & (a14 ^ b14)) | (a14 & b14)) << 1;
    add_results.result = add_results.result | (a15 ^ b15 ^ carry);
    /* result: xxxx xxxx xxxx xxxx */
    add_results.carry = ((carry & (a15 ^ b15)) | (a15 & b15)) << 1;
    return add_results;
}

result_struct add_array(void* array, s32 size)
{
    /* adds together all u16 values of the array. */
    result_struct add_results;
    u16* i;
    u16* top_address;

    add_results.result = 0;
    add_results.carry = 0;

    for (i = array; i < ((u16*)array + size); i++)
    {
        add_results = my_add(add_results.result, *i);
    }
    return add_results;
}

void memset16(void* dest, u16 value, s32 size)
{
    /* does a 16-bit memset. size is the number of u16's (words). */
    u8* i;

    for (i = (u8*)dest; i < ((u8*)dest+(2*size)); i+=2)
    {
        memset(i, (int)(value & 0xff), 1);
        memset(i+1, (int)(value >> 8), 1);
    }  
}

result_struct my_mul(u16 a, u16 b)
{
    /* computes (a * b) */
    u16 bitmask, a_array[MAX_VALUE];
    u32 block_length;
    s16 bit_i;
    s32 count, size;
    u16* i;
    void* p_a_array; 
    p_a_array = a_array;

    result_struct mul_results;
    mul_results.result = 0;

    size = MAX_VALUE;
    memset16(p_a_array, a, size);   // can be replaced with AND.

    /* mask the summands. can be unrolled to
     * use only bitwise operations. */
    i = p_a_array;

    for (bit_i = 0, block_length = 1; bit_i < NUMBER_OF_BITS; bit_i++)
    {
        bitmask = extend_lowest_bit(b >> bit_i);

        for (count = block_length; count > 0; count--)
        {
            *i = (*i & bitmask);
            i++;
        }
        block_length <<= 1;
    }
    /* the array of summands is now masked. */

    /* add the values of the array together. */
    mul_results = add_array(p_a_array, MAX_VALUE);
    return mul_results;
}

int main(void)
{
    int a, b;
    result_struct multiply_results;

    printf("Enter the 1st unsigned 16-bit integer.\n");
    scanf("%d", &a);
    printf("Enter the 2nd unsigned 16-bit integer.\n");
    scanf("%d", &b);
    multiply_results = my_mul((u16)a, (u16)b);
    printf("%d * %d = %d\n", a, b, multiply_results.result);
    return 0;
}

0

仅使用位运算进行乘法(和加法)是一种折磨,因为你将尝试替换的硬件非常复杂。(注意:这里的其他答案使用本机加法;这并不是位运算实现。)

如果你有一个比操作数宽度大两倍的整数类型,那么只需执行正常的乘法并使用右移来将基点对齐到正确的位。如果你将两个范围在 [0, 1) 的8位小数相乘,则结果应该向右移动8位,以将基点从216的位置移回到28的位置。

鉴于long long的普遍存在,此技术可便携地处理32位小数的乘法。

const uint64_t fix_factor
    = static_cast< uint64_t >( std::numeric_limits< uint32_t >::max() ) + 1;

uint32_t a = (7./13) * fix_factor;
uint32_t b = (13./14) * fix_factor;
uint32_t prod = static_cast< uint64_t >( a ) * b / fix_factor;

(使用{{link1:x86-64汇编}}和{{link2:合理的输出}}进行实时演示。)

如果您可以访问汇编语言,无论什么情况下都不需要任何技巧,因为大多数 CPU 都提供了一个乘法高位指令。(这用于实现long long乘法,但使用汇编语言可以确保不计算不必要的低位结果位。)

z80和其他(大多已过时但仍广泛使用的)处理器缺乏硬件乘法功能,必须使用其他操作来实现它。 - Sneftel
@Sneftel 但是它们有硬件加法,所以没有必要仅在按位操作中实现乘法。 - Potatoswatter

0

作为练习,这可能会有帮助:

#include <iostream>
using namespace std;

int main()
{
  unsigned bits = 0x52; // 01010010
  int n = 7; // position of the radix point E.g. 0.1010010
  double result = 0;
  for( int i=0 ; i<n ; ++i )
  {
    result += (bits>>i) & 1;
    result *= 0.5;
  }
  cout << result << endl; // 0.640625
  return 0;
}

一种思考这个问题的方法是

1*(2^-1) + 0*(2^-2) + 1*(2^-3) + 0*(2^-4) + ...

并将其重写为

(1+(0+(1+(0+(0+(1+0/2)/2)/2)/2)/2)/2)/2

正如您所看到的,原始二进制数1010010直接显示在此评估中。


0

听起来你走在了正确的道路上...就这么做吧。

如果我有这些二进制数需要相乘

  abcd
* 1011
=======

其中a、b、c、d只是二进制位,可以是0或1,现在还不重要,因为你很快就会知道原因。

正如你所知道的,1011表示为1*(2^0)+1*(2^1)+0*(2^2)+1*(2^3)。

就像小学数学一样简单,但更简单,因为我们只关心1和0,而不是从9到0。

     abcd
   * 1011
   =======
     abcd  (abcd * 1 * 2^0)
    0000   (abcd * 1 * 2^1)
   abcd    (abcd * 0 * 2^2)
+ abcd     (abcd * 1 * 2^3)
==========

如果你还没有想明白,那么...

     abcd
   * 1011
   =======
     abcd  (abcd << 0)
    00000  (abcd << 1)
   abcd00  (0 << 2)
+ abcd000  (abcd << 3)
==========

然后将这些值相加。

unsigned long long mymult ( unsigned int a, unsigned int b )
{
    unsigned long long ra;
    unsigned int rb;
    ra=0;
    for(rb=0;rb<32;rb++)
    { 
        if(b&(1<<rb)) ra+=(unsigned long long a)<<rb;
    }
}

这在汇编中应该很容易实现。使用一个简单的计算器(使用一个可以进行十六进制运算的计算器),你可以很快地看出0xFF * 0xFF = 0xFE01,如果你继续尝试一些东西,你应该意识到它可能需要比操作数的宽度多两倍的位来保存结果。如果你将两个8位数字相乘,为了处理所有可能的组合,你需要一个16位结果。现在太多的处理器都有乘法,但实际上并不是这样做的,这使得处理器的乘法在我看来有点无用。因此,你可以选择做一个简单的32位=32位*32位,或者你可以尝试像上面那样做一个64位=32位*32位(假设编译器将长和整数的长度解释为我所认为的)。你可能想从32位=32位*32位开始,然后再从那里开始。超过那个范围就会变得非常棘手,这是另一个话题。(这也可以在C示例中轻松建模,相当琐碎,但不如下面的内容琐碎)。

unsigned int mymult ( unsigned int a, unsigned int b )
{
    unsigned int ra;
    unsigned int rb;
    ra=0;
    for(rb=0;rb<32;rb++)
    { 
        if(b&(1<<rb)) ra+=a<<rb;
    }
}

负幂?如果2^4表示向左移动4位,那么2^(-4)就表示向右移动4位,对吧?这样就涵盖了负幂。
就像浮点数一样,你需要任意选择小数点的位置并将其归一化。因此,如果你想要4位小数和28位整数,那么如果你想要将5 * 7相乘,你需要将这些数字调整为5<<4和7<<4,并将它们归一化到你的小数位置。0b1010000和0b1110000相乘得到结果0b1000110000。35(0x23)没有小数部分。在你想象的小数点右侧的第一位是2^-1或1/2,下一位是2^-2或1/4,以此类推。
这就是浮点数的工作原理,但它基本上是以科学记数法的形式进行的,其中大部分数字都在小数点右侧。就像中学或高中的科学记数法数学一样,你必须遵循这些规则来准备你的数字,在简单的数学运算之前和之后。例如,指数必须匹配才能相加,在乘法中不需要,但你需要将指数相加以及乘以数字部分...

请注意,我只谈到了无符号数。有符号数在进行乘法时确实有些不同,这是读者的练习(只需用铅笔和纸进行一些简单的4位数学计算,直到手算结果与计算器相匹配,然后使用移位、加法和其他基本操作实现即可)。 - old_timer
另外请注意,由于您似乎在进行一些浮点运算。浮点数使得所有这些变得更容易,23位乘以23位等于23位。您基本上只需要截掉结果的低位(保留几个进行四舍五入),就不会遇到定点问题了。 - old_timer
在阅读了您的评论之后,一切都变得清晰明了了,dwelch!感谢您的一切! - Andrew T
这些函数不应该返回一些东西吗? - user673679
与问题无关。它们可以被定义为没有返回值。 - old_timer

0

利用位运算进行乘法是完全可行的,而且可以并行执行以提高效率;请查看如何高效地执行三个位向量的加法。

int sum_three(int a, int b, int c)
{
   int sum=a^b^c,c2;
   c=((c&a)|(a&b)|(c&b))<<1;
   while (c) {
     c2=(c&sum)<<1;
     sum^=c;
    c=c2;
   }
   return sum;
}

现在我们有一个可以将三个向量相加的循环,我们可以递归地拆分标准乘法矩阵,或者实现我早期答案中已经提到的CSA树。
     abcdefg
   * hijklmn
   ---------
     abcdefg * n <-- bitwise AND, sum these three vectors with the given
    abcdefg  * m <-- bitwise AND  algorithm to produce vector NMLNMLNMLNML
   abcdefg   * l <-- bitwise AND
      ...

   The first three rows will produce a 9-element vector  0000LMNLMNLMN = a1
   The next three rows will produce a vector             0IJKIJKIJK000 = b1
   And finally h*abcdefgh needs to be added:             HHHHHHH000000 = c1

幸运的是,这些部分和可以通过单个额外调用sum_three(a1,b1,c1);来添加。

比特串乘法器或迭代形式的 CSA 乘法器可以表示为:

 acc_0 = 0;
 cry_0 = 0;
 loop (max 2n) times:
   add_n = vectorY.bit0 AND (vectorX << n);
   vectorY >>= 1;
   cry_i+1 = (cry_i & add_i) | (add_i & acc_i) | (acc_i & cry_i);
   acc_i+1 = acc_i ^ cry_i ^ add_i;

当 cry_i+1 == 0 且 vectorY == 0 时,可以中断循环。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接