intとunsigned intのシフト演算の違いについて
自作STGに固定小数点化を採用する時に、C++(C言語)におけるシフト演算で、多少悩んでしまったのでメモしておく。
C++のシフト演算では、intで変数を宣言した時と、unsigned intで変数を宣言した時とで振る舞いが異なる。
int型で変数を宣言した時には、符号bitが考慮されたシフト演算(算術シフト)となる。
それに対して、unsigned int型で変数を宣言した時には、符号bitが考慮されないシフト演算(論理シフト)となる。
具体的な例を以下に示す。
// 算術シフトと論理シフトの違いを示す例 #include <iostream> const int INITIAL_BIT_PATTERN = 0xFF789ACD; // 初期ビットパターン // 任意の型を2進数で表示するテンプレート関数。 template < typename T > void print_binary( T val ) { for( int i = sizeof( T ) * 8 - 1; i >= 0; --i ){ std::cout << ( ( val >> i ) & 0x1 ) << " "; } } int main() { int i; unsigned int ui; // 初期状態 i = INITIAL_BIT_PATTERN; ui = INITIAL_BIT_PATTERN; std::cout << "[Before]" << std::endl; std::cout << "i -> "; print_binary( i ); std::cout << std::endl; std::cout << "ui -> "; print_binary( ui ); std::cout << std::endl; // 右シフト i >>= 1; // 算術シフト ui >>= 1; // 論理シフト std::cout << "[Right shift]" << std::endl; std::cout << "i -> "; print_binary( i ); std::cout << std::endl; std::cout << "ui -> "; print_binary( ui ); std::cout << std::endl; // 左シフト i = INITIAL_BIT_PATTERN; ui = INITIAL_BIT_PATTERN; i <<= 1; // 算術シフト ui <<= 1; // 論理シフト std::cout << "[Left shift]" << std::endl; std::cout << "i -> "; print_binary( i ); std::cout << std::endl; std::cout << "ui -> "; print_binary( ui ); std::cout << std::endl; return 0; }
このプログラムをコンパイルして、実行した時の結果は以下のようになる。
[Before] i -> 1 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 1 0 1 1 0 0 1 1 0 1 ui -> 1 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 1 0 1 1 0 0 1 1 0 1 [Right shift] i -> 1 1 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 1 0 1 1 0 0 1 1 0 ui -> 0 1 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 1 0 1 1 0 0 1 1 0 [Left shift] i -> 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 1 0 1 1 0 0 1 1 0 1 0 ui -> 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 1 0 1 1 0 0 1 1 0 1 0
結果を見ればわかるように、右シフトを行うと、int型ではシフト前のMSBの値で埋められるのに対し、unsigned int型ではMSBは0となる。
このように違いが出るのは、int型で右シフトを行った時に、負数であっても1/2倍となる振る舞いを実現するためである。
同様にして右シフトについても、int型では右シフトの値は保存されるのに対し、unsigned int型では2番目のbitで書き換えられてしまう。
より詳しく理解するために、コンパイルされたプログラムのアセンブリコードを見てみる。
sarl $1, %eax # 算術右シフト(i>>1) movl %eax, -12(%rbp) movl -16(%rbp), %eax shrl $1, %eax # 論理右シフト(ui>>1) movl %eax, -16(%rbp) leaq L_.str4(%rip), %rax movq -24(%rbp), %rdi movq %rax, %rsi callq __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc movq %rax, %rdi movq -32(%rbp), %rsi callq __ZNSolsEPFRSoS_E movq -24(%rbp), %rdi movq -40(%rbp), %rsi callq __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc movl -12(%rbp), %eax movl %eax, %edi callq __Z12print_binaryIiEvT_ movq -24(%rbp), %rdi movq -32(%rbp), %rsi callq __ZNSolsEPFRSoS_E movq -24(%rbp), %rdi movq -48(%rbp), %rsi callq __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc movl -16(%rbp), %eax movl %eax, %edi callq __Z12print_binaryIjEvT_ movq -24(%rbp), %rdi movq -32(%rbp), %rsi callq __ZNSolsEPFRSoS_E movl $-8873267, -12(%rbp) movl $-8873267, -16(%rbp) movl -12(%rbp), %eax leal (,%rax,2), %eax # 算術左シフト(i<<1) movl %eax, -12(%rbp) movl -16(%rbp), %eax leal (,%rax,2), %eax # 算術左シフト(ui<<1)
どうやら左シフトは型では決まらないらしく、どちらも算術シフトとなった。
実際にソースコードのINITIAL_BIT_PATTERNを0x8F789ACDして実行した結果を以下に示す。
実行結果から、左シフトに関しては算術シフト演算が行われていることがわかる。
[Before] i -> 1 0 0 0 1 1 1 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 1 0 1 1 0 0 1 1 0 1 ui -> 1 0 0 0 1 1 1 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 1 0 1 1 0 0 1 1 0 1 [Right shift] i -> 1 1 0 0 0 1 1 1 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 1 0 1 1 0 0 1 1 0 ui -> 0 1 0 0 0 1 1 1 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 1 0 1 1 0 0 1 1 0 [Left shift] i -> 0 0 0 1 1 1 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 1 0 1 1 0 0 1 1 0 1 0 ui -> 0 0 0 1 1 1 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 1 0 1 1 0 0 1 1 0 1 0