能否利用整数算术来实现位运算符?

71
我面临一个相当奇特的问题。我正在为一种不支持位运算的体系结构编写编译器。但是,它处理带符号的16位整数算术,我想知道是否可能仅使用以下内容实现位运算:
- 加法(c = a + b) - 减法(c = a - b) - 除法(c = a / b) - 乘法(c = a * b) - 取模(c = a%b) - 最小值(c = min(a,b)) - 最大值(c = max(a,b)) - 比较(c =(a < b),c =(a == b),c =(a <= b),等等。) - 跳转(goto,for,et.c.)
我要支持的位运算是:
- 或(c = a | b) - 与(c = a&b) - 异或(c = a ^ b) - 左移(c = a << b) - 右移(c = a >> b)(所有整数都是有符号的,因此这是个问题) - 带符号移位(c = a >>> b) - 取反(a = ~b)(已经找到解决方案,请参见下文)
通常问题是如何使用位操作来实现算术优化。 但在这种情况下不是这样。
在这种体系结构上,可写内存非常稀缺,因此需要进行位运算。位运算本身不应使用大量临时变量。 但是,常量只读数据和指令内存非常充足。 这里还有一个副注,跳转和分支不昂贵,并且所有数据都可以缓存。 跳转的成本是算术(包括加载/存储)指令的一半。 换句话说,以上所有受支持的函数的成本是单个跳转的两倍。
一些可能有用的思考:
我发现您可以使用以下代码完成取反(否定位):
// Bitwise one's complement
b = ~a;
// Arithmetic one's complement
b = -1 - a;

我还记得以前的一种通过除以2的幂次来实现位移操作的方法,具体为:

// Bitwise left shift
b = a << 4;
// Arithmetic left shift
b = a * 16; // 2^4 = 16

// Signed right shift
b = a >>> 4;
// Arithmetic right shift
b = a / 16;

对于其他位运算,我有点不太了解。我希望这个架构的设计者能提供位运算。

我还想知道是否有一种快速/简单的方法来计算2的幂次方(用于移位操作),而不使用内存数据表。一个朴素的解决方案是跳入乘法领域:

b = 1;
switch (a)
{
  case 15: b = b * 2;
  case 14: b = b * 2;
  // ... exploting fallthrough (instruction memory is magnitudes larger)
  case 2: b = b * 2;
  case 1: b = b * 2;
}

或者采用"设置和跳转"的方法:

switch (a)
{
  case 15: b = 32768; break;
  case 14: b = 16384; break;
  // ... exploiting the fact that a jump is faster than one additional mul
  //     at the cost of doubling the instruction memory footprint.
  case 2: b = 4; break;
  case 1: b = 2; break;
}

13
纯属好奇,现在的CPU如果没有布尔运算符,怎么能够构建?这是一种十进制机器吗? - Mike Dunlavey
9
这绝对是我最近在 Stack Overflow 上看到的最有趣的问题。 - bcat
3
如果操作成本关系准确,那么这台机器一定非常奇怪——整数除法的速度和乘法相同?我猜测它可能是由离散逻辑构建的,就像 NASA 在早期航天探测器中使用的定制计算机一样。 - Durandal
7
为了满足你的好奇心,也许还要让你失望,这不是NASA的太空探测器之类的东西(如果我说是的话,我就得杀了你)。实际上,这个架构来自一个叫做RoboCom的游戏。这个游戏有一个有趣而简单的想法:你为机器人编写汇编代码,然后尝试超越其他机器人。每个机器人的内存非常少(大约40字节),我想编写一个高级编译器,同时还能提供一个稍微昂贵的位压缩器来挤入更多信息。常量内存和表可以通过包含SET操作数的数据库来模拟。这是一个专门给程序员玩的游戏! - Statement
4
如果这能让你感到安慰,IBM 1620机器不仅没有内置的二进制运算符,甚至不能进行加法运算。必须通过表格查找来完成加法运算。另一方面,由于它是一个十进制机器,因此可以处理任意精度的小数(在商业领域很有用)。 - Mike Dunlavey
显示剩余7条评论
7个回答

31

移位的第一个解决方案(shift是移动距离,必须为正数,a是要移位的操作数,完成后也包含结果)。所有三个移位操作都使用了幂表。

// table used for shift operations
powtab = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, -32768 };

// logical shift left
if (shift > 15) {
     a = 0; // if shifting more than 15 bits to the left, value is always zero
} else {
     a *= powtab[shift];
}

// logical shift right (unsigned)
if (shift > 15) {
    a = 0; // more than 15, becomes zero
} else if (shift > 0) {
    if (a < 0) {
        // deal with the sign bit (15)
        a += -32768;
        a /= powtab[shift];
        a += powtab[15 - shift];
    } else {
        a /= powtab[shift];
    }
}

// arithmetic shift right (signed)
if (shift >= 15) {
    if (a < 0) {
        a = -1;
    } else {
        a = 0;
    }
} else if (shift > 0) {
    if (a < 0) {
        // deal with the sign bit
        a += -32768;
        a /= powtab[shift];
        a -= powtab[15 - shift];
    } else {
        // same as unsigned shift
        a /= powtab[shift];
    }
}

