67

我面临一个相当特殊的问题。我正在为不支持按位运算的架构开发编译器。但是,它处理有符号的 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 等

我希望能够支持的按位运算是:

  • ( c = a | b )
  • ( c = a & b )
  • 异或( c = a ^ b )
  • 左移( c = a << b )
  • 右移( c = a >> b )
  • (所有整数都有符号,所以这是一个问题)
  • 有符号移位( c = a >>> b )
  • 一个的补码( a = ~b )
  • (已经找到解决方案,见下文)

通常问题是相反的。如何使用按位 hack 实现算术优化。但是在这种情况下不是。

这种架构上的可写内存非常稀缺,因此需要按位运算。按位函数本身不应使用大量临时变量。但是,常量只读数据和指令内存是丰富的。附带说明一下,跳转和分支并不昂贵,并且所有数据都可以轻松缓存。跳转花费的周期是算术(包括加载/存储)指令的一半。换句话说,所有上述支持的功能都花费了单次跳转周期的两倍。


一些可能有帮助的想法:

我发现您可以使用以下代码进行补码(否定位):

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

对于其余的按位运算,我有点不知所措。我希望这种架构的架构师能够提供位操作。

我还想知道是否有一种快速/简单的方法可以在不使用内存数据表的情况下计算两个的幂(用于移位操作)。一个天真的解决方案是跳入乘法领域:

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

或 Set & Jump 方法:

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

7 回答 7

30

移位的第一个解决方案(移位是移位距离,不能为负数,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 实际上是真的)。

编辑:我实际上测试了所有可能的操作数值(-32768 到 32767),以确保正确性从 0 到 31 的移位,并且它工作正常(假设整数除法)。对于 AND/OR/XOR 代码,在我的机器上进行详尽的测试花费的时间太长,但是由于这些代码非常简单,所以无论如何都不应该出现边缘情况。

于 2010-06-06T03:04:20.520 回答
7

在这种环境下,最好设置为实际使用算术运算符来剥离整数的分量。

例如

if (a & 16)  becomes if ((a % 32) > 15)
a &= 16 becomes if ((a % 32) < 15) a += 16

如果您将 RHS 限制为 2 的恒定幂,这些运算符的变换就足够明显了。

剥掉两个或四个位也很容易。

于 2010-06-06T01:36:44.633 回答
6

一个老问题的不完整答案,这里集中在 AND,OR,XOR。一旦找到这些按位运算之一的解决方案,就可以推导出其他两个。有几种方法,一种显示在下面的测试程序中(在 gcc 版本 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5) 上编译)。

2018 年 12 月,我在解决方案中发现了一个错误。下面评论的 XOR 仅起作用,因为中间结果a+b-2*AND(a,b)被提升为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;
}
于 2015-02-04T22:06:14.207 回答
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;
}

你需要 3 个变量来实现它。

每个按位运算都将围绕类似于的宏进行AND_MACRO- 您将 a 和 b 的剩余值与“掩码”(即“c”参数)进行比较。然后将掩码添加到适合您操作的 if 分支中的结果。如果设置了位,则从值中减去掩码。

根据您的平台,它可能比使用 % 和 / 提取每一位,然后使用乘法将其放回更快。

自己看看哪个更适合你。

于 2010-06-06T02:26:22.243 回答
2

只要你愿意它非常昂贵,是的。

基本上,您将明确地将一个数字放入 base-2 表示中。你这样做就像你将一个数字放入base-10(例如,打印出来)一样,即通过重复除法。

这会将您的数字转换为布尔数组(或 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]  ;
  }
}
于 2010-06-06T03:03:18.147 回答
1

只是其他一些方法

例如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 位与:

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

32 位整数2 位 AND

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

16 位整数2 位与

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

16 位整数3 位与

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);
}
于 2018-01-15T22:11:33.143 回答
0

这是我想出的一种使用双 64 整数加法并行处理按位 XOR 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 位硬限制。

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

对于按位或,它是相反的——不是 0 或 6 的任何东西都是输出字符串中的“1”。

对于逐位补码,更简单——从 1,111,111,111,111,111 开始,减去 2 个字节的串联位串得到它。

于 2021-10-23T22:52:58.893 回答