ビットごとのシフト(ビットシフト)演算子とは何ですか?それらはどのように機能しますか?



What Are Bitwise Shift Operators



解決:

ビットシフト演算子は、その名前が示すとおりに実行します。それらはビットをシフトします。これは、さまざまなシフト演算子の簡単な(またはそれほど簡単ではない)紹介です。

オペレーター

  • >>は算術(または符号付き)右シフト演算子です。
  • >>>は論理(または符号なし)右シフト演算子です。
  • << is the left shift operator, and meets the needs of both logical and arithmetic shifts.

これらの演算子はすべて整数値に適用できます(int、長い、おそらく短くてバイトまたはchar)。一部の言語では、シフト演算子をより小さいデータ型に適用しますintは、オペランドのサイズを自動的に変更して、int。



ご了承ください<<< is not an operator, because it would be redundant.

また、注意してください CとC ++は右シフト演算子を区別しません 。彼らはのみを提供します>>演算子、および右シフト動作は、符号付きタイプに対して定義された実装です。残りの回答では、C#/ Java演算子を使用しています。



(GCCおよびClang / LLVMを含むすべての主流のCおよびC ++実装では、>>符号付きタイプは算術です。一部のコードはこれを想定していますが、標準で保証されているものではありません。そうではありません 未定義 、 けれど;この規格では、何らかの方法でそれを定義するための実装が必要です。ただし、負の符号付き数値の左シフト 未定義動作(符号付き整数オーバーフロー)。したがって、算術的な右シフトが必要でない限り、通常、符号なしタイプでビットシフトを行うことをお勧めします。)


左方移動 (<<)

整数は、一連のビットとしてメモリに格納されます。たとえば、32ビットとして格納されている数値6intは次のようになります:

00000000 00000000 00000000 00000110

このビットパターンを左に1つシフトします(6<< 1) would result in the number 12:



00000000 00000000 00000000 00001100

ご覧のとおり、数字は1桁左にシフトしており、右側の最後の数字はゼロで埋められています。また、左にシフトすることは、2の累乗を掛けることと同等であることに気付くかもしれません。6<< 1 is equivalent to 6 * 2、および6<< 3 is equivalent to 6 * 8.優れた最適化コンパイラは、可能な場合、乗算をシフトに置き換えます。

非円形シフト

これらは いいえ 循環シフト。この値を1つ左にシフトします(3,758,096,384<< 1):

11100000 00000000 00000000 00000000

結果は3,221,225,472になります。

11000000 00000000 00000000 00000000

'端から'シフトされた数字は失われます。ラップアラウンドしません。


論理右シフト(>>>)

論理的な右シフトは、左シフトの逆です。ビットを左に移動するのではなく、単に右に移動します。たとえば、番号12をシフトします。

00000000 00000000 00000000 00001100

右に1つの位置(12 >>> 1)元の6を取り戻します:

00000000 00000000 00000000 00000110

したがって、右にシフトすることは、2の累乗で除算することと同等であることがわかります。

失われたビットはなくなりました

ただし、シフトは「失われた」ビットを取り戻すことはできません。たとえば、このパターンをシフトすると、次のようになります。

00111000 00000000 00000000 00000110

左に4つの位置(939,524,102<< 4), we get 2,147,483,744:

10000000 00000000 00000000 01100000

その後、シフトバックします((939,524,102<>> 4)134,217,734を取得します:

00001000 00000000 00000000 00000110

ビットを失うと、元の値に戻すことはできません。


算術右シフト(>>)

算術右シフトは論理右シフトとまったく同じですが、ゼロで埋めるのではなく、最上位ビットで埋められる点が異なります。これは、最上位ビットが サイン ビット、または正と負の数を区別するビット。最上位ビットでパディングすることにより、算術右シフトは符号を保持します。

たとえば、このビットパターンを負の数として解釈すると、次のようになります。

10000000 00000000 00000000 01100000

番号は-2,147,483,552です。これを算術シフト(-2,147,483,552 >> 4)で右に4位置シフトすると、次のようになります。

11111000 00000000 00000000 00000110

または番号-134,217,722。

したがって、論理的な右シフトではなく、算術的な右シフトを使用して、負の数の符号を保持していることがわかります。また、2の累乗で除算を実行していることがわかります。


1バイトあるとしましょう:

0110110

左ビットシフトを1つ適用すると、次のようになります。

1101100

左端のゼロがバイトからシフトアウトされ、新しいゼロがバイトの右端に追加されました。

ビットはロールオーバーしません。それらは破棄されます。つまり、1101100を左シフトしてから右シフトすると、同じ結果が返されません。

左にNをシフトすることは、2を掛けることと同じです。NS

Nで右にシフトすると(1の補数を使用している場合)は2で割るのと同じです。NSゼロに丸めます。

2の累乗で作業している場合、ビットシフトはめちゃくちゃ速い乗算と除算に使用できます。ほとんどすべての低レベルのグラフィックルーチンはビットシフトを使用します。

たとえば、昔はゲームにモード13h(320x200 256色)を使用していました。モード13hでは、ビデオメモリはピクセルごとに順番に配置されていました。これは、ピクセルの位置を計算することを意味し、次の計算を使用します。

memoryOffset =(行* 320)+列

さて、当時は速度が重要だったので、ビットシフトを使用してこの操作を行いました。

ただし、320は2の累乗ではないため、これを回避するには、2の累乗を合計すると320になります。

(行* 320)=(行* 256)+(行* 64)

これで、それを左シフトに変換できます。

(行* 320)=(行<< 8) + (row << 6)  

最終結果は次のとおりです。

memoryOffset =((行<< 8) + (row << 6)) + column  

これで、以前と同じオフセットが得られますが、高価な乗算演算の代わりに2つのビットシフトを使用します... x86では次のようになります(注:アセンブリを実行してからずっとずっと続いています(編集者注:修正済み)いくつかの間違いと32ビットの例を追加しました)):