对于AND、OR和XOR,我无法想出一个简单的解决方案,因此我将通过循环遍历每个单独的位来实现。可能有更好的技巧来解决这个问题。伪代码假设a和b是输入操作数,c是结果值,x是循环计数器(每个循环必须运行16次):

// XOR (^)
c = 0;
for (x = 0; x <= 15; ++x) {
    c += c;
    if (a < 0) {
        if (b >= 0) {
            c += 1;
        }
    } else if (b < 0) {
        c += 1;
    }
    a += a;
    b += b;
}

// AND (&)
c = 0;
for (x = 0; x <= 15; ++x) {
    c += c;
    if (a < 0) {
        if (b < 0) {
            c += 1;
        }
    }
    a += a;
    b += b;
}

// OR (|)
c = 0;
for (x = 0; x <= 15; ++x) {
    c += c;
    if (a < 0) {
        c += 1;
    } else if (b < 0) {
        c += 1;
    }
    a += a;
    b += b;
}

假设所有变量都是16位,并且所有操作都被视为有符号的(因此当第15位被设置时a<0实际上是成立的)。

编辑:我实际上测试了从0到31的移位操作中可能的所有操作数值(-32768到32767),并且在正确的情况下它可以正常工作(假设整数除法)。对于AND/OR/XOR代码,详尽测试在我的机器上需要太长时间,但由于这些代码相当简单,因此不应该存在边缘情况。


6
在这种情况下,最好设置使用算术运算符来分离整数的组件。
例如:
if (a & 16)  becomes if ((a % 32) > 15)
a &= 16 becomes if ((a % 32) < 15) a += 16

这些操作员的转换如果把RHS限制为2的常数幂,那么很容易理解。
去掉两位或四位也很容易做到。

6
在旧问题上的一个不完整的答案,这里集中在AND、OR、XOR。一旦找到其中一种位运算的解决方案,其他两种就可以派生出来。有几种方法,其中一种在下面的测试程序中显示(编译于gcc版本4.6.3(Ubuntu/Linaro 4.6.3-1ubuntu5))。
2018年12月,我发现了解决方案中的一个错误。下面注释的XOR仅起作用是因为a+b-2*AND(a,b)中间结果被提升为int,对于所有现代编译器而言,int比16个比特更大。
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

//#define XOR(a,b) (a + b - 2*AND(a,b)) // Error. Intermediate overflow
#define XOR(a,b) (a - AND(a,b) +  b - AND(a,b) )
#define IOR(a,b) XOR(XOR(a,b),AND(a,b)) // Credit to Jan Gray, Gray Research LLC, for IOR
static const uint16_t andlookup[256] = {
#define C4(a,b) ((a)&(b)), ((a)&(b+1)), ((a)&(b+2)), ((a)&(b+3))
#define L(a) C4(a,0), C4(a,4), C4(a,8), C4(a,12)
#define L4(a) L(a), L(a+1), L(a+2), L(a+3)
    L4(0), L4(4), L4(8), L4(12)
#undef C4
#undef L
#undef L4
};

uint16_t AND(uint16_t a, uint16_t b) {
    uint16_t r=0, i;

    for ( i = 0; i < 16; i += 4 ) {
            r = r/16 + andlookup[(a%16)*16+(b%16)]*4096;
            a /= 16;
            b /= 16;
    }
    return r;
}

int main( void ) {
    uint16_t a = 0, b = 0;

    do {
            do {
                    if ( AND(a,b) != (a&b) ) return printf( "AND error\n" );
                    if ( IOR(a,b) != (a|b) ) return printf( "IOR error\n" );
                    if ( XOR(a,b) != (a^b) ) return printf( "XOR error\n" );
            } while ( ++b != 0 );
            if ( (a & 0xff) == 0 )
                    fprintf( stderr, "." );
    } while ( ++a != 0 );
    return 0;
}

你知道这个查找表是如何计算的吗? - Alexandre
1
@Alexandre,有几种可能性。 我最初使用了一个小的辅助程序,但现在我已经改用宏来回答。 - Baard
我们的同仁Durandal提出了一个实现移位操作符的方法,你知道另外一种实现这些操作符的方式吗? - Alexandre

3

您可以按位操作(如Mark Byers所建议的),通过提取每个位,但这会很慢。

或者您可以加快进程并使用2D查找表来存储结果,例如对两个4位操作数进行操作。与对位进行操作相比,您将需要更少的提取操作。

您也可以只使用加法、减法和 >= 操作来完成所有操作。使用宏,可以将每个位运算展开成类似这样的形式:

