第19回
いろいろな演算子~ビット演算子

ビット演算の必要な理由

ビット演算子を使わなくても、プログラムを作ることは可能です。わざわざ分かりにくいビット演算を行うより、int型などの変数を使った一般的な演算の方が分かりやすいはずです。なぜビット演算が必要なのかを考えてみましょう。

通常の処理では用いない

Cは高級言語ですから、 ~ & | ^ といった演算子によるビット単位の演算をしなくても、先の例でも取り上げたようにint型の整数で通常の演算をすれば同じ機能を実現できます。その方が、ソースも分かりやすくなります。

これも既に述べましたが、変数xの値を0にする場合もビットごとの排他的論理和より
x = 0;
とする方が、ソースとして遙かに分かりやすくなります。実際、一般的な四則演算子を使った計算式のソースも、コンパイルすれば合理的な機械語に変換されます。ソースレベルでビット単位の演算をする必要はほとんどないように思えます。

ではなぜ、Cにはこのようなビット演算子があるのでしょう?

先の例では、性別や組織を判断する処理を紹介しましたが、これはあくまでビット演算の例として取り上げたものです。実のところ、一般的な業務アプリケーションの開発ではビット演算を行うことはほとんどありません。先述したように、通常の四則演算やifなどの制御構造を使った方が分かりやすいためです。

ファイル属性の判別と設定

ビット単位の演算は、アセンブリ言語で高速な演算を行わせるため、CPU内部のレジスタを使った処理で多用されます。このような処理で典型的なものに、UNIX系OSで用いられるファイルの操作権限(パーミッション)の設定があります。

UNIX系OSでは、ファイルやディレクトリを読み書きする際の操作権限をw(書き込み可能)/r(読み取り可能)/x(実行可能)の3種類の記号で表しますが、その実体はファイルの属性として1ビットごとに上述の権限をON/OFFで割り当てた整数値です。

これに3ビットを使い、左からwrx の各権限を割り当てています。つまり
100:書き込みのみ可能
110:書き込みと読み取りが可能
101:書き込みと実行が可能
……といった形です。

さらに、この3つの権限が「所有者・所有者 の所属するグループ・一般ユーザー」の3種類のユーザー種別ごとに設定され、先頭にディレクトリならON、ファイルならOFFとなるビットを割り当てて計10ビットを使用します(図5)。

このような形態でファイルの各属性を調べたり、あるいは設定したりする場合にはビット演算が不可欠です。リスト2は、ユーザーごとの様々なパーミッションをビットパターンに定義し、属性を示す整数値とビット演算をして特定の状態を判別する処理の一部です。

また、TCP/IPプロトコルで用いられるIPアドレスでサブネットマスクによって特定の値を取り出す際にも、ビット演算が行われます。


リスト2:ファイルのパーミッションを調べる処理(一部のみ)
/* パーミッションのビットパターン */
#define  PTN_ALLOK      511  /* 0111111111:すべてONのファイル */
#define  PTN_DIRECTRY   512  /* 1000000000:ディレクトリ */
#define  PTN_OWNER      448  /* 0111000000:所有者 */
#define  PTN_GROUP       56  /* 0000111000:グループ */
#define  PTN_USER         7  /* 0000000111:ユーザー */
#define  PTN_OWNER_W    256  /* 0100000000:所有者・書き込み可 */
#define  PTN_OWNER_R    128  /* 0010000000:所有者・読み取り可 */
#define  PTN_OWNER_X     64  /* 0001000000:所有者・実行可 */
                         :
/* マクロ関数 */
#define isDirectry(n)  (n & PTN_DIRECTRY)  /* ディレクトリのとき真 */
#define isOwnerRead(n) (n & PTN_OWNER_R)   /* 所有者・読み取り可のとき真 */
                         :
/* 所有者の読み取り属性をOFF(不可)にする */
unsigned short int ToOwnerReadOff(unsigned short int n)
{
  unsigned short int bpattern;

  bpattern = PTNOWNER_R;      /* 0010000000 */
  /* ビットパターンを反転して論理和 -- 0のビットのみが0になる */
  return (n & (~bpattern));   /* ~bpattern = 1101111111 */
}
                         :

OSから組込みシステムまで

上記の他にも、OSやデバイスドライバなどハードウェアに近いプログラムでは、少ないメモリで高速な処理を行うためにビット列を使って状態などを表現することが多くあります。また、DVDデッキや炊飯器などの電化製品でROMに焼いて実装される、いわゆる組込み系のシステムでも、限られたメモリ容量で高速な処理を行うためにビット演算が多用されます。

Cはこのようなハードウェアに近いシステムでの使用も守備範囲とする、まさに汎用の言語です。ビット演算はOSやデバイスドライバ、組込みシステムのプログラムなどを開発する際には必要不可欠な機能なのです。

Cは、自分自身を産んだOSのUNIXを記述した言語として有名です。一般的なアプリケーションばかりではなく、OSまで記述できる実に細やかで柔軟な言語なのです。「高級アセンブラ」と呼ばれる理由がお分かりでしょう。

あとがき

hiropの『ちょっと気になる専門用語』~《n進数》

今回紹介したビット演算は、整数の値を2進数のビット列として扱います。コンピュータは2進数、人間は10進数の世界――と、よく言われます。今回は、この「n進数」についてちょっと考えてみます。

「n進数」とは1から数えていってnになったとき桁上がりして「10」(イチ・ゼロ)となる規則を持った数の数え方です。我々人間になじみの深い10進数では9の次で1桁上がって10になります。プログラミングでよく用いられる16進数では、9の次にA、B……とアルファベットを充てて1桁で進み、F(10進数の15)の次で10(10進数の16)と1桁上がります。

このことから、n進数では『1桁上がると値がn倍になる』ことが分かります。10進数で考えれば簡単ですね。10進数の30を1桁上げる(左シフトする)と300に、1桁下げる(右シフトする)と3になります。同様に16進数では1桁のシフトで16倍と1/16を求められます。しかし、このような方法で10倍や16倍を求めることができても、あまり大きな意味はありません。

ところが2進数では、1桁のシフト――値を1桁上下することで簡単に2倍と1/2を求めることができます。すると、本文で紹介したように3倍、5倍など様々な整数の乗除を効率的に計算できます。

0と1の2つの値しか持たない2進数は、10進数に慣れた人間には非常に煩雑で分かりにくい数え方ですが、コンピュータで高速な計算をするには、この『簡単に倍数を求められる』性質が実に好都合なのです。