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、et.c.

サポートできるようにしたいビット演算は次のとおりです。

  • またはc = a | b
  • そしてc = a&b
  • Xorc = a ^ b
  • 左シフトc = a << b
  • 右シフトc = a >> b
  • (すべての整数は符号付きなので、これは問題です)
  • 符号付きシフトc = a >>> b
  • 1の補数a = 〜b )
  • (すでに解決策が見つかりました。以下を参照してください)

通常、問題はその逆です。ビット単位のハックを使用して算術最適化を実現する方法。ただし、この場合はそうではありません。

このアーキテクチャでは書き込み可能なメモリが非常に少ないため、ビット単位の演算が必要です。ビット単位の関数自体は、多くの一時変数を使用するべきではありません。ただし、一定の読み取り専用データと命令メモリは豊富です。ここでの注意点は、ジャンプとブランチは高価ではなく、すべてのデータが簡単にキャッシュされることです。ジャンプのコストは、算術(ロード/ストアを含む)命令の半分のサイクルです。つまり、上記でサポートされているすべての関数は、1回のジャンプの2倍のサイクルのコストがかかります。


役立つかもしれないいくつかの考え:

次のコードで1の補数(ビットを否定)を実行できることがわかりました。

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

7 に答える 7

30

シフトの最初の解決策(シフトはシフト距離であり、負であってはなりません。aはシフトされるオペランドであり、実行時の結果も含まれます)。パワーテーブルは、3つのシフト操作すべてで使用されます。

// 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 の累乗に制限する場合に十分明らかです。

2~4ビットの剥がしも簡単です。

于 2010-06-06T01:36:44.633 に答える
6

AND、OR、XORに集中する古い質問に対する不完全な回答。これらのビット演算の 1 つの解が見つかったら、残りの 2 つを導き出すことができます。いくつかの方法がありますが、その 1 つを次のテスト プログラム (gcc バージョン 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5) でコンパイル) に示します。

2018 年 12 月に、ソリューションのエラーを発見しました。以下にコメントされている XOR は、最新のすべてのコンパイラで 16 ビットを超える の中間結果a+b-2*AND(a,b)が に昇格されるためにのみ機能します。int

#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が提案したように)ビットごとに操作できます。

または、プロセスを高速化し、たとえば 2 つの 4 ビット オペランドの結果を格納する 2D ルックアップ テーブルを使用して、それらを操作することもできます。ビットを操作する場合よりも、必要な抽出が少なくて済みます。

足し算、引き算、>= 演算を使用してすべてを行うこともできます。すべてのビット操作は、マクロを使用して次のように展開できます。

/*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 つの変数が必要です。

すべてのビット単位の操作は、次のようなマクロを中心に展開します。aAND_MACROと b の残りの値を「マスク」(「c」パラメーター) と比較します。次に、操作に適した if ブランチの結果にマスクを追加します。ビットが設定されている場合は、値からマスクを減算します。

プラットフォームによっては、 % と / を使用してすべてのビットを抽出し、乗算を使用して元に戻すよりも高速な場合があります。

どちらが良いかは、ご自分の目でお確かめください。

于 2010-06-06T02:26:22.243 に答える
2

あなたがそれが非常に高価であることをいとわない限り、そうです。

基本的に、2を底とする表現に明示的に数値を入れます。これは、数値を基数10に入れるのと同じように(たとえば、印刷するために)、つまり、除算を繰り返すことによって行います。

これにより、数値がboolの配列(または0,1の範囲のint)に変換され、それらの配列を操作する関数が追加されます。

繰り返しになりますが、これはビット単位の演算よりも非常に高価であり、ほとんどすべてのアーキテクチャがビット単位の演算子を提供するというわけではありません。

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 に答える