/*I didn't actually compile/test it, it is just illustration for the idea*/
uint16 and(uint16 a, uint16 b){
    uint16 result = 0;
    #define AND_MACRO(c) \
        if (a >= c){ \ 
            if (b >= c){\
                result += c;\
                b -= c;\
            }\
            a -= c;\
        }\
        else if (b >= c)\
            b -= c;

    AND_MACRO(0x8000)
    AND_MACRO(0x4000)
    AND_MACRO(0x2000)
    AND_MACRO(0x1000)
    AND_MACRO(0x0800)
    AND_MACRO(0x0400)
    AND_MACRO(0x0200)
    AND_MACRO(0x0100)
    AND_MACRO(0x0080)
    AND_MACRO(0x0040)
    AND_MACRO(0x0020)
    AND_MACRO(0x0010)
    AND_MACRO(0x0008)
    AND_MACRO(0x0004)
    AND_MACRO(0x0002)
    AND_MACRO(0x0001)
    #undef AND_MACRO
    return result;
}

您需要三个变量来实现这个操作。
每个位运算都将围绕类似于 AND_MACRO 的宏展开——您将比较 a 和 b 的剩余值与“掩码”(即“c”参数)。然后,在适合您操作的 if 分支中,将掩码添加到结果中。如果设置了比特位,则从值中减去掩码。
根据您的平台,使用此方法可能比使用 % 和 / 提取每个位,然后使用乘法将其放回更快。
请自行判断哪种方法对您更好。

2
只要你愿意花费很多钱,那么是可以的。
基本上,您需要将数字显式放入二进制表示中。这与将数字放入十进制(例如打印出来)一样,通过重复除法完成。
这将使您的数字变成一个布尔数组(或0,1范围内的整数),然后我们添加函数来操作这些数组。
请注意,这比位运算要昂贵得多,并且几乎任何体系结构都将提供位运算符。
在C语言中(当然,在C语言中,您有位运算符,但...)一个实现可能是:
include <limits.h>
const int BITWIDTH = CHAR_BIT;
typedef int[BITWIDTH] bitpattern;

// fill bitpattern with base-2 representation of n
// we used an lsb-first (little-endian) representation
void base2(char n, bitpattern array) {
  for( int i = 0 ; i < BITWIDTH ; ++i ) {
    array[i] = n % 2 ;
    n /= 2 ;
  }
}

void bitand( bitpattern op1, bitpattern op2, bitpattern result ) {
  for( int i = 0 ; i < BITWIDTH ; ++i ) {
    result[i] = op1[i] * op2[i];
  }
}


void bitor( bitpattern op1, bitpattern op2, bitpattern result ) {
  for( int i = 0 ; i < BITWIDTH ; ++i ) {
    result[i] = (op1[i] + op2[i] != 0 );
  }
}

// assumes compiler-supplied bool to int conversion 
void bitxor( bitpattern op1, bitpattern op2, bitpattern result ) {
  for( int i = 0 ; i < BITWIDTH ; ++i ) {
    result[i] = op1[i] != op2[i]  ;
  }
}

2

其他一些方法

例如,一个16位AND操作:

int and(int a, int b) {
    int d=0x8000;
    int result=0;
    while (d>0)  {
        if (a>=d && b>=d) result+=d;
        if (a>=d) a-=d;
        if (b>=d) b-=d;
        d/=2;
    }
    return result;
}

不使用循环或表格查找的双精度解决2位 AND:

int and(int a, int b) {
    double x=a*b/12;
    return (int) (4*(sign(ceil(tan(50*x)))/6+x));
}

32位整数 解决方案 2位与运算:

int and(int a, int b) {
    return ((684720128*a*a -b) * a) % (b+1);
}

16位整数解决方案2位AND:

int and(int a, int b) {
    return ((121 * a) % 16) % (b+1);
}

16位整数解决方案3位AND

int and(int a, int b) {
    return sign(a) * ((((-23-a) * (40+b)) % 2)+40+b) % ((10624 * ((((-23-a) * (40+b))%2)+40+b)) % (a%2 - 2 -a) - a%2 + 2 +a);
}

0

这是我想出来的一种方法,使用双64位整数加法并行处理16位按位异或:

[gmn]awk '{ CONVFMT = OFMT = "%.20g" 

     c = (a=3e15+("1011000111110101"))+
         (b=3e15+("1101010010101110"))             
           
         sub(/[7]/,   "1",c)
        gsub(/[268]/ ,"0",c)
         sub(/^[^01]+/,"",c); print c }'

这些二进制字符串看起来像这样(为了清晰起见,我去掉了3e15的保护数字):
 a =    1011 0001 1111 0101
 b =    1101 0100 1010 1110
 c =    8112 0101 2121 1211 (intermediate)
-------------------------------------------
 c =    0110 0101 0101 1011 (output)

只需一个52位无符号整数加法和少量的字符串替换调用,输出就已经处于可以传递给下游的状态。

这个加法的绝对最高值将达到8222,2222,222,222,略低于53位硬限制。

对于按位与运算,将所有的1,前导6或7转换为0:只有2和前导8是真正的位应该转换为1。

对于按位或运算,反之亦然 - 任何不是0或6的东西在输出字符串中都是“1”。

对于按位补码,更容易 - 从1,111,111,111,111,111开始,减去2字节的连接位字符串即可得到结果。


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