mov axe、320; 2サイクルマルチワード[行]; 22CPUサイクルmovdi、ax; 2サイクルでdi、[列]を追加します。 2サイクル; di = [行] * 320 + [列]; 16ビットアドレッシングモードの制限:; [di]は有効なアドレッシングモードですが、[ax]はそうではありません。そうでない場合は、最後の移動をスキップできます。

合計:これらのタイミングがあった古いCPUで28サイクル。

Vrs

mov axe、[行]; 2サイクルmovdi、ax; 2 shl ax、6; 2 shl di、8; 2アドイン、斧; 2(320 = 256 + 64)add di、[列]; 2; di = [行] *(256 + 64)+ [列]

同じ古いCPUで12サイクル。

はい、16CPUサイクルを削減するためにこれを一生懸命働きます。

32ビットモードまたは64ビットモードでは、どちらのバージョンもはるかに短く高速になります。 Intel Skylake(http://agner.org/optimize/を参照)のような最新のアウトオブオーダー実行CPUは、非常に高速なハードウェア乗算(低遅延と高スループット)を備えているため、ゲインははるかに小さくなります。 AMD Bulldozerファミリは、特に64ビット乗算の場合は少し遅くなります。 IntelCPUおよびAMDRyzenでは、2つのシフトはレイテンシーがわずかに低くなりますが、乗算よりも命令が多くなります(スループットが低下する可能性があります)。

imul edi、[行]、320; [行]の準備ができてから3サイクルのレイテンシーaddedi、[column]; 1サイクルのレイテンシー([列]とediの準備ができていることから)。 ; edi = [row] *(256 + 64)+ [column]、[row]の準備ができてから4サイクル。

対。

mov edi、[行] shl edi、6;行* 64。 1サイクルのレイテンシーleaedi、[edi + edi * 4];行*(64 + 64 * 4)。 1サイクルのレイテンシーaddedi、[列]; ediと[column]の両方の準備ができてからの1サイクルのレイテンシー。 edi = [row] *(256 + 64)+ [column]、[row]の準備ができてから3サイクル。

コンパイラーがこれを行います。GCC、Clang、およびMicrosoft Visual C ++がすべて最適化時にshift + leaをどのように使用するかを確認してください。320 * row + col;を返します。

ここで注意すべき最も興味深い点は、x86にはシフトアンドアド命令があることです(LEA)小さな左シフトを行うと同時に追加することができ、パフォーマンスは命令を追加します。 ARMはさらに強力です。任意の命令の1つのオペランドを、無料で左または右にシフトできます。したがって、2の累乗であることが知られているコンパイル時定数によるスケーリングは、乗算よりもさらに効率的です。


OK、現代に戻って...今より便利なことは、ビットシフトを使用して2つの8ビット値を16ビット整数に格納することです。たとえば、C#の場合:

// Byte1:11110000 // Byte2:00001111 Int16 value =((byte)(Byte1 >> 8)| Byte2)); //値= 000011111110000;

C ++では、コンパイラがこれを行う必要があります。2つの8ビットメンバーで構造体を作成しますが、実際には常にそうとは限りません。


ビットシフトを含むビット単位の演算は、低レベルのハードウェアまたは組み込みプログラミングの基本です。デバイスの仕様や一部のバイナリファイル形式を読むと、バイト、ワード、およびdwordが、さまざまな対象の値を含む、バイトに整列されていないビットフィールドに分割されていることがわかります。読み取り/書き込みのためにこれらのビットフィールドにアクセスすることが最も一般的な使用法です。

グラフィックプログラミングの簡単な実際の例は、16ビットピクセルが次のように表されることです。

ビット| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |ブルー|緑|赤|

緑の値を取得するには、次のようにします。

#define GREEN_MASK 0x7E0 #define GREEN_OFFSET 5 //緑を読み取るuint16_tgreen =(pixel&GREEN_MASK)>> GREEN_OFFSET;

説明

オフセット5で始まり10(つまり6ビット長)で終わる緑のみの値を取得するには、(ビット)マスクを使用する必要があります。これを16ビットピクセル全体に適用すると、次のようになります。私たちが興味を持っているのはほんの少しだけです。

#define GREEN_MASK 0x7E0

適切なマスクは0x7E0で、バイナリでは0000011111100000(10進数では2016)です。

uint16_t緑=(ピクセル&GREEN_MASK)...;

マスクを適用するには、AND演算子(&)を使用します。

uint16_t緑=(ピクセル&GREEN_MASK)>> GREEN_OFFSET;

マスクを適用すると、MSBが11ビットにあるため、実際には11ビットの数値である16ビットの数値になります。緑は実際には6ビットの長さしかないため、右シフト(11-6 = 5)を使用して縮小する必要があります。したがって、オフセットとして5を使用します(#define GREEN_OFFSET 5)。

また、2の累乗による高速な乗算と除算にビットシフトを使用することも一般的です。

私<<= x; // i *= 2^x; i>> =および; // i / = 2 ^ y;