私は暇なときにCを学ぼうとしていました、そして他の言語(C#、Javaなど)も同じ概念を持っています(そしてしばしば同じ演算子を使用します)...
私が思っているのは、コアレベルでは、ビットシフト(<<
、>>
、>>>
)は何をするのか、それはどのような問題を解決するのに役立つのでしょうか?言い換えれば、そのすべての長所を少し変えるための絶対的な初心者向けガイドです。
ビットシフト演算子は、その名前が意味するとおりに動作します。ビットをシフトします。これは、さまざまなシフト演算子についての簡単な説明です。
>>
は算術(または符号付き)右シフト演算子です。>>>
は、論理(または符号なし)右シフト演算子です。<<
は左シフト演算子で、論理シフトと算術シフトの両方のニーズを満たします。これらの演算子はすべて整数値に適用できます(int
、long
、場合によってはshort
およびbyte
またはchar
)。一部の言語では、int
より小さいデータ型にシフト演算子を適用すると、オペランドのサイズが自動的にint
に変更されます。
<<<
は冗長であるため、演算子ではありません。また、CとC++は右シフト演算子を区別しません。これらは>>
演算子のみを提供し、右シフトの振る舞いは符号付き型に対して定義された実装です。
整数は一連のビットとしてメモリに格納されます。たとえば、32ビットのint
として格納されている数値6は次のようになります。
00000000 00000000 00000000 00000110
このビットパターンを1つ左の位置(6 << 1
)にシフトすると、数値12になります。
00000000 00000000 00000000 00001100
ご覧のとおり、数字は1桁左にシフトし、右側の最後の数字はゼロで埋められています。また、左へのシフトは2のべき乗での乗算に相当します。したがって、6 << 1
は6 * 2
に相当し、6 << 3
は6 * 8
と等価です。最適化コンパイラとしては、可能であれば乗算をシフトに置き換えます。
これらはnot循環シフトです。この値を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
)に行くと、2,147,483,744となります。
10000000 00000000 00000000 01100000
それから((939,524,102 << 4) >>> 4
)シフトバックすると、134,217,734が得られます。
00001000 00000000 00000000 00000110
ビットを失うと元の値を取り戻すことはできません。
算術右シフトは、ゼロでパディングする代わりに最上位ビットでパディングすることを除いて、論理右シフトとまったく同じです。これは、最上位ビットがsignビット、つまり正数と負数を区別するビットであるためです。最上位ビットを埋め込むことによって、算術右シフトは符号を保存します。
たとえば、このビットパターンを負の数として解釈すると、
10000000 00000000 00000000 01100000
-2,147,483,552です。算術シフト(-2,147,483,552 >> 4)でこれを右の4つの位置にシフトすると、次のようになります。
11111000 00000000 00000000 00000110
または-134,217,722の数。
したがって、論理右シフトではなく算術右シフトを使用して、負数の符号を保持していることがわかります。またしても、2のべき乗による除算を行っていることがわかります。
単一のバイトがあるとしましょう:
0110110
単一の左ビットシフトを適用すると、次のようになります。
1101100
左端のゼロがバイトからシフトアウトされ、バイトの右端に新しいゼロが追加されました。
ビットはロールオーバーしません。それらは破棄されます。つまり、1101100を左にシフトしてから右にシフトすると、同じ結果は得られません。
Nだけ左にシフトすることは、2を乗算することと同じです。N。
Nだけ右にシフトすることは( one'scomplement を使用している場合)は2で除算することと同等ですN ゼロに丸めます。
2のべき乗で作業している場合、ビットシフトは非常に高速な乗算と除算に使用できます。ほとんどすべての低レベルグラフィックルーチンはビットシフトを使用します。
たとえば、昔のことですが、ゲームにはモード13h(320x200 256色)を使用していました。モード13hでは、ビデオメモリはピクセルごとに順番にレイアウトされました。これは、ピクセルの位置を計算することを意味し、次の数学を使用します。
memoryOffset = (row * 320) + column
さて、その日と時代に戻って、速度が重要だったので、ビットシフトを使用してこの操作を行いました。
ただし、320は2のべき乗ではないため、これを回避するには、加算された2のべき乗とは何かを調べる必要があります。
(row * 320) = (row * 256) + (row * 64)
これを左シフトに変換できます:
(row * 320) = (row << 8) + (row << 6)
最終結果:
memoryOffset = ((row << 8) + (row << 6)) + column
今度は以前と同じオフセットを取得しますが、高価な乗算演算の代わりに2つのビットシフトを使用します...いくつかの間違いと32ビットの例を追加)):
mov ax, 320; 2 cycles
mul Word [row]; 22 CPU Cycles
mov di,ax; 2 cycles
add di, [column]; 2 cycles
; di = [row]*320 + [column]
; 16-bit addressing mode limitations:
; [di] is a valid addressing mode, but [ax] isn't, otherwise we could skip the last mov
合計:古代のCPUがこれらのタイミングを持っていたとしても28サイクル。
Vrs
mov ax, [row]; 2 cycles
mov di, ax; 2
shl ax, 6; 2
shl di, 8; 2
add di, ax; 2 (320 = 256+64)
add di, [column]; 2
; di = [row]*(256+64) + [column]
同じ古代のCPUで12サイクル。
はい、16 CPUサイクルを削減するために一生懸命働きます。
32ビットモードまたは64ビットモードでは、両方のバージョンが大幅に短縮され、高速になります。 Intel Skylake( http://agner.org/optimize/ を参照)のような最新のアウトオブオーダー実行CPUは、非常に高速なハードウェア乗算(低レイテンシおよび高スループット)を備えているため、ゲインははるかに小さくなります。 AMD Bulldozerファミリは、特に64ビット乗算の場合、少し遅くなります。 Intel CPUおよびAMD Ryzenでは、2つのシフトはレイテンシがわずかに低くなりますが、乗算よりも命令が多くなります(スループットが低下する可能性があります)。
imul edi, [row], 320 ; 3 cycle latency from [row] being ready
add edi, [column] ; 1 cycle latency (from [column] and edi being ready).
; edi = [row]*(256+64) + [column], in 4 cycles from [row] being ready.
vs.
mov edi, [row]
shl edi, 6 ; row*64. 1 cycle latency
lea edi, [edi + edi*4] ; row*(64 + 64*4). 1 cycle latency
add edi, [column] ; 1 cycle latency from edi and [column] both being ready
; edi = [row]*(256+64) + [column], in 3 cycles from [row] being ready.
コンパイラがこれを行います。 gcc、clang、およびMSVCがすべてreturn 320*row + col;
を最適化するときにshift + leaを使用する方法を参照してください。
ここで注意すべき最も興味深いことは、 x86にはshift-and-add命令(LEA
) があり、小さな左シフトを実行し、同時に追加できることです。 add
命令。 ARMはさらに強力です。任意の命令の1つのオペランドを自由に左または右にシフトできます。したがって、2のべき乗であることが知られているコンパイル時定数によるスケーリングは、乗算よりもさらに効率的です。
わかりました、現代に戻って...今より便利なのは、ビットシフトを使用して2つの8ビット値を16ビット整数に格納することです。たとえば、C#の場合:
// Byte1: 11110000
// Byte2: 00001111
Int16 value = ((byte)(Byte1 >> 8) | Byte2));
// value = 000011111110000;
C++では、2つの8ビットメンバーでstruct
を使用した場合、コンパイラがこれを行う必要がありますが、実際には常にそうとは限りません。
ビットシフトを含むビット単位の操作は、低レベルのハードウェアまたは組み込みプログラミングにとって基本です。デバイスの仕様やバイナリファイル形式を読むと、バイト、ワード、およびdwordが、バイト単位で整列されていないビットフィールドに分割されています。読み書きのためにこれらのビットフィールドにアクセスするのが最も一般的な使い方です。
グラフィックプログラミングの簡単な実例は、16ビットピクセルが次のように表されることです。
bit | 15| 14| 13| 12| 11| 10| 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| Blue | Green | Red |
緑色の値を取得するには、次のようにします。
#define GREEN_MASK 0x7E0
#define GREEN_OFFSET 5
// Read green
uint16_t green = (pixel & GREEN_MASK) >> GREEN_OFFSET;
説明
オフセット5で始まり10(つまり6ビット長)で終わる緑色の値のみを取得するには、(ビット)マスクを使用する必要があります。これを16ビットピクセル全体に適用すると、興味のある部分だけ.
#define GREEN_MASK 0x7E0
適切なマスクは0x7E0で、これは2進数で0000011111100000(10進数で2016)です。
uint16_t green = (pixel & GREEN_MASK) ...;
マスクを適用するには、AND演算子(&)を使います。
uint16_t green = (pixel & GREEN_MASK) >> GREEN_OFFSET;
マスクを適用した後、そのMSBが11番目のビットにあるので、実際にはちょうど11ビットの数値である16ビットの数値になります。 Greenは実際には6ビット長なので、右シフト(11 - 6 = 5)を使用して縮小する必要があります。したがって、offset(#define GREEN_OFFSET 5
)として5を使用します。
高速乗算と2のべき乗による除算にビットシフトを使用することも一般的です。
i <<= x; // i *= 2^x;
i >>= y; // i /= 2^y;
ビットシフトは、低レベルのグラフィックプログラミングでよく使用されます。たとえば、32ビットのWordでエンコードされた特定のピクセルカラー値。
Pixel-Color Value in Hex: B9B9B900
Pixel-Color Value in Binary: 10111001 10111001 10111001 00000000
よりよく理解するために、どのセクションでラベル付けされた同じ2進値は、どの色部分を表します。
Red Green Blue Alpha
Pixel-Color Value in Binary: 10111001 10111001 10111001 00000000
たとえば、このピクセルカラーの緑色の値を取得したいとしましょう。その値は マスキング と シフト で簡単に得ることができます。
私たちのマスク:
Red Green Blue Alpha
color : 10111001 10111001 10111001 00000000
green_mask : 00000000 11111111 00000000 00000000
masked_color = color & green_mask
masked_color: 00000000 10111001 00000000 00000000
論理的な&
演算子は、マスクが1の値だけが保持されるようにします。今しなければならない最後のことは、それらのすべてのビットを16桁右にシフトすることによって正しい整数値を取得することです (論理右シフト) 。
green_value = masked_color >>> 16
Etvoilá、ピクセルの色で緑の量を表す整数があります。
Pixels-Green Value in Hex: 000000B9
Pixels-Green Value in Binary: 00000000 00000000 00000000 10111001
Pixels-Green Value in Decimal: 185
これは、jpg
、png
、...
などの画像フォーマットのエンコードまたはデコードによく使用されます。
1つの落とし穴は、次のものが実装に依存していることです(ANSI規格に準拠)。
char x = -1;
x >> 1;
xは127(01111111)または-1(11111111)のままです。
実際には、それは通常後者です。
私はヒントやコツだけを書いていますが、テスト/試験に役立つことがあります。
n = n*2
:n = n<<1
n = n/2
:n = n>>1
!(n & (n-1))
をチェックするn
の一部:n |= (1 << x)
x&1 == 0
(even)x ^ (1<<n)
Javaの実装では、シフトするビット数はソースのサイズによって変更されます。
例えば:
(long) 4 >> 65
2に等しいです。ビットを65回右にシフトすると、すべてがゼロになると予想されるかもしれませんが、実際には次のようになります。
(long) 4 >> (65 % 64)
これは<<、>>、>>>にも当てはまります。他の言語で試したことはありません。
Pythonでの便利なビット操作/操作Pythonで@Ravi Prakash回答を実装しました。
# basic bit operations
# int to bin
print(bin(10))
# bin to int
print(int('1010',2))
# multiplying x with 2 .... x**2== x << 1
print(200<<1)
# dividing x with 2 .... x /2 == x >> 1
print(200>>1)
# modulo x with 2 .... x%2 == x&1
if 20&1==0:
print("20 is a even number")
# check if n is power of 2 : check !(n & (n-1))
print(not(33 &(33-1)))
# getting xth bit of n : (n>>x)&1
print((10>>2)&1) # bin of 10==1010 and 2nd bit is 0
# toggle nth bit of x : x^(1<<n)
# take bin(10)==1010 and toggling 2nd bit in bin(10) we get 1110 === bin(14)
print(10^(1<<2))