私は暇なときにCを学ぼうとしましたが、他の言語(C#、Javaなど)は同じ概念(そして多くの場合同じ演算子)を持っています...
私は思ったんだけどは、コアレベルでは、何が(ビットシフトんされ<<
、>>
、>>>
)、それが解決する何の問題を助けることができる、そしてどのような落とし穴は、曲げの周りに潜んでいるのですか?言い換えれば、そのすべての良さをビットシフトするための絶対初心者向けガイドです。
私は暇なときにCを学ぼうとしましたが、他の言語(C#、Javaなど)は同じ概念(そして多くの場合同じ演算子)を持っています...
私は思ったんだけどは、コアレベルでは、何が(ビットシフトんされ<<
、>>
、>>>
)、それが解決する何の問題を助けることができる、そしてどのような落とし穴は、曲げの周りに潜んでいるのですか?言い換えれば、そのすべての良さをビットシフトするための絶対初心者向けガイドです。
回答:
ビットシフト演算子は、その名前が示すとおりに動作します。彼らはビットをシフトします。以下は、さまざまなシフト演算子の簡単な(またはそれほど簡単ではない)紹介です。
>>
算術(または符号付き)右シフト演算子です。>>>
論理(または符号なし)右シフト演算子です。<<
左シフト演算子であり、論理シフトと算術シフトの両方のニーズを満たします。これらの演算子の全ては、(整数値に適用することができint
、long
おそらく、short
及びbyte
又はchar
)。一部の言語では、シフト演算子をより小さいデータ型に適用するとint
、オペランドのサイズがに自動的に変更されますint
。
<<<
は冗長になるため、は演算子ではありません。
また、CおよびC ++は右シフト演算子を区別しないことに注意してください。これらは>>
演算子のみを提供し、右シフト動作は符号付きの型に対して定義された実装です。残りの回答では、C#/ Java演算子を使用しています。
(GCCおよびClang / LLVMを含むすべての主流のCおよびC ++実装では>>
、符号付きの型は算術です。一部のコードはこれを前提としていますが、これは標準で保証されているものではありません。ただし、undefinedではありません。しかし、負の符号付き数値の左シフトは未定義の動作です(符号付き整数オーバーフロー)。したがって、算術右シフトが必要でない限り、通常は符号なしの型でビットシフトを行うことをお勧めします。)
整数は一連のビットとしてメモリに格納されます。たとえば、32ビットとして保存される数値6 int
は次のようになります。
00000000 00000000 00000000 00000110
このビットパターンを左に1ポジション(6 << 1
)シフトすると、数値は12になります。
00000000 00000000 00000000 00001100
ご覧のとおり、数字は左に1桁シフトしており、右側の最後の数字はゼロで埋められています。左シフトは2のべき乗による乗算と同等であることに注意してください。したがって、と6 << 1
同等であり6 * 2
、と6 << 3
同等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
)には、2,147,483,744が得られます。
10000000 00000000 00000000 01100000
次にシフトバック((939,524,102 << 4) >>> 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の累乗による除算を実行していることがわかります。
A good optimizing compiler will substitute shifts for multiplications when possible.
何?Bitshiftsは、それがCPUの低レベルの操作にダウンしていたときに、優れた最適化コンパイラはどうしたら速く桁違いに正確なビットシフトに2のべき乗で、通常の乗算を回し、ある反対を。
シングルバイトがあるとしましょう:
0110110
1つの左ビットシフトを適用すると、次のようになります。
1101100
左端のゼロはバイトからシフトアウトされ、新しいゼロがバイトの右端に追加されました。
ビットはロールオーバーしません。それらは破棄されます。つまり、シフト1101100を左にシフトしてから右にシフトすると、同じ結果が返されません。
左にNシフトすることは、2 Nを掛けることと同じです。
Nだけ右シフトすることは(1の補数を使用している場合)、2 Nで除算してゼロに丸めることと同じです。
2の累乗で作業している場合、ビットシフトは非常に高速な乗算と除算に使用できます。ほとんどすべての低レベルグラフィックルーチンはビットシフトを使用します。
たとえば、昔、ゲームにはモード13h(320x200 256色)を使用していました。モード13hでは、ビデオメモリはピクセルごとに順番に配置されました。これは、ピクセルの位置を計算することを意味し、次の計算を使用します。
memoryOffset = (row * 320) + column
さて、その時代には、速度が重要でした。そのため、ビットシフトを使用してこの操作を行いました。
ただし、320は2の累乗ではないため、これを回避するには、2の累乗を加算すると320になります。
(row * 320) = (row * 256) + (row * 64)
これを左シフトに変換できます:
(row * 320) = (row << 8) + (row << 6)
最終結果:
memoryOffset = ((row << 8) + (row << 6)) + column
これで、以前と同じオフセットが得られますが、高価な乗算演算の代わりに、2つのビットシフトを使用します... x86では、次のようなものになります(注、私が組み立てを行って以来ずっとずっと続いています(編集者注:修正済み)。いくつかのミスと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サイクル。
VR
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.
対
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、およびMicrosoft Visual C ++がすべてshift + leareturn 320*row + col;
をどのように使用するかを確認してください。
ここで注目すべき最も興味深いことは、x86にはシフトと追加の命令(LEA
)があり、add
命令としてのパフォーマンスで、小さな左シフトと追加を同時に実行できることです。ARMはさらに強力です。命令の1つのオペランドを自由に左または右にシフトできます。したがって、2の累乗であることがわかっているコンパイル時定数によるスケーリングは、乗算よりもさらに効率的です。
さて、現代に戻って...今より便利なのは、ビットシフトを使用して2つの8ビット値を16ビット整数に格納することです。たとえば、C#の場合:
// Byte1: 11110000
// Byte2: 00001111
Int16 value = ((byte)(Byte1 >> 8) | Byte2));
// value = 000011111110000;
C ++では、struct
2つの8ビットメンバーでを使用した場合、コンパイラーがこれを行う必要がありますが、実際には常にそうとは限りません。
c=4*d
ばあなたはシフトを得るでしょう。あなたが書く場合k = (n<0)
、それもシフトで行われるかもしれません:k = (n>>31)&1
分岐を避けるため。結論として、このコンパイラの巧妙さの向上は、Cコードでこれらのトリックを使用する必要がなくなり、読みやすさと移植性が損なわれることを意味します。たとえばSSEベクトルコードを記述している場合は、それらを知ることは非常に良いことです。または、それを高速に必要とし、コンパイラーが使用していないトリック(GPUコードなど)がある状況。
if(x >= 1 && x <= 9)
て行うことができif( (unsigned)(x-1) <=(unsigned)(9-1))
、大きな速度の利点することができ一から二条件テストを変更します。特に、ブランチの代わりに述語実行を許可する場合。10年前にコンパイラがオプティマイザでこの変換を開始したことに気づくまで、私はこれを何年も(正当化された場合)使用し、その後停止しました。コンパイラはあなたのために変換を行うことができない同様の状況があるので、まだ知っておくと良いでしょう。または、コンパイラに取り組んでいる場合。
ビットシフトを含むビット単位の演算は、低レベルのハードウェアまたは組み込みプログラミングの基本です。デバイスの仕様または一部のバイナリファイル形式を読み取ると、バイト、ワード、および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で、これはバイナリでは0000011111100000(10進数では2016)です。
uint16_t green = (pixel & GREEN_MASK) ...;
マスクを適用するには、AND演算子(&)を使用します。
uint16_t green = (pixel & GREEN_MASK) >> GREEN_OFFSET;
マスクを適用すると、MSBが11番目のビットにあるため、16ビットの数値になり、これは実際には11ビットの数値にすぎません。緑は実際には6ビット長しかないため、右シフト(11-6 = 5)を使用して縮小する必要があるため、オフセットとして5を使用します(#define GREEN_OFFSET 5
)。
また、ビット乗算を使用して、2の累乗で乗算および除算を高速に行うことも一般的です。
i <<= x; // i *= 2^x;
i >>= y; // i /= 2^y;
ビットシフトは、低レベルのグラフィックプログラミングでよく使用されます。たとえば、32ビットワードにエンコードされた特定のピクセルカラー値。
Pixel-Color Value in Hex: B9B9B900
Pixel-Color Value in Binary: 10111001 10111001 10111001 00000000
よりよく理解するために、どのセクションでラベル付けされた同じバイナリ値は、どの色の部分を表すか
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
ちなみに、ピクセルの色の緑の量を表す整数があります。
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
偶数)x ^ (1<<n)
Java実装では、シフトするビット数はソースのサイズによって変更されることに注意してください。
例えば:
(long) 4 >> 65
2に等しい。ビットを右に65回シフトすると、すべてがゼロになると予想されるかもしれませんが、実際には次と同等です。
(long) 4 >> (65 % 64)
これは、<<、>>、および>>>に当てはまります。他の言語では試していません。
Pythonでのいくつかの便利なビット操作/操作。
私は実装ラヴィ・プラカシュの答えを Pythonで。
# Basic bit operations
# Integer to binary
print(bin(10))
# Binary to integer
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 second bit is 0
# Toggle nth bit of x : x^(1 << n)
# take bin(10) == 1010 and toggling second bit in bin(10) we get 1110 === bin(14)
print(10^(1 << 2))
Windowsプラットフォームでは32ビットバージョンのPHPしか使用できないことに注意してください。
次に、たとえば、<<または>>を31ビット以上シフトすると、予期しない結果になります。通常、ゼロではなく元の数値が返されますが、これは本当にトリッキーなバグになる可能性があります。
もちろん、64ビットバージョンのPHP(Unix)を使用している場合は、63ビットを超えるシフトは避けてください。ただし、たとえば、MySQLは64ビットのBIGINTを使用するため、互換性の問題はありません。
更新:PHP 7 Windowsから、PHPビルドは最終的に完全な64ビット整数を使用できるようになりました:整数 のサイズはプラットフォームに依存しますが、最大値は約20億が通常の値です(32ビットが署名されています)。64ビットプラットフォームの最大値は通常9E18ですが、PHP 7より前のWindowsでは常に32ビットでした。